Procházet zdrojové kódy

feat: nostr mint backup (#1459)

tsk před 2 týdny
rodič
revize
96d2305f77

+ 136 - 105
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,9 +4533,9 @@ 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",
@@ -4540,14 +4543,24 @@ dependencies = [
 ]
 
 [[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]]
@@ -5730,6 +5745,15 @@ dependencies = [
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"

+ 88 - 66
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,9 +4532,9 @@ 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",
@@ -4539,14 +4542,24 @@ dependencies = [
 ]
 
 [[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"

+ 1 - 1
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"

+ 4 - 0
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

+ 2 - 0
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
 

+ 6 - 0
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,
+};

+ 2 - 1
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()),

+ 406 - 0
crates/cashu/src/nuts/nut27.rs

@@ -0,0 +1,406 @@
+//! NUT-27: Nostr Mint Backup
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/27.md>
+//!
+//! 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<MintUrl>,
+    /// 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<MintUrl>) -> 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<MintUrl>, 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<Keys, Error> {
+    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<Event, Error> {
+    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<MintBackup, Error> {
+    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);
+    }
+}

+ 1 - 0
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]

+ 89 - 0
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -1004,6 +1004,95 @@ impl From<MultiMintSendOptions> 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<String, FfiError> {
+        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<String>,
+        options: BackupOptions,
+    ) -> Result<BackupResult, FfiError> {
+        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<String>,
+        add_mints: bool,
+        options: RestoreOptions,
+    ) -> Result<RestoreResult, FfiError> {
+        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<String>,
+        options: RestoreOptions,
+    ) -> Result<MintBackup, FfiError> {
+        let backup = self.inner.fetch_backup(relays, options.into()).await?;
+        Ok(backup.into())
+    }
+}
+
 /// Type alias for balances by mint URL
 pub type BalanceMap = HashMap<String, Amount>;
 

+ 2 - 0
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::*;

+ 104 - 0
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<String>,
+}
+
+impl From<BackupOptions> 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<RestoreOptions> 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<CdkBackupResult> 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<CdkRestoreResult> 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<MintUrl>,
+    /// Unix timestamp of when the backup was created
+    pub timestamp: u64,
+}
+
+impl From<cdk::nuts::nut27::MintBackup> for MintBackup {
+    fn from(backup: cdk::nuts::nut27::MintBackup) -> Self {
+        Self {
+            mints: backup.mints.into_iter().map(|m| m.into()).collect(),
+            timestamp: backup.timestamp,
+        }
+    }
+}

+ 2 - 2
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);

+ 5 - 1
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"]
@@ -147,6 +147,10 @@ name = "payment_request"
 required-features = ["wallet", "nostr"]
 
 [[example]]
+name = "nostr_backup"
+required-features = ["wallet", "nostr"]
+
+[[example]]
 name = "token-proofs"
 required-features = ["wallet"]
 

+ 187 - 0
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(())
+}

+ 5 - 1
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;

+ 9 - 0
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.

+ 274 - 0
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<String>,
+}
+
+impl BackupOptions {
+    /// Create new backup options
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Set the client name
+    pub fn client(mut self, client: impl Into<String>) -> 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<Keys, Error> {
+        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<S>(
+        &self,
+        relays: Vec<S>,
+        options: BackupOptions,
+    ) -> Result<BackupResult, Error>
+    where
+        S: AsRef<str>,
+    {
+        let keys = self.backup_keys()?;
+
+        let wallets = self.get_wallets().await;
+        let mint_urls: Vec<MintUrl> = 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<S>(
+        &self,
+        relays: Vec<S>,
+        add_mints: bool,
+        options: RestoreOptions,
+    ) -> Result<RestoreResult, Error>
+    where
+        S: AsRef<str>,
+    {
+        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<S>(
+        &self,
+        relays: Vec<S>,
+        options: RestoreOptions,
+    ) -> Result<MintBackup, Error>
+    where
+        S: AsRef<str>,
+    {
+        let result = self.restore_mints(relays, false, options).await?;
+        Ok(result.backup)
+    }
+}

+ 1 - 0
rustfmt.toml

@@ -7,3 +7,4 @@ indent_style = "Block"
 normalize_comments = false
 imports_granularity = "Module"
 group_imports = "StdExternalCrate"
+format_code_in_doc_comments = true