]> Untitled Git - bdk-cli/commitdiff
feat: integrate BIP 353 dns payment instructions
authorSonkeng Maldini <sdmg15@pm.me>
Wed, 7 Jan 2026 21:56:11 +0000 (22:56 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Wed, 8 Jul 2026 08:13:01 +0000 (09:13 +0100)
Cargo.lock
Cargo.toml
src/commands.rs
src/dns_payment_instructions.rs [new file with mode: 0644]
src/handlers.rs
src/main.rs
src/utils.rs

index 992f3c633f59758ecf67ac8c01d3130e79181100..a5826c4ba0dd92b4bc107b9c42b7d4a335b4245b 100644 (file)
@@ -121,9 +121,9 @@ dependencies = [
 
 [[package]]
 name = "arrayvec"
-version = "0.7.7"
+version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
 
 [[package]]
 name = "async-trait"
@@ -204,6 +204,7 @@ dependencies = [
  "bdk_redb",
  "bdk_sp",
  "bdk_wallet",
+ "bitcoin-payment-instructions",
  "claims",
  "clap",
  "clap_complete",
@@ -500,6 +501,19 @@ dependencies = [
  "toml 0.5.11",
 ]
 
+[[package]]
+name = "bitcoin-payment-instructions"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c300c948b2ff78c965ea3a613372352125448a22f1acf49e95e3878149824091"
+dependencies = [
+ "bitcoin",
+ "dnssec-prover",
+ "getrandom 0.3.4",
+ "lightning",
+ "lightning-invoice",
+]
+
 [[package]]
 name = "bitcoin-units"
 version = "0.1.101"
@@ -615,9 +629,9 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
 
 [[package]]
 name = "cc"
-version = "1.2.65"
+version = "1.2.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -660,6 +674,17 @@ dependencies = [
  "cpufeatures 0.2.17",
 ]
 
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
 [[package]]
 name = "chacha20-poly1305"
 version = "0.1.2"
@@ -876,6 +901,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "crunchy"
 version = "0.2.4"
@@ -935,9 +969,9 @@ dependencies = [
 
 [[package]]
 name = "defmt"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
 dependencies = [
  "bitflags 1.3.2",
  "defmt-macros",
@@ -945,12 +979,11 @@ dependencies = [
 
 [[package]]
 name = "defmt-macros"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
 dependencies = [
  "defmt-parser",
- "proc-macro-error2",
  "proc-macro2",
  "quote",
  "syn",
@@ -1017,6 +1050,16 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "dnssec-prover"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec4f825369fc7134da70ca4040fddc8e03b80a46d249ae38d9c1c39b7b4476bf"
+dependencies = [
+ "bitcoin_hashes 0.14.101",
+ "tokio",
+]
+
 [[package]]
 name = "dunce"
 version = "1.0.5"
@@ -1267,11 +1310,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
 dependencies = [
  "cfg-if",
- "js-sys",
  "libc",
  "r-efi 5.3.0",
  "wasip2",
- "wasm-bindgen",
 ]
 
 [[package]]
@@ -1281,8 +1322,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -1306,6 +1350,12 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+
 [[package]]
 name = "hashbrown"
 version = "0.14.5"
@@ -1735,11 +1785,11 @@ dependencies = [
 
 [[package]]
 name = "jobserver"
-version = "0.1.34"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
 dependencies = [
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
  "libc",
 ]
 
@@ -1778,6 +1828,12 @@ version = "0.2.186"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
 
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
 [[package]]
 name = "libredox"
 version = "0.1.18"
@@ -1798,6 +1854,54 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "lightning"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63a1ab99a13ab3343a9b4d1d25c455bf72322819fc5a5f05c5182e704528a583"
+dependencies = [
+ "bech32",
+ "bitcoin",
+ "dnssec-prover",
+ "hashbrown 0.13.2",
+ "libm",
+ "lightning-invoice",
+ "lightning-macros",
+ "lightning-types",
+ "possiblyrandom",
+]
+
+[[package]]
+name = "lightning-invoice"
+version = "0.34.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d83bd798e04ab9eecc8bbef1fa17d3808859bcdc0406bd16c55d51c8834444"
+dependencies = [
+ "bech32",
+ "bitcoin",
+ "lightning-types",
+]
+
+[[package]]
+name = "lightning-macros"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c717494cdc2c8bb85bee7113031248f5f6c64f8802b33c1c9e2d98e594aa71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "lightning-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c211dfcff95ca308247da8b1e0e81604bc9e568239967cd2c34572558511e869"
+dependencies = [
+ "bitcoin",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.12.1"
@@ -1833,9 +1937,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
 
 [[package]]
 name = "memchr"
-version = "2.8.2"
+version = "2.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
 
 [[package]]
 name = "miniscript"
@@ -2082,6 +2186,15 @@ dependencies = [
  "portable-atomic",
 ]
 
+[[package]]
+name = "possiblyrandom"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c564dbf654befd49035528299f1208a40508f6e07efb11c163444e304e4484f"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
 [[package]]
 name = "potential_utf"
 version = "0.1.5"
@@ -2100,28 +2213,6 @@ dependencies = [
  "zerocopy",
 ]
 
-[[package]]
-name = "proc-macro-error-attr2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
-dependencies = [
- "proc-macro2",
- "quote",
-]
-
-[[package]]
-name = "proc-macro-error2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
-dependencies = [
- "proc-macro-error-attr2",
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "proc-macro2"
 version = "1.0.106"
@@ -2153,15 +2244,16 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.11.15"
+version = "0.11.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
 dependencies = [
  "aws-lc-rs",
  "bytes",
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
  "lru-slab",
- "rand 0.9.4",
+ "rand 0.10.2",
+ "rand_pcg",
  "ring",
  "rustc-hash",
  "rustls 0.23.41",
@@ -2175,16 +2267,16 @@ dependencies = [
 
 [[package]]
 name = "quinn-udp"
-version = "0.5.14"
+version = "0.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
 dependencies = [
  "cfg_aliases",
  "libc",
  "once_cell",
  "socket2",
  "tracing",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2215,18 +2307,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
 dependencies = [
  "libc",
- "rand_chacha 0.3.1",
+ "rand_chacha",
  "rand_core 0.6.4",
 ]
 
 [[package]]
 name = "rand"
-version = "0.9.4"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
 dependencies = [
- "rand_chacha 0.9.0",
- "rand_core 0.9.5",
+ "chacha20 0.10.1",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
 ]
 
 [[package]]
@@ -2239,16 +2332,6 @@ dependencies = [
  "rand_core 0.6.4",
 ]
 
-[[package]]
-name = "rand_chacha"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.9.5",
-]
-
 [[package]]
 name = "rand_core"
 version = "0.6.4"
@@ -2260,11 +2343,17 @@ dependencies = [
 
 [[package]]
 name = "rand_core"
-version = "0.9.5"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
 dependencies = [
- "getrandom 0.3.4",
+ "rand_core 0.10.1",
 ]
 
 [[package]]
@@ -2431,9 +2520,9 @@ dependencies = [
 
 [[package]]
 name = "rustc-hash"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
 
 [[package]]
 name = "rustc_version"
@@ -2558,9 +2647,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.22"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
 
 [[package]]
 name = "ryu"
@@ -3436,16 +3525,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.5",
+ "windows-targets",
 ]
 
 [[package]]
@@ -3463,31 +3543,14 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.53.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
-dependencies = [
- "windows-link",
- "windows_aarch64_gnullvm 0.53.1",
- "windows_aarch64_msvc 0.53.1",
- "windows_i686_gnu 0.53.1",
- "windows_i686_gnullvm 0.53.1",
- "windows_i686_msvc 0.53.1",
- "windows_x86_64_gnu 0.53.1",
- "windows_x86_64_gnullvm 0.53.1",
- "windows_x86_64_msvc 0.53.1",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
 ]
 
 [[package]]
@@ -3496,96 +3559,48 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
-[[package]]
-name = "windows_i686_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
-
 [[package]]
 name = "windows_i686_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
-[[package]]
-name = "windows_i686_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
-
 [[package]]
 name = "winnow"
 version = "1.0.3"
@@ -3629,18 +3644,18 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.52"
+version = "0.8.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.52"
+version = "0.8.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
 dependencies = [
  "proc-macro2",
  "quote",
index 04a713f96ce98d391a8b8b0efc1e2e6dd2dba432..8097662a2e222ffacbfa4d926be0b67c0336e110 100644 (file)
@@ -40,6 +40,7 @@ payjoin = { version = "0.25.0", features = ["v1", "v2", "io", "_test-utils"], op
 reqwest = { version = "0.13.2", default-features = false, features = ["rustls"], optional = true }
 url = { version = "2.5.8", optional = true }
 bdk_bip322 = { version = "0.1.0", optional = true }
+bitcoin-payment-instructions = { version = "0.7.0", optional = true}
 
 [features]
 default = ["repl", "sqlite"]
@@ -55,7 +56,8 @@ redb = ["bdk_redb"]
 cbf = ["bdk_kyoto", "_payjoin-dependencies"]
 electrum = ["bdk_electrum", "_payjoin-dependencies"]
 esplora = ["bdk_esplora", "_payjoin-dependencies"]
-rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"] 
+rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]
+dns_payment = ["bitcoin-payment-instructions"] 
 
 # Internal features
 _payjoin-dependencies = ["payjoin", "reqwest", "url", "sqlite"]
index 435cb6804ea2fcb07b12e5fa6414cb30d97fadab..f9a0e2a732426915dfe4c5930fdbee9ef119ff95 100644 (file)
@@ -16,6 +16,7 @@
 #[cfg(feature = "silent-payments")]
 use {crate::utils::parse_sp_code_value_pairs, bdk_sp::encoding::SilentPaymentCode};
 
+
 use bdk_wallet::bitcoin::{
     Address, Network, OutPoint, ScriptBuf,
     bip32::{DerivationPath, Xpriv},
@@ -23,6 +24,8 @@ use bdk_wallet::bitcoin::{
 use clap::{Args, Parser, Subcommand, ValueEnum, value_parser};
 use clap_complete::Shell;
 
+#[cfg(feature = "dns_payment")]
+use crate::utils::parse_dns_recipient;
 #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
 use crate::utils::parse_proxy_auth;
 use crate::utils::{parse_address, parse_outpoint, parse_recipient};
@@ -198,6 +201,16 @@ pub enum CliSubCommand {
         #[arg(long = "spend_key")]
         spend: bdk_sp::bitcoin::secp256k1::PublicKey,
     },
+
+    #[cfg(feature = "dns_payment")]
+    /// Resolves BIP-353 DNS payment instructions for a human-readable name.
+    ResolveDnsRecipient {
+        /// Human-readable name (e.g. user@domain.com)
+        hrn: String,
+        /// DNS resolver address
+        #[arg(long, default_value = "8.8.8.8")]
+        resolver: String,
+    },
 }
 
 /// Wallet operation subcommands.
@@ -370,6 +383,14 @@ pub enum OfflineWalletSubCommand {
         // Address and amount parsing is done at run time in handler function.
         #[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]
         recipients: Vec<(ScriptBuf, u64)>,
+        #[cfg(feature = "dns_payment")]
+        /// Adds DNS recipients to the transaction
+        #[arg(long = "to_dns", value_parser = parse_dns_recipient)]
+        dns_recipients: Vec<(String, u64)>,
+        #[cfg(feature = "dns_payment")]
+        /// Custom resolver DNS IP to be used for resolution.
+        #[arg(long = "dns_resolver", default_value = "8.8.8.8:53")]
+        dns_resolver: String,
         /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
         #[arg(long = "send_all", short = 'a')]
         send_all: bool,
diff --git a/src/dns_payment_instructions.rs b/src/dns_payment_instructions.rs
new file mode 100644 (file)
index 0000000..88453d7
--- /dev/null
@@ -0,0 +1,287 @@
+use bdk_wallet::bitcoin::{Address, Amount, Network};
+use bitcoin_payment_instructions::{
+    FixedAmountPaymentInstructions, ParseError, PaymentInstructions, PaymentMethod,
+    PaymentMethodType, amount, dns_resolver::DNSHrnResolver,
+};
+use cli_table::{Cell, Style, Table, format::Justify};
+use core::{net::SocketAddr, str::FromStr};
+
+use crate::{error::BDKCliError as Error, utils::shorten};
+
+#[derive(Debug)]
+pub struct Payment {
+    pub payment_methods: Vec<PaymentMethod>,
+    pub min_amount: Option<Amount>,
+    pub max_amount: Option<Amount>,
+    pub description: Option<String>,
+    pub expected_amount: Option<Amount>,
+    pub receiving_addr: Option<Address>,
+    pub hrn: String,
+    pub notes: String,
+}
+
+impl Payment {
+    pub fn display(&self, pretty: bool) -> Result<String, Error> {
+        let mut methods: Vec<String> = Vec::new();
+        self.payment_methods.iter().for_each(|pm| match pm {
+            bitcoin_payment_instructions::PaymentMethod::LightningBolt11(bolt11) => {
+                methods.push(format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15)))
+            }
+            bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
+                methods.push(format!("Bolt 12 invoice ({})", shorten(offer, 20, 15)))
+            }
+            bitcoin_payment_instructions::PaymentMethod::OnChain(address) => {
+                methods.push(format!("On chain ({})", address))
+            }
+            bitcoin_payment_instructions::PaymentMethod::Cashu(csh) => {
+                methods.push(format!("Cashu payment ({})", shorten(csh, 20, 15)))
+            }
+        });
+
+        if pretty {
+            let mut table = vec![vec![
+                "HRN".cell().bold(true),
+                self.hrn.to_string().cell().justify(Justify::Right),
+            ]];
+
+            if let Some(min_amnt) = self.min_amount {
+                table.push(vec![
+                    "Min amount".cell().bold(true),
+                    min_amnt.to_string().cell().justify(Justify::Right),
+                ]);
+            }
+
+            if let Some(max_amnt) = self.max_amount {
+                table.push(vec![
+                    "Max amount".cell().bold(true),
+                    max_amnt.to_string().cell().justify(Justify::Right),
+                ]);
+            }
+
+            if let Some(send_amnt) = self.expected_amount {
+                table.push(vec![
+                    "Expected Amount to send".cell().bold(true),
+                    send_amnt.to_string().cell().justify(Justify::Right),
+                ]);
+            }
+
+            if let Some(descr) = &self.description {
+                table.push(vec![
+                    "Description".cell().bold(true),
+                    descr.cell().justify(Justify::Right),
+                ]);
+            }
+
+            table.push(vec![
+                "Accepted methods".cell().bold(true),
+                methods.join(", ").cell().justify(Justify::Right),
+            ]);
+            table.push(vec![
+                "Notes".cell().bold(true),
+                self.notes.clone().cell().justify(Justify::Right),
+            ]);
+
+            let table = table
+                .table()
+                .display()
+                .map_err(|e| Error::Generic(e.to_string()))?;
+
+            Ok(format!("{table}"))
+        } else {
+            Ok(serde_json::to_string_pretty(&serde_json::json!({
+                    "hrn": self.hrn,
+                    "payment_methods": methods,
+                    "description": self.description,
+                    "min_amount": self.min_amount,
+                    "max_amount": self.max_amount,
+                    "expected_amount_to_send": self.expected_amount,
+                    "notes": self.notes
+            }))?)
+        }
+    }
+}
+pub(crate) async fn parse_dns_instructions(
+    hrn: &str,
+    network: Network,
+    resolver_ip: String,
+) -> Result<(DNSHrnResolver, PaymentInstructions), ParseError> {
+    let ip_address = if resolver_ip.contains(':') {
+        resolver_ip
+    } else {
+        format!("{resolver_ip}:53")
+    };
+
+    let sock_addr = SocketAddr::from_str(&ip_address).map_err(|_| {
+        ParseError::HrnResolutionError("Unable to create socket from provided address")
+    })?;
+    let resolver = DNSHrnResolver(sock_addr);
+    let instructions = PaymentInstructions::parse(hrn, network, &resolver, true).await?;
+    Ok((resolver, instructions))
+}
+
+fn get_onchain_info(
+    instructions: &FixedAmountPaymentInstructions,
+) -> Result<(Address, Amount), Error> {
+    // Look for on chain payment method as it's the only one we can support
+    let PaymentMethod::OnChain(addr) = instructions
+        .methods()
+        .iter()
+        .find(|ix| matches!(ix, PaymentMethod::OnChain(_)))
+        .ok_or(Error::Generic(
+            "Missing Onchain payment method option.".to_string(),
+        ))?
+    else {
+        return Err(Error::Generic("Unsupported payment method".to_string()));
+    };
+
+    let Some(onchain_amount) = instructions.onchain_payment_amount() else {
+        return Err(Error::Generic(
+            "On chain amount should be specified".to_string(),
+        ));
+    };
+
+    // We need this conversion since Amount from instructions is different from Amount from bitcoin
+    Ok((addr.clone(), Amount::from_sat(onchain_amount.milli_sats())))
+}
+
+pub async fn process_instructions(
+    amount_to_send: Amount,
+    payment_instructions: &PaymentInstructions,
+    resolver: DNSHrnResolver,
+) -> Result<Payment, Error> {
+    match payment_instructions {
+        PaymentInstructions::ConfigurableAmount(instructions) => {
+            // Look for on chain payment method as it's the only one we can support
+            if !instructions
+                .methods()
+                .any(|method| matches!(method.method_type(), PaymentMethodType::OnChain))
+            {
+                return Err(Error::Generic("Unsupported payment method".to_string()));
+            }
+
+            let min_amount = instructions
+                .min_amt()
+                .map(|amnt| Amount::from_sat(amnt.milli_sats()));
+
+            let max_amount = instructions
+                .max_amt()
+                .map(|amnt| Amount::from_sat(amnt.milli_sats()));
+
+            if min_amount.is_some_and(|min| amount_to_send < min) {
+                return Err(Error::Generic(
+                    format!(
+                        "Amount to send should be greater than min {}",
+                        min_amount.unwrap()
+                    )
+                    .to_string(),
+                ));
+            }
+
+            if max_amount.is_some_and(|max| amount_to_send > max) {
+                return Err(Error::Generic(
+                    format!(
+                        "Amount to send should be lower than max {}",
+                        max_amount.unwrap()
+                    )
+                    .to_string(),
+                ));
+            }
+
+            let fixed_instructions = instructions
+                .clone()
+                .set_amount(
+                    amount::Amount::from_sats(amount_to_send.to_sat()).unwrap(),
+                    &resolver,
+                )
+                .await
+                .map_err(|err| {
+                    Error::Generic(format!("Error occured while parsing instructions {err}"))
+                })?;
+
+            let onchain_details = get_onchain_info(&fixed_instructions)?;
+
+            Ok(Payment {
+                payment_methods: vec![PaymentMethod::OnChain(onchain_details.clone().0)],
+                min_amount,
+                max_amount,
+                description: instructions.recipient_description().map(|s| s.to_string()),
+                expected_amount: Some(onchain_details.1),
+                receiving_addr: Some(onchain_details.0.clone()),
+                hrn: instructions.human_readable_name().unwrap().to_string(),
+                notes: "".to_string(),
+            })
+        }
+
+        PaymentInstructions::FixedAmount(instructions) => {
+            let onchain_info = get_onchain_info(instructions)?;
+
+            Ok(Payment {
+                payment_methods: vec![PaymentMethod::OnChain(onchain_info.clone().0)],
+                min_amount: None,
+                max_amount: instructions
+                    .max_amount()
+                    .map(|amnt| Amount::from_sat(amnt.milli_sats())),
+                description: instructions.recipient_description().map(|s| s.to_string()),
+                expected_amount: Some(onchain_info.1),
+                receiving_addr: Some(onchain_info.0),
+                notes: "".to_string(),
+                hrn: instructions.human_readable_name().unwrap().to_string(),
+            })
+        }
+    }
+}
+
+/// Resolves the dns payment instructions found at the specified Human Readable Name
+pub async fn resolve_dns_recipient(
+    hrn: &str,
+    network: Network,
+    ip: String,
+) -> Result<Payment, ParseError> {
+    let (resolver, instructions) = parse_dns_instructions(hrn, network, ip).await?;
+
+    match instructions {
+        PaymentInstructions::ConfigurableAmount(ix) => {
+            let description = ix.recipient_description().map(|s| s.to_string());
+            let min_amount = ix.min_amt().map(|amnt| Amount::from_sat(amnt.milli_sats()));
+            let max_amount = ix.max_amt().map(|amnt| Amount::from_sat(amnt.milli_sats()));
+
+            // Let's set a dummy amount to resolve the payment methods accepted.
+            let fixed_instructions = ix
+                .set_amount(amount::Amount::ZERO, &resolver)
+                .await
+                .map_err(ParseError::InvalidInstructions)?;
+
+            let payment = Payment {
+                min_amount,
+                max_amount,
+                payment_methods: fixed_instructions.methods().into(),
+                description,
+                expected_amount: None,
+                receiving_addr: None,
+                hrn: hrn.to_string(),
+                notes: "This is configurable payment instructions. You must send an amount between min_amount and max_amount if set.".to_string()
+            };
+
+            Ok(payment)
+        }
+
+        PaymentInstructions::FixedAmount(ix) => {
+            let max_amount = ix
+                .max_amount()
+                .map(|amnt| Amount::from_sat(amnt.milli_sats()));
+
+            let payment = Payment {
+                min_amount: None,
+                max_amount,
+                payment_methods: ix.methods().into(),
+                description: ix.recipient_description().map(|s| s.to_string()),
+                expected_amount: None,
+                receiving_addr: None,
+                hrn: hrn.to_string(),
+                notes: "This is a fixed payment instructions. You must send exactly the amount specified in max_amount.".to_string()
+            };
+
+            Ok(payment)
+        }
+    }
+}
index 4f7900a02a00a70e0db6eb78fa3fe85206f016ce..f9b4ad6319dbbac022f03cfdcbb0e32a1d51b8a9 100644 (file)
@@ -12,6 +12,8 @@
 use crate::commands::OfflineWalletSubCommand::*;
 use crate::commands::*;
 use crate::config::{WalletConfig, WalletConfigInner};
+#[cfg(feature = "dns_payment")]
+use crate::dns_payment_instructions::resolve_dns_recipient;
 use crate::error::BDKCliError as Error;
 #[cfg(any(feature = "sqlite", feature = "redb"))]
 use crate::persister::Persister;
@@ -21,6 +23,7 @@ use crate::utils::*;
 #[cfg(feature = "redb")]
 use bdk_redb::Store as RedbStore;
 use bdk_wallet::bip39::{Language, Mnemonic};
+use bdk_wallet::bitcoin::ScriptBuf;
 use bdk_wallet::bitcoin::base64::Engine;
 use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD;
 use bdk_wallet::bitcoin::{
@@ -112,7 +115,7 @@ const NUMS_UNSPENDABLE_KEY_HEX: &str =
 /// Execute an offline wallet sub-command
 ///
 /// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
-pub fn handle_offline_wallet_subcommand(
+pub async fn handle_offline_wallet_subcommand(
     wallet: &mut Wallet,
     wallet_opts: &WalletOpts,
     cli_opts: &CliOpts,
@@ -526,6 +529,10 @@ pub fn handle_offline_wallet_subcommand(
         }
         CreateTx {
             recipients,
+            #[cfg(feature = "dns_payment")]
+            dns_recipients,
+            #[cfg(feature = "dns_payment")]
+            dns_resolver,
             send_all,
             enable_rbf,
             offline_signer,
@@ -548,10 +555,30 @@ pub fn handle_offline_wallet_subcommand(
                     ));
                 }
             } else {
-                let recipients = recipients
+                #[allow(unused_mut)]
+                let mut recipients: Vec<(ScriptBuf, Amount)> = recipients
                     .into_iter()
                     .map(|(script, amount)| (script, Amount::from_sat(amount)))
                     .collect();
+
+                #[cfg(feature = "dns_payment")]
+                for recipient in dns_recipients {
+                    use crate::dns_payment_instructions::{
+                        parse_dns_instructions, process_instructions,
+                    };
+                    let amount = Amount::from_sat(recipient.1);
+                    let (resolver, instructions) = parse_dns_instructions(
+                        &recipient.0,
+                        cli_opts.network,
+                        dns_resolver.clone(),
+                    )
+                    .await
+                    .map_err(|e| Error::Generic(format!("Parsing error occured {e:#?}")))?;
+                    let payment = process_instructions(amount, &instructions, resolver).await?;
+
+                    recipients.push((payment.receiving_addr.unwrap().into(), amount));
+                }
+
                 tx_builder.set_recipients(recipients);
             }
 
@@ -1367,6 +1394,20 @@ pub(crate) fn handle_compile_subcommand(
     }
 }
 
+#[cfg(feature = "dns_payment")]
+pub(crate) async fn handle_resolve_dns_recipient_command(
+    pretty: bool,
+    hrn: &str,
+    resolver: String,
+    network: Network,
+) -> Result<String, Error> {
+    let resolved = resolve_dns_recipient(hrn, network, resolver)
+        .await
+        .map_err(|e| Error::Generic(format!("{:?}", e)))?;
+
+    resolved.display(pretty)
+}
+
 /// Handle wallets command to show all saved wallet configurations
 pub fn handle_wallets_subcommand(datadir: &Path, pretty: bool) -> Result<String, Error> {
     let load_config = WalletConfig::load(datadir)?;
@@ -1619,7 +1660,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
                     &wallet_opts,
                     &cli_opts,
                     offline_subcommand.clone(),
-                )?;
+                )
+                .await?;
                 wallet.persist(&mut persister)?;
                 result
             };
@@ -1631,7 +1673,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
                     &wallet_opts,
                     &cli_opts,
                     offline_subcommand.clone(),
-                )?
+                )
+                .await?
             };
             Ok(result)
         }
@@ -1757,6 +1800,18 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
 
             Ok("".to_string())
         }
+
+        #[cfg(feature = "dns_payment")]
+        CliSubCommand::ResolveDnsRecipient { hrn, resolver } => {
+            let res = handle_resolve_dns_recipient_command(
+                cli_opts.pretty,
+                &hrn,
+                resolver,
+                cli_opts.network,
+            )
+            .await?;
+            Ok(res)
+        }
     };
     result
 }
@@ -1803,6 +1858,7 @@ async fn respond(
         } => {
             let value =
                 handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
+                    .await
                     .map_err(|e| e.to_string())?;
             Some(value)
         }
index 90d701b0527f12fb8ddd14cecf9414c377af3b0f..6b7745c1d01a71f8ada0174097587c5c9c868240 100644 (file)
@@ -25,6 +25,9 @@ mod payjoin;
 mod persister;
 mod utils;
 
+#[cfg(feature = "dns_payment")]
+mod dns_payment_instructions;
+
 use bdk_wallet::bitcoin::Network;
 use log::{debug, error, warn};
 
index 47139fbf9d4575520d6640b50b5291ef91e114ff..5c71dfb99f344e921899f3011af79f8717030437 100644 (file)
@@ -96,6 +96,17 @@ pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u
     Ok((key, value))
 }
 
+#[cfg(feature = "dns_payment")]
+/// Parse dns recipients in the form "test@me.com:10000" from cli input
+pub(crate) fn parse_dns_recipient(s: &str) -> Result<(String, u64), String> {
+    let parts: Vec<_> = s.split(':').collect();
+    if parts.len() != 2 {
+        return Err("Invalid format".to_string());
+    }
+    let sending_amount = u64::from_str(parts[1]).map_err(|e| e.to_string())?;
+    Ok((parts[0].to_string(), sending_amount))
+}
+
 #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
 /// Parse the proxy (Socket:Port) argument from the cli input.
 pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), Error> {