diff --git a/Cargo.lock b/Cargo.lock index 859576557..3e8093d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,7 +172,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -183,7 +183,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -203,9 +203,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "arraydeque" @@ -356,9 +359,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -515,9 +518,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" dependencies = [ "aws-lc-sys", "zeroize", @@ -525,9 +528,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" dependencies = [ "cc", "cmake", @@ -565,11 +568,11 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "base64 0.22.1", "bytes", "form_urlencoded", @@ -618,9 +621,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -1012,9 +1015,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "by_address" @@ -1042,9 +1045,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -1141,9 +1144,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -1211,7 +1214,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.7", + "axum 0.8.8", "cdk", "cdk-prometheus", "futures", @@ -1354,7 +1357,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.7", + "axum 0.8.8", "bip39", "bitcoin 0.32.8", "cashu", @@ -1396,7 +1399,7 @@ name = "cdk-ldk-node" version = "0.14.0" dependencies = [ "async-trait", - "axum 0.8.7", + "axum 0.8.8", "cdk-common", "futures", "ldk-node", @@ -1484,7 +1487,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.7", + "axum 0.8.8", "bip39", "bitcoin 0.32.8", "cdk", @@ -1834,9 +1837,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -1917,7 +1920,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "winnow 0.7.14", "yaml-rust2", ] @@ -2450,7 +2453,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2671,7 +2674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2796,9 +2799,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -3323,11 +3326,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3830,9 +3833,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -3986,13 +3989,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -4008,9 +4011,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" dependencies = [ "zlib-rs", ] @@ -4440,9 +4443,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" dependencies = [ "async-lock", "crossbeam-channel", @@ -4453,7 +4456,6 @@ dependencies = [ "futures-util", "parking_lot", "portable-atomic", - "rustc_version", "smallvec", "tagptr", "uuid", @@ -4506,9 +4508,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.43.1" +version = "0.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62a97d745f1bd8d5e05a978632bbb87b0614567d5142906fe7c86fb2440faac6" +checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ "aes", "base64 0.22.1", @@ -4519,6 +4521,7 @@ dependencies = [ "chacha20", "chacha20poly1305", "getrandom 0.2.16", + "hex", "instant", "scrypt", "secp256k1 0.29.1", @@ -4530,24 +4533,34 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c75a8c2175d2785ba73cfddef21d1e30da5fbbdf158569b6808ba44973a15b" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" dependencies = [ "lru", "nostr", "tokio", ] +[[package]] +name = "nostr-gossip" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +dependencies = [ + "nostr", +] + [[package]] name = "nostr-relay-pool" -version = "0.43.1" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2f43b70d13dfc50508a13cd902e11f4625312b2ce0e4b7c4c2283fd04001bd" +checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" dependencies = [ "async-utility", "async-wsocket", "atomic-destructor", + "hex", "lru", "negentropy", "nostr", @@ -4558,22 +4571,24 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.43.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599f8963d6a1522a13b1a2b0ea6e168acfc367706606f1d33fa595e91fa22db0" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" dependencies = [ "async-utility", "nostr", "nostr-database", + "nostr-gossip", "nostr-relay-pool", "tokio", + "tracing", ] [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" dependencies = [ "winapi", ] @@ -4584,7 +4599,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4850,7 +4865,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -5160,9 +5175,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" [[package]] name = "possiblyrandom" @@ -5309,7 +5324,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.9", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -5729,6 +5744,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -5802,9 +5826,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.25" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -6022,15 +6046,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6093,9 +6117,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "web-time", "zeroize", @@ -6131,9 +6155,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "safelog" @@ -6199,9 +6223,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -6428,15 +6452,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -6461,9 +6485,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -6492,7 +6516,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -6575,10 +6599,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6911,15 +6936,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", - "windows-sys 0.52.0", + "rustix 1.1.3", + "windows-sys 0.61.2", ] [[package]] @@ -7315,13 +7340,13 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] @@ -7337,9 +7362,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -7371,21 +7396,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow 0.7.14", ] @@ -7433,7 +7458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", - "axum 0.8.7", + "axum 0.8.8", "base64 0.22.1", "bytes", "h2 0.4.12", @@ -8278,9 +8303,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -8313,9 +8338,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -8415,9 +8440,9 @@ dependencies = [ [[package]] name = "typed-index-collections" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd393dbd1e7b23e0cab7396570309b4068aa504e9dac2cd41d827583b4e9ab7" +checksum = "5318ee4ce62a4e948a33915574021a7a953d83e84fba6e25c72ffcfd7dad35ff" dependencies = [ "bincode", "serde", @@ -8742,7 +8767,7 @@ version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ - "axum 0.8.7", + "axum 0.8.8", "base64 0.22.1", "mime_guess", "regex", @@ -9070,7 +9095,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -9637,9 +9662,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + +[[package]] +name = "zmij" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" [[package]] name = "zopfli" diff --git a/Cargo.lock.msrv b/Cargo.lock.msrv index 36a2dd9b8..a6769537b 100644 --- a/Cargo.lock.msrv +++ b/Cargo.lock.msrv @@ -203,9 +203,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "arraydeque" @@ -356,9 +359,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -565,11 +568,11 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "base64 0.22.1", "bytes", "form_urlencoded", @@ -618,9 +621,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -1141,9 +1144,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -1211,7 +1214,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.7", + "axum 0.8.8", "cdk", "cdk-prometheus", "futures", @@ -1353,7 +1356,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.7", + "axum 0.8.8", "bip39", "bitcoin 0.32.8", "cashu", @@ -1395,7 +1398,7 @@ name = "cdk-ldk-node" version = "0.14.0" dependencies = [ "async-trait", - "axum 0.8.7", + "axum 0.8.8", "cdk-common", "futures", "ldk-node", @@ -1483,7 +1486,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.7", + "axum 0.8.8", "bip39", "bitcoin 0.32.8", "cdk", @@ -2795,9 +2798,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -3829,9 +3832,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -3985,13 +3988,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -4007,9 +4010,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" dependencies = [ "zlib-rs", ] @@ -4439,9 +4442,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" dependencies = [ "async-lock", "crossbeam-channel", @@ -4452,7 +4455,6 @@ dependencies = [ "futures-util", "parking_lot", "portable-atomic", - "rustc_version", "smallvec", "tagptr", "uuid", @@ -4505,9 +4507,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.43.1" +version = "0.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62a97d745f1bd8d5e05a978632bbb87b0614567d5142906fe7c86fb2440faac6" +checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ "aes", "base64 0.22.1", @@ -4518,6 +4520,7 @@ dependencies = [ "chacha20", "chacha20poly1305", "getrandom 0.2.16", + "hex", "instant", "scrypt", "secp256k1 0.29.1", @@ -4529,24 +4532,34 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c75a8c2175d2785ba73cfddef21d1e30da5fbbdf158569b6808ba44973a15b" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" dependencies = [ "lru", "nostr", "tokio", ] +[[package]] +name = "nostr-gossip" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +dependencies = [ + "nostr", +] + [[package]] name = "nostr-relay-pool" -version = "0.43.1" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2f43b70d13dfc50508a13cd902e11f4625312b2ce0e4b7c4c2283fd04001bd" +checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" dependencies = [ "async-utility", "async-wsocket", "atomic-destructor", + "hex", "lru", "negentropy", "nostr", @@ -4557,22 +4570,24 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.43.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599f8963d6a1522a13b1a2b0ea6e168acfc367706606f1d33fa595e91fa22db0" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" dependencies = [ "async-utility", "nostr", "nostr-database", + "nostr-gossip", "nostr-relay-pool", "tokio", + "tracing", ] [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" dependencies = [ "winapi", ] @@ -5159,9 +5174,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" [[package]] name = "possiblyrandom" @@ -5730,9 +5745,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags 2.10.0", ] @@ -5810,9 +5825,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -6030,9 +6045,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -6139,9 +6154,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "safelog" @@ -6207,9 +6222,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -6436,15 +6451,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -6500,7 +6515,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -6583,10 +6598,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6919,14 +6935,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.52.0", ] @@ -7441,7 +7457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", - "axum 0.8.7", + "axum 0.8.8", "base64 0.22.1", "bytes", "h2 0.4.12", @@ -8286,9 +8302,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -8321,9 +8337,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -8750,7 +8766,7 @@ version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ - "axum 0.8.7", + "axum 0.8.8", "base64 0.22.1", "mime_guess", "regex", @@ -9645,9 +9661,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + +[[package]] +name = "zmij" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index d2d1ae673..9e4e4d41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,7 +115,7 @@ strum = "0.27.1" strum_macros = "0.27.1" rustls = { version = "0.23.27", default-features = false, features = ["ring"] } prometheus = { version = "0.13.4", features = ["process"], default-features = false } -nostr-sdk = { version = "0.43.0", default-features = false, features = [ +nostr-sdk = { version = "0.44.1", default-features = false, features = [ "nip04", "nip44", "nip59" diff --git a/README.md b/README.md index 3d41f77be..ad6032792 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ For a guide to settings up a development environment see [DEVELOPMENT.md](./DEVE | [22][22] | Blind Authentication | :heavy_check_mark: | | [23][23] | Payment Method: BOLT11 | :heavy_check_mark: | | [25][25] | Payment Method: BOLT12 | :heavy_check_mark: | +| [26][26] | Payment Request Bech32m Encoding | :heavy_check_mark: | +| [27][27] | Nostr Mint Backup | :heavy_check_mark: | ## License @@ -120,3 +122,5 @@ Please see the [development guide](DEVELOPMENT.md). [22]: https://github.com/cashubtc/nuts/blob/main/22.md [23]: https://github.com/cashubtc/nuts/blob/main/23.md [25]: https://github.com/cashubtc/nuts/blob/main/25.md +[26]: https://github.com/cashubtc/nuts/blob/main/26.md +[27]: https://github.com/cashubtc/nuts/blob/main/27.md diff --git a/crates/cashu/Cargo.toml b/crates/cashu/Cargo.toml index 950a04979..1fb203be0 100644 --- a/crates/cashu/Cargo.toml +++ b/crates/cashu/Cargo.toml @@ -16,6 +16,7 @@ swagger = ["dep:utoipa"] mint = [] wallet = [] auth = ["dep:strum", "dep:strum_macros", "dep:regex"] +nostr = ["dep:nostr-sdk"] bench = [] [dependencies] @@ -36,6 +37,7 @@ serde_with.workspace = true regex = { workspace = true, optional = true } strum = { workspace = true, optional = true } strum_macros = { workspace = true, optional = true } +nostr-sdk = { workspace = true, optional = true } zeroize = "1" web-time.workspace = true diff --git a/crates/cashu/src/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index 6d4d7cb54..b7e362ac2 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/crates/cashu/src/nuts/mod.rs @@ -26,6 +26,8 @@ pub mod nut20; pub mod nut23; pub mod nut25; pub mod nut26; +#[cfg(all(feature = "wallet", feature = "nostr"))] +pub mod nut27; #[cfg(feature = "auth")] mod auth; @@ -74,3 +76,7 @@ pub use nut23::{ MintQuoteBolt11Response, QuoteState as MintQuoteState, }; pub use nut25::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response}; +#[cfg(all(feature = "wallet", feature = "nostr"))] +pub use nut27::{ + backup_filter_params, create_backup_event, decrypt_backup_event, derive_nostr_keys, MintBackup, +}; diff --git a/crates/cashu/src/nuts/nut26/encoding.rs b/crates/cashu/src/nuts/nut26/encoding.rs index b9679a5e7..ec44a1ecc 100644 --- a/crates/cashu/src/nuts/nut26/encoding.rs +++ b/crates/cashu/src/nuts/nut26/encoding.rs @@ -134,9 +134,10 @@ impl PaymentRequest { /// # Examples /// /// ``` + /// use std::str::FromStr; + /// /// use cashu::nuts::nut18::PaymentRequest; /// use cashu::{Amount, MintUrl}; - /// use std::str::FromStr; /// /// let payment_request = PaymentRequest { /// payment_id: Some("test123".to_string()), diff --git a/crates/cashu/src/nuts/nut27.rs b/crates/cashu/src/nuts/nut27.rs new file mode 100644 index 000000000..a981261e3 --- /dev/null +++ b/crates/cashu/src/nuts/nut27.rs @@ -0,0 +1,406 @@ +//! NUT-27: Nostr Mint Backup +//! +//! +//! +//! This NUT describes a method for wallets to backup their mint list as Nostr events. +//! The backup keys are deterministically derived from the wallet's mnemonic seed phrase. + +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; +use nostr_sdk::nips::nip44::{self, Version}; +use nostr_sdk::{Event, EventBuilder, Keys, Kind, Tag, TagKind}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::mint_url::MintUrl; + +/// Domain separator for mint backup key derivation +const DOMAIN_SEPARATOR: &[u8] = b"cashu-mint-backup"; + +/// Event kind for addressable events (NIP-78) +const KIND_APPLICATION_SPECIFIC_DATA: u16 = 30078; + +/// The "d" tag identifier for mint list backup +const MINT_LIST_IDENTIFIER: &str = "mint-list"; + +/// NUT-27 Error +#[derive(Debug, Error)] +pub enum Error { + /// Nostr key error + #[error(transparent)] + NostrKey(#[from] nostr_sdk::key::Error), + /// Nostr event builder error + #[error(transparent)] + NostrEventBuilder(#[from] nostr_sdk::event::builder::Error), + /// NIP-44 encryption error + #[error(transparent)] + Nip44(#[from] nip44::Error), + /// JSON serialization error + #[error(transparent)] + Json(#[from] serde_json::Error), + /// Invalid event kind + #[error("Invalid event kind: expected {expected}, got {got}")] + InvalidEventKind { + /// Expected kind + expected: u16, + /// Actual kind + got: u16, + }, + /// Missing "d" tag + #[error("Missing 'd' tag with identifier '{0}'")] + MissingIdentifierTag(String), +} + +/// Mint backup data structure +/// +/// This represents the plaintext data that gets encrypted in the Nostr event content. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintBackup { + /// List of mint URLs + pub mints: Vec, + /// Unix timestamp of when the backup was created + pub timestamp: u64, +} + +impl MintBackup { + /// Create a new mint backup with the current timestamp + pub fn new(mints: Vec) -> Self { + let timestamp = web_time::SystemTime::now() + .duration_since(web_time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + Self { mints, timestamp } + } + + /// Create a new mint backup with a specific timestamp + pub fn with_timestamp(mints: Vec, timestamp: u64) -> Self { + Self { mints, timestamp } + } +} + +/// Derive Nostr keys for mint backup from a BIP39 seed +/// +/// The derivation follows the NUT-27 specification: +/// 1. Concatenate the seed with the domain separator "cashu-mint-backup" +/// 2. Hash the combined data with SHA256 to produce the private key +/// 3. Derive the public key from the private key +/// +/// # Arguments +/// +/// * `seed` - A 64-byte seed derived from a BIP39 mnemonic +/// +/// # Returns +/// +/// A nostr_sdk `Keys` struct containing the derived secret and public keys +/// +/// # Example +/// +/// ``` +/// use std::str::FromStr; +/// +/// use bip39::Mnemonic; +/// use cashu::nuts::nut27::derive_nostr_keys; +/// +/// let mnemonic = Mnemonic::from_str( +/// "half depart obvious quality work element tank gorilla view sugar picture humble", +/// ) +/// .unwrap(); +/// let seed: [u8; 64] = mnemonic.to_seed(""); +/// let keys = derive_nostr_keys(&seed).unwrap(); +/// ``` +pub fn derive_nostr_keys(seed: &[u8; 64]) -> Result { + let mut combined_data = Vec::with_capacity(seed.len() + DOMAIN_SEPARATOR.len()); + combined_data.extend_from_slice(seed); + combined_data.extend_from_slice(DOMAIN_SEPARATOR); + + let hash = Sha256Hash::hash(&combined_data); + let private_key_bytes = hash.to_byte_array(); + + let secret_key = nostr_sdk::SecretKey::from_slice(&private_key_bytes)?; + let keys = Keys::new(secret_key); + + Ok(keys) +} + +/// Create a Nostr backup event for the mint list +/// +/// This creates a NIP-78 addressable event (kind 30078) with the mint list +/// encrypted using NIP-44. The event is self-encrypted using the same key +/// for both sender and receiver. +/// +/// # Arguments +/// +/// * `keys` - The Nostr keys derived from the wallet seed +/// * `backup` - The mint backup data to encrypt +/// * `client` - Optional client name to include in the event tags +/// +/// # Returns +/// +/// A signed Nostr event ready to be published to relays +pub fn create_backup_event( + keys: &Keys, + backup: &MintBackup, + client: Option<&str>, +) -> Result { + let plaintext = serde_json::to_string(backup)?; + + // Self-encryption: same key for sender and receiver per NIP-44 + let encrypted_content = nip44::encrypt( + keys.secret_key(), + &keys.public_key(), + plaintext, + Version::V2, + )?; + + let mut builder = EventBuilder::new( + Kind::Custom(KIND_APPLICATION_SPECIFIC_DATA), + encrypted_content, + ) + .tag(Tag::identifier(MINT_LIST_IDENTIFIER)); + + if let Some(client_name) = client { + builder = builder.tag(Tag::custom( + nostr_sdk::TagKind::Custom(std::borrow::Cow::Borrowed("client")), + [client_name], + )); + } + + let event = builder.sign_with_keys(keys)?; + + Ok(event) +} + +/// Decrypt and parse a mint backup event +/// +/// This decrypts a NIP-78 event containing an encrypted mint list and +/// returns the parsed backup data. +/// +/// # Arguments +/// +/// * `keys` - The Nostr keys derived from the wallet seed +/// * `event` - The Nostr event to decrypt +/// +/// # Returns +/// +/// The decrypted mint backup data +pub fn decrypt_backup_event(keys: &Keys, event: &Event) -> Result { + let expected_kind = Kind::Custom(KIND_APPLICATION_SPECIFIC_DATA); + if event.kind != expected_kind { + return Err(Error::InvalidEventKind { + expected: KIND_APPLICATION_SPECIFIC_DATA, + got: event.kind.as_u16(), + }); + } + + let has_mint_list_tag = event + .tags + .iter() + .any(|tag| tag.kind() == TagKind::d() && tag.content() == Some(MINT_LIST_IDENTIFIER)); + + if !has_mint_list_tag { + return Err(Error::MissingIdentifierTag( + MINT_LIST_IDENTIFIER.to_string(), + )); + } + + let decrypted = nip44::decrypt(keys.secret_key(), &keys.public_key(), &event.content)?; + + let backup: MintBackup = serde_json::from_str(&decrypted)?; + + Ok(backup) +} + +/// Create a Nostr filter for discovering mint backup events +/// +/// This creates filter parameters that can be used to query relays +/// for mint backup events created by the given public key. +/// +/// # Arguments +/// +/// * `keys` - The Nostr keys derived from the wallet seed +/// +/// # Returns +/// +/// A tuple of (kind, authors, d_tag) that can be used to construct a filter +pub fn backup_filter_params(keys: &Keys) -> (Kind, nostr_sdk::PublicKey, &'static str) { + ( + Kind::Custom(KIND_APPLICATION_SPECIFIC_DATA), + keys.public_key(), + MINT_LIST_IDENTIFIER, + ) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use bip39::Mnemonic; + + use super::*; + + fn test_keys() -> Keys { + let mnemonic = Mnemonic::from_str( + "half depart obvious quality work element tank gorilla view sugar picture humble", + ) + .unwrap(); + let seed: [u8; 64] = mnemonic.to_seed(""); + derive_nostr_keys(&seed).unwrap() + } + + #[test] + fn test_derive_nostr_keys_from_seed() { + let keys = test_keys(); + + // Verify the keys are valid + let secret_key = keys.secret_key(); + let public_key = keys.public_key(); + + // Secret key should be 32 bytes + assert_eq!(secret_key.as_secret_bytes().len(), 32); + + // Public key in nostr is x-only (32 bytes), displayed as 64 hex chars + assert_eq!(public_key.to_hex().len(), 64); + } + + /// Test vector for key derivation from BIP39 mnemonic. + /// + /// Mnemonic: "half depart obvious quality work element tank gorilla view sugar picture humble" + /// Expected secret key: e7ca79469a270b36617e4227ff2f068d3bcbb6b072c8584190b0203597c53c0d + /// Expected public key: 0767277aaed200af7a8843491745272fc1ad2c7bfe340225e6f34f3a9a273aed + #[test] + fn test_key_derivation_vector() { + use crate::util::hex; + + let keys = test_keys(); + + // Test vector: secret key + let expected_secret_key = + "e7ca79469a270b36617e4227ff2f068d3bcbb6b072c8584190b0203597c53c0d"; + assert_eq!( + hex::encode(keys.secret_key().as_secret_bytes()), + expected_secret_key + ); + + // Test vector: public key + let expected_public_key = + "0767277aaed200af7a8843491745272fc1ad2c7bfe340225e6f34f3a9a273aed"; + assert_eq!(keys.public_key().to_hex(), expected_public_key); + } + + #[test] + fn test_mint_backup_new() { + let mints = vec![ + MintUrl::from_str("https://mint.example.com").unwrap(), + MintUrl::from_str("https://another-mint.org").unwrap(), + ]; + + let backup = MintBackup::new(mints.clone()); + + assert_eq!(backup.mints, mints); + assert!(backup.timestamp > 0); + } + + #[test] + fn test_mint_backup_serialization() { + let mints = vec![ + MintUrl::from_str("https://mint.example.com").unwrap(), + MintUrl::from_str("https://another-mint.org").unwrap(), + ]; + let backup = MintBackup::with_timestamp(mints, 1703721600); + + let json = serde_json::to_string(&backup).unwrap(); + let parsed: MintBackup = serde_json::from_str(&json).unwrap(); + + assert_eq!(backup, parsed); + } + + #[test] + fn test_create_and_decrypt_backup_event() { + let keys = test_keys(); + let mints = vec![ + MintUrl::from_str("https://mint.example.com").unwrap(), + MintUrl::from_str("https://another-mint.org").unwrap(), + ]; + let backup = MintBackup::with_timestamp(mints.clone(), 1703721600); + + // Create the backup event + let event = create_backup_event(&keys, &backup, Some("cashu-test")).unwrap(); + + // Verify event properties + assert_eq!(event.kind, Kind::Custom(KIND_APPLICATION_SPECIFIC_DATA)); + assert_eq!(event.pubkey, keys.public_key()); + + // Verify tags + let has_d_tag = event + .tags + .iter() + .any(|tag| tag.kind() == TagKind::d() && tag.content() == Some(MINT_LIST_IDENTIFIER)); + assert!(has_d_tag, "Event should have 'd' tag with 'mint-list'"); + + // Decrypt and verify content + let decrypted = decrypt_backup_event(&keys, &event).unwrap(); + assert_eq!(decrypted.mints, mints); + assert_eq!(decrypted.timestamp, 1703721600); + } + + #[test] + fn test_create_backup_event_without_client() { + let keys = test_keys(); + let backup = MintBackup::with_timestamp(vec![], 1703721600); + + let event = create_backup_event(&keys, &backup, None).unwrap(); + + // Should not have a client tag + let has_client_tag = event.tags.iter().any( + |tag| matches!(tag.kind(), nostr_sdk::TagKind::Custom(cow) if cow.as_ref() == "client"), + ); + assert!(!has_client_tag); + } + + #[test] + fn test_decrypt_wrong_event_kind() { + let keys = test_keys(); + + // Create an event with wrong kind + let event = EventBuilder::new(Kind::TextNote, "test") + .tag(Tag::identifier(MINT_LIST_IDENTIFIER)) + .sign_with_keys(&keys) + .unwrap(); + + let result = decrypt_backup_event(&keys, &event); + assert!(matches!(result, Err(Error::InvalidEventKind { .. }))); + } + + #[test] + fn test_decrypt_missing_d_tag() { + let keys = test_keys(); + let backup = MintBackup::with_timestamp(vec![], 1703721600); + let plaintext = serde_json::to_string(&backup).unwrap(); + let encrypted = nip44::encrypt( + keys.secret_key(), + &keys.public_key(), + plaintext, + Version::V2, + ) + .unwrap(); + + // Create event without the d tag + let event = EventBuilder::new(Kind::Custom(KIND_APPLICATION_SPECIFIC_DATA), encrypted) + .sign_with_keys(&keys) + .unwrap(); + + let result = decrypt_backup_event(&keys, &event); + assert!(matches!(result, Err(Error::MissingIdentifierTag(_)))); + } + + #[test] + fn test_backup_filter_params() { + let keys = test_keys(); + let (kind, pubkey, d_tag) = backup_filter_params(&keys); + + assert_eq!(kind, Kind::Custom(KIND_APPLICATION_SPECIFIC_DATA)); + assert_eq!(pubkey, keys.public_key()); + assert_eq!(d_tag, MINT_LIST_IDENTIFIER); + } +} diff --git a/crates/cdk-common/Cargo.toml b/crates/cdk-common/Cargo.toml index a3e67a33c..804724f70 100644 --- a/crates/cdk-common/Cargo.toml +++ b/crates/cdk-common/Cargo.toml @@ -18,6 +18,7 @@ bench = [] wallet = ["cashu/wallet"] mint = ["cashu/mint", "dep:uuid"] auth = ["cashu/auth"] +nostr = ["wallet", "cashu/nostr"] prometheus = ["cdk-prometheus/default"] [dependencies] diff --git a/crates/cdk-ffi/src/multi_mint_wallet.rs b/crates/cdk-ffi/src/multi_mint_wallet.rs index f619ebc5c..f37c46111 100644 --- a/crates/cdk-ffi/src/multi_mint_wallet.rs +++ b/crates/cdk-ffi/src/multi_mint_wallet.rs @@ -1004,6 +1004,95 @@ impl From for CdkMultiMintSendOptions { } } +/// Nostr backup methods for MultiMintWallet (NUT-XX) +#[uniffi::export(async_runtime = "tokio")] +impl MultiMintWallet { + /// Get the hex-encoded public key used for Nostr mint backup + /// + /// This key is deterministically derived from the wallet seed and can be used + /// to identify and decrypt backup events on Nostr relays. + pub fn backup_public_key(&self) -> Result { + let keys = self.inner.backup_keys()?; + Ok(keys.public_key().to_hex()) + } + + /// Backup the current mint list to Nostr relays + /// + /// Creates an encrypted NIP-78 addressable event containing all mint URLs + /// and publishes it to the specified relays. + /// + /// # Arguments + /// + /// * `relays` - List of Nostr relay URLs (e.g., "wss://relay.damus.io") + /// * `options` - Backup options including optional client name + /// + /// # Example + /// + /// ```ignore + /// let relays = vec!["wss://relay.damus.io".to_string(), "wss://nos.lol".to_string()]; + /// let options = BackupOptions { client: Some("my-wallet".to_string()) }; + /// let result = wallet.backup_mints(relays, options).await?; + /// println!("Backup published with event ID: {}", result.event_id); + /// ``` + pub async fn backup_mints( + &self, + relays: Vec, + options: BackupOptions, + ) -> Result { + let result = self.inner.backup_mints(relays, options.into()).await?; + Ok(result.into()) + } + + /// Restore mint list from Nostr relays + /// + /// Fetches the most recent backup event from the specified relays, + /// decrypts it, and optionally adds the discovered mints to the wallet. + /// + /// # Arguments + /// + /// * `relays` - List of Nostr relay URLs to fetch from + /// * `add_mints` - If true, automatically add discovered mints to the wallet + /// * `options` - Restore options including timeout + /// + /// # Example + /// + /// ```ignore + /// let relays = vec!["wss://relay.damus.io".to_string()]; + /// let result = wallet.restore_mints(relays, true, RestoreOptions::default()).await?; + /// println!("Restored {} mints, {} newly added", result.mint_count, result.mints_added); + /// ``` + pub async fn restore_mints( + &self, + relays: Vec, + add_mints: bool, + options: RestoreOptions, + ) -> Result { + let result = self + .inner + .restore_mints(relays, add_mints, options.into()) + .await?; + Ok(result.into()) + } + + /// Fetch the backup without adding mints to the wallet + /// + /// This is useful for previewing what mints are in the backup before + /// deciding to add them. + /// + /// # Arguments + /// + /// * `relays` - List of Nostr relay URLs to fetch from + /// * `options` - Restore options including timeout + pub async fn fetch_backup( + &self, + relays: Vec, + options: RestoreOptions, + ) -> Result { + let backup = self.inner.fetch_backup(relays, options.into()).await?; + Ok(backup.into()) + } +} + /// Type alias for balances by mint URL pub type BalanceMap = HashMap; diff --git a/crates/cdk-ffi/src/types/mod.rs b/crates/cdk-ffi/src/types/mod.rs index 68d8b0e8f..72216cef1 100644 --- a/crates/cdk-ffi/src/types/mod.rs +++ b/crates/cdk-ffi/src/types/mod.rs @@ -8,6 +8,7 @@ pub mod amount; pub mod invoice; pub mod keys; pub mod mint; +pub mod nostr_backup; pub mod payment_request; pub mod proof; pub mod quote; @@ -20,6 +21,7 @@ pub use amount::*; pub use invoice::*; pub use keys::*; pub use mint::*; +pub use nostr_backup::*; pub use payment_request::*; pub use proof::*; pub use quote::*; diff --git a/crates/cdk-ffi/src/types/nostr_backup.rs b/crates/cdk-ffi/src/types/nostr_backup.rs new file mode 100644 index 000000000..a05402c9f --- /dev/null +++ b/crates/cdk-ffi/src/types/nostr_backup.rs @@ -0,0 +1,104 @@ +//! FFI types for Nostr mint backup (NUT-27) + +use cdk::wallet::{ + BackupOptions as CdkBackupOptions, BackupResult as CdkBackupResult, + RestoreOptions as CdkRestoreOptions, RestoreResult as CdkRestoreResult, +}; + +use super::MintUrl; + +/// Options for backup operations +#[derive(Debug, Clone, Default, uniffi::Record)] +pub struct BackupOptions { + /// Client name to include in the event tags + pub client: Option, +} + +impl From for CdkBackupOptions { + fn from(options: BackupOptions) -> Self { + let mut opts = CdkBackupOptions::new(); + if let Some(client) = options.client { + opts = opts.client(client); + } + opts + } +} + +/// Options for restore operations +#[derive(Debug, Clone, uniffi::Record)] +pub struct RestoreOptions { + /// Timeout in seconds for waiting for relay responses + pub timeout_secs: u64, +} + +impl Default for RestoreOptions { + fn default() -> Self { + Self { timeout_secs: 10 } + } +} + +impl From for CdkRestoreOptions { + fn from(options: RestoreOptions) -> Self { + CdkRestoreOptions::new().timeout(std::time::Duration::from_secs(options.timeout_secs)) + } +} + +/// Result of a backup operation +#[derive(Debug, Clone, uniffi::Record)] +pub struct BackupResult { + /// The event ID of the published backup (hex encoded) + pub event_id: String, + /// The public key used for the backup (hex encoded) + pub public_key: String, + /// Number of mints backed up + pub mint_count: u64, +} + +impl From for BackupResult { + fn from(result: CdkBackupResult) -> Self { + Self { + event_id: result.event_id.to_hex(), + public_key: result.public_key.to_hex(), + mint_count: result.mint_count as u64, + } + } +} + +/// Result of a restore operation +#[derive(Debug, Clone, uniffi::Record)] +pub struct RestoreResult { + /// The restored mint backup data + pub backup: MintBackup, + /// Number of mints found in the backup + pub mint_count: u64, + /// Number of mints that were newly added (not already in wallet) + pub mints_added: u64, +} + +impl From for RestoreResult { + fn from(result: CdkRestoreResult) -> Self { + Self { + backup: result.backup.into(), + mint_count: result.mint_count as u64, + mints_added: result.mints_added as u64, + } + } +} + +/// Mint backup data containing the list of mints and timestamp +#[derive(Debug, Clone, uniffi::Record)] +pub struct MintBackup { + /// List of mint URLs in the backup + pub mints: Vec, + /// Unix timestamp of when the backup was created + pub timestamp: u64, +} + +impl From for MintBackup { + fn from(backup: cdk::nuts::nut27::MintBackup) -> Self { + Self { + mints: backup.mints.into_iter().map(|m| m.into()).collect(), + timestamp: backup.timestamp, + } + } +} diff --git a/crates/cdk-integration-tests/tests/fake_auth.rs b/crates/cdk-integration-tests/tests/fake_auth.rs index 75c306c07..c92a6bbaa 100644 --- a/crates/cdk-integration-tests/tests/fake_auth.rs +++ b/crates/cdk-integration-tests/tests/fake_auth.rs @@ -748,7 +748,7 @@ async fn get_access_token(mint_info: &MintInfo) -> (String, String) { .nuts .nut21 .clone() - .expect("Nutxx defined") + .expect("Nut21 defined") .openid_discovery; let oidc_client = OidcClient::new(openid_discovery, None); @@ -806,7 +806,7 @@ async fn get_custom_access_token( .nuts .nut21 .clone() - .expect("Nutxx defined") + .expect("Nut21 defined") .openid_discovery; let oidc_client = OidcClient::new(openid_discovery, None); diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index d7480d3f5..ffa8c3660 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -13,7 +13,7 @@ license.workspace = true [features] default = ["mint", "wallet", "auth", "nostr", "bip353"] wallet = ["dep:futures", "dep:reqwest", "cdk-common/wallet", "dep:rustls"] -nostr = ["wallet", "dep:nostr-sdk"] +nostr = ["wallet", "dep:nostr-sdk", "cdk-common/nostr"] mint = ["dep:futures", "dep:reqwest", "cdk-common/mint", "cdk-signatory"] auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"] bip353 = ["dep:hickory-resolver"] @@ -146,6 +146,10 @@ required-features = ["wallet", "bip353"] name = "payment_request" required-features = ["wallet", "nostr"] +[[example]] +name = "nostr_backup" +required-features = ["wallet", "nostr"] + [[example]] name = "token-proofs" required-features = ["wallet"] diff --git a/crates/cdk/examples/nostr_backup.rs b/crates/cdk/examples/nostr_backup.rs new file mode 100644 index 000000000..c1b69fab7 --- /dev/null +++ b/crates/cdk/examples/nostr_backup.rs @@ -0,0 +1,187 @@ +//! # Nostr Mint Backup Example (NUT-XX) +//! +//! This example demonstrates how to backup and restore your mint list +//! to/from Nostr relays using the MultiMintWallet. +//! +//! ## Features +//! +//! - Backup your mint list to multiple Nostr relays +//! - Restore your mints on any device with the same seed +//! - Keep your mint configuration synchronized across wallets +//! +//! ## Security +//! +//! - Backup keys are derived deterministically from your seed +//! - Mint list is encrypted using NIP-44 (self-encryption) +//! - Only someone with your seed can decrypt the backup +//! - Events use addressable format (kind 30078) for easy updates +//! +//! ## Usage +//! +//! ```bash +//! cargo run --example nostr_backup --features="wallet nostr" +//! ``` + +use std::sync::Arc; + +use cdk::nuts::CurrencyUnit; +use cdk::wallet::multi_mint_wallet::MultiMintWallet; +use cdk::wallet::{BackupOptions, RestoreOptions}; +use cdk_sqlite::wallet::memory; +use rand::random; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize tracing for debug output + tracing_subscriber::fmt() + .with_max_level(tracing::Level::ERROR) + .init(); + + println!("NUT-XX Nostr Mint Backup Example"); + println!("=================================\n"); + + // Generate a random seed for the wallet + // In production, this would be derived from a BIP-39 mnemonic + let seed: [u8; 64] = random(); + + // Currency unit for the wallet + let unit = CurrencyUnit::Sat; + + // Initialize the memory store for the first wallet + let localstore = Arc::new(memory::empty().await?); + + // Create a new MultiMintWallet + let wallet = MultiMintWallet::new(localstore.clone(), seed, unit.clone()).await?; + + // ============================================================================ + // Step 1: Add test mints to the wallet + // ============================================================================ + + println!("Step 1: Adding mints to the wallet"); + println!("-----------------------------------"); + + let mints = vec![ + "https://fake.thesimplekid.dev", + "https://testnut.cashu.space", + ]; + + for mint_url in &mints { + println!(" Adding mint: {}", mint_url); + match wallet.add_mint(mint_url.parse()?).await { + Ok(()) => println!(" + Added successfully"), + Err(e) => println!(" x Failed to add: {}", e), + } + } + + // Verify mints were added + let wallets = wallet.get_wallets().await; + println!("\n Wallet now contains {} mint(s):", wallets.len()); + for w in &wallets { + println!(" - {}", w.mint_url); + } + + println!(); + + // ============================================================================ + // Step 2: Derive backup keys + // ============================================================================ + + println!("Step 2: Deriving backup keys from seed"); + println!("---------------------------------------"); + + let backup_keys = wallet.backup_keys()?; + println!(" Public key: {}", backup_keys.public_key().to_hex()); + println!(" This key is deterministically derived from your wallet seed."); + println!(" Anyone with the same seed will derive the same keys.\n"); + + // ============================================================================ + // Step 3: Backup mints to Nostr relays + // ============================================================================ + + println!("Step 3: Backing up mint list to Nostr relays"); + println!("---------------------------------------------"); + + let relays = vec!["wss://relay.damus.io", "wss://nos.lol"]; + + println!(" Relays: {:?}", relays); + println!(" Publishing backup event..."); + + let backup_result = wallet + .backup_mints( + relays.clone(), + BackupOptions::new().client("nostr-backup-example"), + ) + .await?; + + println!(" + Backup published!"); + println!(" Event ID: {}", backup_result.event_id); + println!(" Public Key: {}", backup_result.public_key.to_hex()); + println!(" Mints backed up: {}", backup_result.mint_count); + + println!(); + + // ============================================================================ + // Step 4: Simulate restore on a "new device" + // ============================================================================ + + println!("Step 4: Simulating restore on a new device"); + println!("-------------------------------------------"); + + // Create a fresh wallet with the same seed (simulating a new device) + let new_localstore = Arc::new(memory::empty().await?); + let new_wallet = MultiMintWallet::new(new_localstore, seed, unit.clone()).await?; + + // Verify the new wallet is empty + let new_wallets = new_wallet.get_wallets().await; + println!(" New wallet starts with {} mint(s)", new_wallets.len()); + + // Derive keys on the new wallet - should be the same! + let new_backup_keys = new_wallet.backup_keys()?; + println!( + " New wallet public key: {}", + new_backup_keys.public_key().to_hex() + ); + println!( + " Keys match: {}", + backup_keys.public_key() == new_backup_keys.public_key() + ); + + println!(); + + // ============================================================================ + // Step 5: Restore mints from Nostr relays + // ============================================================================ + + println!("Step 5: Restoring mint list from Nostr relays"); + println!("----------------------------------------------"); + + println!(" Fetching backup from relays..."); + + let restore_result = new_wallet + .restore_mints(relays.clone(), true, RestoreOptions::default()) + .await?; + + println!(" + Restore complete!"); + println!(" Mints found in backup: {}", restore_result.mint_count); + println!(" Mints newly added: {}", restore_result.mints_added); + println!(" Backup timestamp: {}", restore_result.backup.timestamp); + + println!("\n Mints in backup:"); + for mint in &restore_result.backup.mints { + println!(" - {}", mint); + } + + // Verify the mints were restored + let restored_wallets = new_wallet.get_wallets().await; + println!( + "\n New wallet now contains {} mint(s):", + restored_wallets.len() + ); + for w in &restored_wallets { + println!(" - {}", w.mint_url); + } + + println!(); + + Ok(()) +} diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 890dd3fbe..8ab653cf2 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -38,6 +38,8 @@ use crate::OidcClient; #[cfg(feature = "auth")] mod auth; +#[cfg(feature = "nostr")] +mod nostr_backup; #[cfg(all(feature = "tor", not(target_arch = "wasm32")))] pub use mint_connector::TorHttpClient; mod balance; @@ -72,6 +74,8 @@ pub use mint_connector::transport::Transport as HttpTransport; pub use mint_connector::AuthHttpClient; pub use mint_connector::{HttpClient, LnurlPayInvoiceResponse, LnurlPayResponse, MintConnector}; pub use multi_mint_wallet::{MultiMintReceiveOptions, MultiMintSendOptions, MultiMintWallet}; +#[cfg(feature = "nostr")] +pub use nostr_backup::{BackupOptions, BackupResult, RestoreOptions, RestoreResult}; pub use payment_request::CreateRequestParams; #[cfg(feature = "nostr")] pub use payment_request::NostrWaitInfo; @@ -167,9 +171,9 @@ impl Wallet { /// Create new [`Wallet`] using the builder pattern /// # Synopsis /// ```rust - /// use bitcoin::bip32::Xpriv; /// use std::sync::Arc; /// + /// use bitcoin::bip32::Xpriv; /// use cdk::nuts::CurrencyUnit; /// use cdk::wallet::{Wallet, WalletBuilder}; /// use cdk_sqlite::wallet::memory; diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index 7e46ca17d..eff197c1d 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -272,6 +272,15 @@ impl MultiMintWallet { Ok(wallet) } + /// Get a reference to the wallet seed + /// + /// This is used internally for key derivation operations. + #[inline(always)] + #[cfg(all(feature = "wallet", feature = "nostr"))] + pub(crate) fn seed(&self) -> &[u8; 64] { + &self.seed + } + /// Adds a mint to this [MultiMintWallet] /// /// Creates a wallet for the specified mint using default or global settings. diff --git a/crates/cdk/src/wallet/nostr_backup.rs b/crates/cdk/src/wallet/nostr_backup.rs new file mode 100644 index 000000000..3b0cbe476 --- /dev/null +++ b/crates/cdk/src/wallet/nostr_backup.rs @@ -0,0 +1,274 @@ +//! Nostr Mint Backup +//! +//! This module provides functionality to backup and restore the mint list +//! to/from Nostr relays using NUT-27 specification. + +use std::time::Duration; + +use nostr_sdk::prelude::*; +use nostr_sdk::{Client as NostrClient, Filter, Keys}; +use tracing::instrument; + +use super::multi_mint_wallet::MultiMintWallet; +use crate::error::Error; +use crate::mint_url::MintUrl; +use crate::nuts::nut27::{ + self, backup_filter_params, create_backup_event, decrypt_backup_event, MintBackup, +}; + +/// Options for backup operations +#[derive(Debug, Clone, Default)] +pub struct BackupOptions { + /// Client name to include in the event tags + pub client: Option, +} + +impl BackupOptions { + /// Create new backup options + pub fn new() -> Self { + Self::default() + } + + /// Set the client name + pub fn client(mut self, client: impl Into) -> Self { + self.client = Some(client.into()); + self + } +} + +/// Options for restore operations +#[derive(Debug, Clone)] +pub struct RestoreOptions { + /// Timeout for waiting for relay responses + pub timeout: Duration, +} + +impl Default for RestoreOptions { + fn default() -> Self { + Self { + timeout: Duration::from_secs(10), + } + } +} + +impl RestoreOptions { + /// Create new restore options + pub fn new() -> Self { + Self::default() + } + + /// Set the timeout for relay responses + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } +} + +/// Result of a backup operation +#[derive(Debug, Clone)] +pub struct BackupResult { + /// The event ID of the published backup + pub event_id: EventId, + /// The public key used for the backup + pub public_key: PublicKey, + /// Number of mints backed up + pub mint_count: usize, +} + +/// Result of a restore operation +#[derive(Debug, Clone)] +pub struct RestoreResult { + /// The restored mint backup data + pub backup: MintBackup, + /// Number of mints found in the backup + pub mint_count: usize, + /// Number of mints that were newly added (not already in wallet) + pub mints_added: usize, +} + +impl MultiMintWallet { + /// Derive the Nostr keys used for mint backup from the wallet seed + /// + /// These keys can be used to identify and decrypt backup events. + pub fn backup_keys(&self) -> Result { + nut27::derive_nostr_keys(self.seed()).map_err(|e| Error::Custom(e.to_string())) + } + + /// Backup the current mint list to Nostr relays + /// + /// This creates an encrypted NIP-78 addressable event containing all mint URLs + /// and publishes it to the specified relays. + /// + /// # Arguments + /// + /// * `relays` - List of relay URLs to publish the backup to + /// * `options` - Optional backup configuration + /// + /// # Example + /// + /// ```ignore + /// let relays = vec!["wss://relay.damus.io", "wss://nos.lol"]; + /// let result = wallet.backup_mints( + /// relays, + /// BackupOptions::new().client("my-wallet"), + /// ).await?; + /// println!("Backup published with event ID: {}", result.event_id); + /// ``` + #[instrument(skip(self, relays))] + pub async fn backup_mints( + &self, + relays: Vec, + options: BackupOptions, + ) -> Result + where + S: AsRef, + { + let keys = self.backup_keys()?; + + let wallets = self.get_wallets().await; + let mint_urls: Vec = wallets.iter().map(|w| w.mint_url.clone()).collect(); + + let backup = MintBackup::new(mint_urls.clone()); + + let event = create_backup_event(&keys, &backup, options.client.as_deref()) + .map_err(|e| Error::Custom(format!("Failed to create backup event: {e}")))?; + + let event_id = event.id; + + let client = NostrClient::new(keys.clone()); + + for relay in relays.iter() { + client + .add_write_relay(relay.as_ref()) + .await + .map_err(|e| Error::Custom(format!("Failed to add relay: {e}")))?; + } + + client.connect().await; + + client + .send_event(&event) + .await + .map_err(|e| Error::Custom(format!("Failed to publish backup event: {e}")))?; + + client.disconnect().await; + + Ok(BackupResult { + event_id, + public_key: keys.public_key(), + mint_count: mint_urls.len(), + }) + } + + /// Restore mint list from Nostr relays + /// + /// This fetches the most recent backup event from the specified relays, + /// decrypts it, and optionally adds the discovered mints to the wallet. + /// + /// # Arguments + /// + /// * `relays` - List of relay URLs to fetch the backup from + /// * `add_mints` - If true, automatically add discovered mints to the wallet + /// * `options` - Optional restore configuration + /// + /// # Example + /// + /// ```ignore + /// let relays = vec!["wss://relay.damus.io", "wss://nos.lol"]; + /// let result = wallet.restore_mints( + /// relays, + /// true, // automatically add mints + /// RestoreOptions::default(), + /// ).await?; + /// println!("Restored {} mints, {} newly added", result.mint_count, result.mints_added); + /// ``` + #[instrument(skip(self, relays))] + pub async fn restore_mints( + &self, + relays: Vec, + add_mints: bool, + options: RestoreOptions, + ) -> Result + where + S: AsRef, + { + let keys = self.backup_keys()?; + + let (kind, pubkey, d_tag) = backup_filter_params(&keys); + + let filter = Filter::new() + .kind(kind) + .author(pubkey) + .identifier(d_tag) + .limit(1); + + let client = NostrClient::new(keys.clone()); + + for relay in relays.iter() { + client + .add_read_relay(relay.as_ref()) + .await + .map_err(|e| Error::Custom(format!("Failed to add relay: {e}")))?; + } + + client.connect().await; + + let events = client + .fetch_events(filter, options.timeout) + .await + .map_err(|e| Error::Custom(format!("Failed to fetch backup events: {e}")))?; + + client.disconnect().await; + + // Addressable events ensure only one event per pubkey+d-tag combination + let event = events + .into_iter() + .next() + .ok_or_else(|| Error::Custom("No backup event found".to_string()))?; + + let backup = decrypt_backup_event(&keys, &event) + .map_err(|e| Error::Custom(format!("Failed to decrypt backup event: {e}")))?; + + let mint_count = backup.mints.len(); + let mut mints_added = 0; + + if add_mints { + for mint_url in &backup.mints { + if !self.has_mint(mint_url).await { + // Ignore errors for individual mints to continue restoring others + if self.add_mint(mint_url.clone()).await.is_ok() { + mints_added += 1; + } + } + } + } + + Ok(RestoreResult { + backup, + mint_count, + mints_added, + }) + } + + /// Fetch the backup without adding mints to the wallet + /// + /// This is useful for previewing what mints are in the backup before + /// deciding to add them. + /// + /// # Arguments + /// + /// * `relays` - List of relay URLs to fetch the backup from + /// * `options` - Optional restore configuration + #[instrument(skip(self, relays))] + pub async fn fetch_backup( + &self, + relays: Vec, + options: RestoreOptions, + ) -> Result + where + S: AsRef, + { + let result = self.restore_mints(relays, false, options).await?; + Ok(result.backup) + } +} diff --git a/rustfmt.toml b/rustfmt.toml index 547421e78..854c9de40 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -7,3 +7,4 @@ indent_style = "Block" normalize_comments = false imports_granularity = "Module" group_imports = "StdExternalCrate" +format_code_in_doc_comments = true