From: Sonkeng Maldini Date: Wed, 7 Jan 2026 21:56:11 +0000 (+0100) Subject: feat: integrate BIP 353 dns payment instructions X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/struct.ScriptHash.html?a=commitdiff_plain;h=7a2c5dfef61885494391a88d23ebc6edd14add3e;p=bdk-cli feat: integrate BIP 353 dns payment instructions --- diff --git a/Cargo.lock b/Cargo.lock index 992f3c6..a5826c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 04a713f..8097662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/src/commands.rs b/src/commands.rs index 435cb68..f9a0e2a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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 index 0000000..88453d7 --- /dev/null +++ b/src/dns_payment_instructions.rs @@ -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, + pub min_amount: Option, + pub max_amount: Option, + pub description: Option, + pub expected_amount: Option, + pub receiving_addr: Option
, + pub hrn: String, + pub notes: String, +} + +impl Payment { + pub fn display(&self, pretty: bool) -> Result { + let mut methods: Vec = 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 { + 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 { + 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) + } + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 4f7900a..f9b4ad6 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -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 { + 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 { let load_config = WalletConfig::load(datadir)?; @@ -1619,7 +1660,8 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { &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 { &wallet_opts, &cli_opts, offline_subcommand.clone(), - )? + ) + .await? }; Ok(result) } @@ -1757,6 +1800,18 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { 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) } diff --git a/src/main.rs b/src/main.rs index 90d701b..6b7745c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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}; diff --git a/src/utils.rs b/src/utils.rs index 47139fb..5c71dfb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -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> {