mirror of
https://github.com/lndk-org/lndk.git
synced 2026-08-13 12:33:05 +02:00
feat: pay BOLT 12 offers via BIP-353 human-readable names
Resolves a name like `alice@example.com` to a BOLT 12 offer over DoH and pays it. DNSSEC is validated locally, as required by BIP-353. Self-resolution only for now; peer-based resolution via BLIP-32 will follow. Exposes a `PayHumanReadableAddress` gRPC endpoint and a `lndk-cli pay-hrn` command.
This commit is contained in:
parent
e0b23440a3
commit
bb14ecd7ec
10 changed files with 821 additions and 31 deletions
377
Cargo.lock
generated
377
Cargo.lock
generated
|
|
@ -322,6 +322,22 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-payment-instructions"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c300c948b2ff78c965ea3a613372352125448a22f1acf49e95e3878149824091"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitreq",
|
||||
"dnssec-prover",
|
||||
"getrandom 0.3.3",
|
||||
"lightning 0.2.2",
|
||||
"lightning-invoice 0.34.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-units"
|
||||
version = "0.1.2"
|
||||
|
|
@ -379,6 +395,19 @@ version = "2.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bitreq"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08221cf31c5f00fb6fc8fa697cea54176b06801a518bd9d3482aa27099827a3a"
|
||||
dependencies = [
|
||||
"rustls 0.21.12",
|
||||
"rustls-webpki 0.101.7",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
|
|
@ -744,6 +773,15 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fragile"
|
||||
version = "2.0.0"
|
||||
|
|
@ -1028,7 +1066,7 @@ dependencies = [
|
|||
"rustls 0.23.31",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
|
|
@ -1089,6 +1127,109 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
|
|
@ -1158,11 +1299,11 @@ dependencies = [
|
|||
"bitcoin-bech32",
|
||||
"chrono",
|
||||
"libc",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
"lightning-background-processor",
|
||||
"lightning-block-sync",
|
||||
"lightning-dns-resolver",
|
||||
"lightning-invoice",
|
||||
"lightning-invoice 0.33.2",
|
||||
"lightning-net-tokio",
|
||||
"lightning-persister",
|
||||
"lightning-rapid-gossip-sync",
|
||||
|
|
@ -1195,10 +1336,27 @@ dependencies = [
|
|||
"dnssec-prover",
|
||||
"hashbrown 0.13.2",
|
||||
"libm",
|
||||
"lightning-invoice",
|
||||
"lightning-types",
|
||||
"lightning-invoice 0.33.2",
|
||||
"lightning-types 0.2.0",
|
||||
"musig2",
|
||||
"possiblyrandom",
|
||||
"possiblyrandom 0.2.0 (git+https://github.com/vincenzopalazzo/rust-lightning?branch=2025-06-cherry-pick-exposed-offer-amount-fix)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightning"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c90397b635e3ece6b9a723fb470a46cb9b3592f217d72e40540a5fada00289d"
|
||||
dependencies = [
|
||||
"bech32 0.11.0",
|
||||
"bitcoin",
|
||||
"dnssec-prover",
|
||||
"hashbrown 0.13.2",
|
||||
"libm",
|
||||
"lightning-invoice 0.34.0",
|
||||
"lightning-macros",
|
||||
"lightning-types 0.3.1",
|
||||
"possiblyrandom 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1209,7 +1367,7 @@ dependencies = [
|
|||
"bitcoin",
|
||||
"bitcoin-io",
|
||||
"bitcoin_hashes",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
"lightning-rapid-gossip-sync",
|
||||
]
|
||||
|
||||
|
|
@ -1220,7 +1378,7 @@ source = "git+https://github.com/vincenzopalazzo/rust-lightning?branch=2025-06-c
|
|||
dependencies = [
|
||||
"bitcoin",
|
||||
"chunked_transfer",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
|
@ -1231,8 +1389,8 @@ version = "0.2.0"
|
|||
source = "git+https://github.com/vincenzopalazzo/rust-lightning?branch=2025-06-cherry-pick-exposed-offer-amount-fix#afa9bea123b15fe3f2b602a126de94f13080272f"
|
||||
dependencies = [
|
||||
"dnssec-prover",
|
||||
"lightning",
|
||||
"lightning-types",
|
||||
"lightning 0.1.3",
|
||||
"lightning-types 0.2.0",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
@ -1243,7 +1401,29 @@ source = "git+https://github.com/vincenzopalazzo/rust-lightning?branch=2025-06-c
|
|||
dependencies = [
|
||||
"bech32 0.11.0",
|
||||
"bitcoin",
|
||||
"lightning-types",
|
||||
"lightning-types 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightning-invoice"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b85e5e14bcdb30d746e9785b04f27938292e8944f78f26517e01e91691f6b3f2"
|
||||
dependencies = [
|
||||
"bech32 0.11.0",
|
||||
"bitcoin",
|
||||
"lightning-types 0.3.1",
|
||||
]
|
||||
|
||||
[[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 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1252,7 +1432,7 @@ version = "0.1.0"
|
|||
source = "git+https://github.com/vincenzopalazzo/rust-lightning?branch=2025-06-cherry-pick-exposed-offer-amount-fix#afa9bea123b15fe3f2b602a126de94f13080272f"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
@ -1262,7 +1442,7 @@ version = "0.1.0"
|
|||
source = "git+https://github.com/vincenzopalazzo/rust-lightning?branch=2025-06-cherry-pick-exposed-offer-amount-fix#afa9bea123b15fe3f2b602a126de94f13080272f"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
|
|
@ -1274,7 +1454,7 @@ dependencies = [
|
|||
"bitcoin",
|
||||
"bitcoin-io",
|
||||
"bitcoin_hashes",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1285,12 +1465,27 @@ dependencies = [
|
|||
"bitcoin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightning-types"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb1aac93f22f2c2eac8a0ee83bb1a1ea58673caa2c82847302710b83364d04e6"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "lndk"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1298,6 +1493,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"async_fn_traits",
|
||||
"bitcoin",
|
||||
"bitcoin-payment-instructions",
|
||||
"bitcoincore-rpc",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
|
@ -1309,7 +1505,7 @@ dependencies = [
|
|||
"hex",
|
||||
"home",
|
||||
"ldk-sample",
|
||||
"lightning",
|
||||
"lightning 0.1.3",
|
||||
"lndk-tonic-lnd",
|
||||
"log",
|
||||
"log4rs",
|
||||
|
|
@ -1328,6 +1524,7 @@ dependencies = [
|
|||
"tonic-prost-build",
|
||||
"tonic-types",
|
||||
"triggered",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1699,6 +1896,15 @@ version = "0.3.30"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "possiblyrandom"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b122a615d72104fb3d8b26523fdf9232cd8ee06949fb37e4ce3ff964d15dffd"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "possiblyrandom"
|
||||
version = "0.2.0"
|
||||
|
|
@ -1707,6 +1913,15 @@ dependencies = [
|
|||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
|
@ -1840,9 +2055,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
|
@ -2250,6 +2465,12 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -2411,6 +2632,16 @@ dependencies = [
|
|||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.38.1"
|
||||
|
|
@ -2440,6 +2671,16 @@ dependencies = [
|
|||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.2"
|
||||
|
|
@ -2505,7 +2746,7 @@ dependencies = [
|
|||
"socket2 0.6.0",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
|
|
@ -2685,6 +2926,23 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
|
|
@ -2981,6 +3239,12 @@ version = "0.45.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.17.0"
|
||||
|
|
@ -3019,12 +3283,89 @@ dependencies = [
|
|||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ tar = "0.4.45"
|
|||
triggered = "0.1.2"
|
||||
prost = "0.14.1"
|
||||
async_fn_traits = "0.1.1"
|
||||
bitcoin-payment-instructions = { version = "0.7", features = ["http"] }
|
||||
url = "2.5"
|
||||
|
||||
# Integration test dependencies, only enabled with itest feature
|
||||
bitcoincore-rpc = { version = "0.19.0", optional = true }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import "external/google/rpc/error_details.proto";
|
|||
|
||||
service Offers {
|
||||
rpc PayOffer (PayOfferRequest) returns (PayOfferResponse);
|
||||
rpc PayHumanReadableAddress (PayHumanReadableAddressRequest) returns (PayOfferResponse);
|
||||
rpc GetInvoice (GetInvoiceRequest) returns (GetInvoiceResponse);
|
||||
rpc DecodeInvoice (DecodeInvoiceRequest) returns (Bolt12InvoiceContents);
|
||||
rpc PayInvoice (PayInvoiceRequest) returns (PayInvoiceResponse);
|
||||
|
|
@ -20,6 +21,15 @@ message PayOfferRequest {
|
|||
optional uint32 fee_limit_percent = 6;
|
||||
}
|
||||
|
||||
message PayHumanReadableAddressRequest {
|
||||
string name = 1;
|
||||
optional uint64 amount = 2;
|
||||
optional string payer_note = 3;
|
||||
optional uint32 response_invoice_timeout = 4;
|
||||
optional uint32 fee_limit = 5;
|
||||
optional uint32 fee_limit_percent = 6;
|
||||
}
|
||||
|
||||
message PayOfferResponse {
|
||||
string payment_preimage = 2;
|
||||
}
|
||||
|
|
|
|||
78
src/cli.rs
78
src/cli.rs
|
|
@ -1,7 +1,10 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use lightning::offers::invoice::Bolt12Invoice;
|
||||
use lndk::lndkrpc::offers_client::OffersClient;
|
||||
use lndk::lndkrpc::{CreateOfferRequest, GetInvoiceRequest, PayInvoiceRequest, PayOfferRequest};
|
||||
use lndk::lndkrpc::{
|
||||
CreateOfferRequest, GetInvoiceRequest, PayHumanReadableAddressRequest, PayInvoiceRequest,
|
||||
PayOfferRequest,
|
||||
};
|
||||
use lndk::offers::decode;
|
||||
use lndk::offers::handler::DEFAULT_RESPONSE_INVOICE_TIMEOUT;
|
||||
use lndk::{
|
||||
|
|
@ -121,8 +124,37 @@ enum Commands {
|
|||
/// The offer string.
|
||||
offer_string: String,
|
||||
|
||||
/// Amount the user would like to pay. If this isn't set, we'll assume the user is paying
|
||||
/// whatever the offer amount is.
|
||||
/// Amount in millisatoshis the user would like to pay. If this isn't set, we'll assume
|
||||
/// the user is paying whatever the offer amount is.
|
||||
#[arg(required = false)]
|
||||
amount: Option<u64>,
|
||||
|
||||
/// A payer-provided note which will be seen by the recipient.
|
||||
#[arg(required = false)]
|
||||
payer_note: Option<String>,
|
||||
|
||||
/// The amount of time in seconds that the user would like to wait for an invoice to
|
||||
/// arrive. If this isn't set, we'll use the default value.
|
||||
#[arg(long, global = false, required = false, default_value = DEFAULT_RESPONSE_INVOICE_TIMEOUT.to_string())]
|
||||
response_invoice_timeout: Option<u32>,
|
||||
|
||||
/// A fixed fee limit in millisatoshis.
|
||||
/// Mutually exclusive with fee_limit_percent - only one can be set.
|
||||
#[arg(long, required = false, conflicts_with = "fee_limit_percent")]
|
||||
fee_limit: Option<u32>,
|
||||
/// A percentage-based fee limit of the payment amount.
|
||||
/// Mutually exclusive with fee_limit - only one can be set.
|
||||
#[arg(long, required = false, conflicts_with = "fee_limit")]
|
||||
fee_limit_percent: Option<u32>,
|
||||
},
|
||||
/// PayHrn pays a BOLT 12 offer by resolving a human-readable name (BIP-353).
|
||||
#[command(name = "pay-hrn")]
|
||||
PayHrn {
|
||||
/// The human-readable name to resolve (e.g., "user@example.com").
|
||||
name: String,
|
||||
|
||||
/// Amount in millisatoshis the user would like to pay. If this isn't set, we'll assume
|
||||
/// the user is paying whatever the offer amount is.
|
||||
#[arg(required = false)]
|
||||
amount: Option<u64>,
|
||||
|
||||
|
|
@ -150,8 +182,8 @@ enum Commands {
|
|||
/// The offer string.
|
||||
offer_string: String,
|
||||
|
||||
/// Amount the user would like to pay. If this isn't set, we'll assume the user is paying
|
||||
/// whatever the offer amount is.
|
||||
/// Amount in millisatoshis the user would like to pay. If this isn't set, we'll assume
|
||||
/// the user is paying whatever the offer amount is.
|
||||
#[arg(required = false)]
|
||||
amount: Option<u64>,
|
||||
|
||||
|
|
@ -168,8 +200,8 @@ enum Commands {
|
|||
PayInvoice {
|
||||
/// The hex-encoded invoice string.
|
||||
invoice_string: String,
|
||||
/// Amount the user would like to pay. If this isn't set, we'll assume the user is paying
|
||||
/// whatever the invoice amount is set to.
|
||||
/// Amount in millisatoshis the user would like to pay. If this isn't set, we'll assume
|
||||
/// the user is paying whatever the invoice amount is set to.
|
||||
#[arg(required = false)]
|
||||
amount: Option<u64>,
|
||||
/// A fixed fee limit in millisatoshis.
|
||||
|
|
@ -263,6 +295,38 @@ async fn main() {
|
|||
Err(err) => err.exit_gracefully(),
|
||||
};
|
||||
}
|
||||
Commands::PayHrn {
|
||||
ref name,
|
||||
amount,
|
||||
payer_note,
|
||||
response_invoice_timeout,
|
||||
fee_limit,
|
||||
fee_limit_percent,
|
||||
} => {
|
||||
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
|
||||
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
|
||||
let (mut client, macaroon) = create_authenticated_client(
|
||||
channel,
|
||||
args.macaroon_path,
|
||||
args.macaroon_hex,
|
||||
&args.network,
|
||||
);
|
||||
|
||||
let mut request = Request::new(PayHumanReadableAddressRequest {
|
||||
name: name.clone(),
|
||||
amount,
|
||||
payer_note,
|
||||
response_invoice_timeout,
|
||||
fee_limit,
|
||||
fee_limit_percent,
|
||||
});
|
||||
add_metadata(&mut request, macaroon);
|
||||
|
||||
match client.pay_human_readable_address(request).await {
|
||||
Ok(_) => println!("Successfully paid for name {name}!"),
|
||||
Err(err) => err.exit_gracefully(),
|
||||
};
|
||||
}
|
||||
Commands::GetInvoice {
|
||||
ref offer_string,
|
||||
amount,
|
||||
|
|
|
|||
118
src/dns_resolver.rs
Normal file
118
src/dns_resolver.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use crate::OfferError;
|
||||
use bitcoin_payment_instructions::hrn_resolution::{HrnResolution, HrnResolver, HumanReadableName};
|
||||
use bitcoin_payment_instructions::http_resolver::HTTPHrnResolver;
|
||||
use log::{debug, info};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
const DEFAULT_DNS_TIMEOUT_SECS: u64 = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DNSResolverMessageHandler {
|
||||
resolver: Arc<dyn HrnResolver + Send + Sync>,
|
||||
}
|
||||
|
||||
impl Default for DNSResolverMessageHandler {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DNSResolverMessageHandler {
|
||||
pub fn new() -> Self {
|
||||
Self::with_resolver(HTTPHrnResolver::new())
|
||||
}
|
||||
|
||||
pub fn with_resolver<R: HrnResolver + Send + Sync + 'static>(resolver: R) -> Self {
|
||||
Self {
|
||||
resolver: Arc::new(resolver),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_hrn_to_offer(&self, name_str: &str) -> Result<String, OfferError> {
|
||||
info!("Resolving human-readable name: {}", name_str);
|
||||
let resolved_uri = self.resolve_locally(name_str.to_string()).await?;
|
||||
debug!("Resolved {} to URI: {}", name_str, resolved_uri);
|
||||
|
||||
let offer = self.extract_offer_from_uri(&resolved_uri)?;
|
||||
info!("Successfully resolved {} to offer: {}", name_str, offer);
|
||||
Ok(offer)
|
||||
}
|
||||
|
||||
pub fn extract_offer_from_uri(&self, uri: &str) -> Result<String, OfferError> {
|
||||
let url = Url::parse(uri)
|
||||
.map_err(|_| OfferError::ResolveUriError("Invalid URI format".to_string()))?;
|
||||
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key.eq_ignore_ascii_case("lno") {
|
||||
return Ok(value.into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
Err(OfferError::ResolveUriError(
|
||||
"URI does not contain 'lno' parameter with BOLT12 offer".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn resolve_locally(&self, name: String) -> Result<String, OfferError> {
|
||||
let hrn_parsed = HumanReadableName::from_encoded(&name)
|
||||
.map_err(|_| OfferError::ParseHrnFailure(name.clone()))?;
|
||||
|
||||
let resolution = tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_DNS_TIMEOUT_SECS),
|
||||
self.resolver.resolve_hrn(&hrn_parsed),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
OfferError::HrnResolutionFailure(format!(
|
||||
"{name}: resolution timed out after {DEFAULT_DNS_TIMEOUT_SECS}s"
|
||||
))
|
||||
})?
|
||||
.map_err(|e| OfferError::HrnResolutionFailure(format!("{name}: {e}")))?;
|
||||
|
||||
let uri = match resolution {
|
||||
HrnResolution::DNSSEC { result, .. } => result,
|
||||
HrnResolution::LNURLPay { .. } => {
|
||||
return Err(OfferError::ResolveUriError(
|
||||
"LNURL resolution not supported in this flow".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_offer_from_simple_uri() {
|
||||
let handler = DNSResolverMessageHandler::new();
|
||||
let uri = "bitcoin:?lno=lno1qgsqvgnwgcg35z";
|
||||
let result = handler.extract_offer_from_uri(uri);
|
||||
assert_eq!(result.unwrap(), "lno1qgsqvgnwgcg35z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_offer_with_percent_encoding() {
|
||||
let handler = DNSResolverMessageHandler::new();
|
||||
let uri = "bitcoin:?lno=lno1%20test%3Dvalue";
|
||||
let result = handler.extract_offer_from_uri(uri);
|
||||
assert_eq!(result.unwrap(), "lno1 test=value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_offer_missing_param() {
|
||||
let handler = DNSResolverMessageHandler::new();
|
||||
let uri = "bitcoin:?amount=50&label=test";
|
||||
let result = handler.extract_offer_from_uri(uri);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("does not contain 'lno' parameter"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod clock;
|
||||
mod dns_resolver;
|
||||
mod grpc;
|
||||
#[allow(dead_code)]
|
||||
pub mod lnd;
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ use super::lnd_requests::{
|
|||
send_invoice_request, LndkBolt12InvoiceInfo,
|
||||
};
|
||||
use super::OfferError;
|
||||
use crate::dns_resolver::DNSResolverMessageHandler;
|
||||
use crate::offers::lnd_requests::{send_payment, track_payment, CreateOfferArgs};
|
||||
use crate::offers::{get_destination, parse::decode};
|
||||
use crate::onion_messenger::MessengerUtilities;
|
||||
use bitcoin_payment_instructions::hrn_resolution::HrnResolver;
|
||||
|
||||
pub const DEFAULT_RESPONSE_INVOICE_TIMEOUT: u32 = 15;
|
||||
|
||||
|
|
@ -57,6 +60,7 @@ pub struct OfferHandler {
|
|||
/// an invoice. If not provided, we will use the default value of 15 seconds.
|
||||
pub response_invoice_timeout: u32,
|
||||
client: Option<Client>,
|
||||
dns_resolver: DNSResolverMessageHandler,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -77,6 +81,18 @@ pub struct PayOfferParams {
|
|||
pub fee_limit: Option<FeeLimit>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PayHumanReadableAddressParams {
|
||||
pub name: String,
|
||||
pub amount: Option<u64>,
|
||||
pub payer_note: Option<String>,
|
||||
pub network: Network,
|
||||
pub client: Client,
|
||||
pub reply_path: Option<BlindedMessagePath>,
|
||||
pub response_invoice_timeout: Option<u32>,
|
||||
pub fee_limit: Option<FeeLimit>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SendPaymentParams {
|
||||
pub path: BlindedPaymentPath,
|
||||
|
|
@ -115,6 +131,36 @@ impl OfferHandler {
|
|||
response_invoice_timeout: Option<u32>,
|
||||
seed: Option<[u8; 32]>,
|
||||
client: Option<Client>,
|
||||
) -> Self {
|
||||
Self::with_dns_resolver(
|
||||
response_invoice_timeout,
|
||||
seed,
|
||||
client,
|
||||
DNSResolverMessageHandler::new(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds an `OfferHandler` with a custom `HrnResolver` implementation. Intended for tests
|
||||
/// that need to inject a mock resolver; production code should use `new`.
|
||||
pub fn with_hrn_resolver<R: HrnResolver + Send + Sync + 'static>(
|
||||
response_invoice_timeout: Option<u32>,
|
||||
seed: Option<[u8; 32]>,
|
||||
client: Option<Client>,
|
||||
hrn_resolver: R,
|
||||
) -> Self {
|
||||
Self::with_dns_resolver(
|
||||
response_invoice_timeout,
|
||||
seed,
|
||||
client,
|
||||
DNSResolverMessageHandler::with_resolver(hrn_resolver),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn with_dns_resolver(
|
||||
response_invoice_timeout: Option<u32>,
|
||||
seed: Option<[u8; 32]>,
|
||||
client: Option<Client>,
|
||||
dns_resolver: DNSResolverMessageHandler,
|
||||
) -> Self {
|
||||
let messenger_utils = MessengerUtilities::default();
|
||||
let random_bytes = match seed {
|
||||
|
|
@ -132,6 +178,7 @@ impl OfferHandler {
|
|||
expanded_key,
|
||||
response_invoice_timeout,
|
||||
client,
|
||||
dns_resolver,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +199,28 @@ impl OfferHandler {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Resolves a human-readable name (BIP-353) to an offer and pays it.
|
||||
pub async fn pay_hrn(&self, cfg: PayHumanReadableAddressParams) -> Result<Payment, OfferError> {
|
||||
let offer_str = self.dns_resolver.resolve_hrn_to_offer(&cfg.name).await?;
|
||||
|
||||
let offer = decode(offer_str)?;
|
||||
let destination = get_destination(&offer).await?;
|
||||
|
||||
let pay_offer_cfg = PayOfferParams {
|
||||
offer,
|
||||
amount: cfg.amount,
|
||||
payer_note: cfg.payer_note,
|
||||
network: cfg.network,
|
||||
client: cfg.client,
|
||||
destination,
|
||||
reply_path: cfg.reply_path,
|
||||
response_invoice_timeout: cfg.response_invoice_timeout,
|
||||
fee_limit: cfg.fee_limit,
|
||||
};
|
||||
|
||||
self.pay_offer(pay_offer_cfg).await
|
||||
}
|
||||
|
||||
/// Sends an invoice request and waits for an invoice to be sent back to us.
|
||||
/// Reminder that if this method returns an error after create_invoice_request is called, we
|
||||
/// *must* remove the payment_id from self.active_payments.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ pub enum OfferError {
|
|||
EncodeInvoiceFailure(BitcoinIoError),
|
||||
/// Cannot list channels.
|
||||
ListChannelsFailure(String),
|
||||
/// Failed to parse human-readable name.
|
||||
ParseHrnFailure(String),
|
||||
/// HRN resolution failed.
|
||||
HrnResolutionFailure(String),
|
||||
/// DNS resolution or URI parsing error.
|
||||
ResolveUriError(String),
|
||||
}
|
||||
|
||||
impl OfferError {
|
||||
|
|
@ -101,6 +107,9 @@ impl OfferError {
|
|||
OfferError::IntroductionNodeNotFound => "INTRODUCTION_NODE_NOT_FOUND",
|
||||
OfferError::GetChannelInfo(_) => "GET_CHANNEL_INFO",
|
||||
OfferError::ListChannelsFailure(_) => "LIST_CHANNELS_FAILURE",
|
||||
OfferError::ParseHrnFailure(_) => "PARSE_HRN_FAILURE",
|
||||
OfferError::HrnResolutionFailure(_) => "HRN_RESOLUTION_FAILURE",
|
||||
OfferError::ResolveUriError(_) => "RESOLVE_URI_ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +119,10 @@ impl OfferError {
|
|||
| OfferError::InvalidCurrency
|
||||
| OfferError::ParseOfferFailure(_)
|
||||
| OfferError::ParseInvoiceFailure(_)
|
||||
| OfferError::EncodeInvoiceFailure(_) => Code::InvalidArgument,
|
||||
| OfferError::EncodeInvoiceFailure(_)
|
||||
| OfferError::ParseHrnFailure(_)
|
||||
| OfferError::HrnResolutionFailure(_)
|
||||
| OfferError::ResolveUriError(_) => Code::InvalidArgument,
|
||||
_ => Code::Internal,
|
||||
}
|
||||
}
|
||||
|
|
@ -168,6 +180,15 @@ impl Display for OfferError {
|
|||
}
|
||||
OfferError::ListChannelsFailure(e) => write!(f, "Error listing channels: {e:?}"),
|
||||
OfferError::EncodeInvoiceFailure(e) => write!(f, "Failed to encode invoice: {e:?}"),
|
||||
OfferError::ParseHrnFailure(e) => {
|
||||
write!(f, "Invalid human-readable name: {e}")
|
||||
}
|
||||
OfferError::HrnResolutionFailure(e) => {
|
||||
write!(f, "HRN resolution failed: {e}")
|
||||
}
|
||||
OfferError::ResolveUriError(e) => {
|
||||
write!(f, "Failed to resolve URI: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::lnd::{get_lnd_client, get_network, Creds, LndCfg, LndError};
|
||||
use crate::lndkrpc::{CreateOfferRequest, CreateOfferResponse};
|
||||
use crate::offers::get_destination;
|
||||
use crate::offers::handler::{CreateOfferParams, PayOfferParams};
|
||||
use crate::offers::handler::{CreateOfferParams, PayHumanReadableAddressParams, PayOfferParams};
|
||||
use crate::offers::parse::decode;
|
||||
use crate::offers::validate_amount;
|
||||
use crate::offers::OfferError;
|
||||
|
|
@ -17,8 +17,8 @@ use lightning::util::ser::Writeable;
|
|||
use lndkrpc::offers_server::Offers;
|
||||
use lndkrpc::{
|
||||
Bolt12InvoiceContents, DecodeInvoiceRequest, FeatureBit, GetInvoiceRequest, GetInvoiceResponse,
|
||||
PayInvoiceRequest, PayInvoiceResponse, PayOfferRequest, PayOfferResponse, PaymentHash,
|
||||
PaymentPaths,
|
||||
PayHumanReadableAddressRequest, PayInvoiceRequest, PayInvoiceResponse, PayOfferRequest,
|
||||
PayOfferResponse, PaymentHash, PaymentPaths,
|
||||
};
|
||||
use std::num::NonZeroU64;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -111,6 +111,57 @@ impl Offers for LNDKServer {
|
|||
Ok(Response::new(reply))
|
||||
}
|
||||
|
||||
async fn pay_human_readable_address(
|
||||
&self,
|
||||
request: Request<PayHumanReadableAddressRequest>,
|
||||
) -> Result<Response<PayOfferResponse>, Status> {
|
||||
log::info!("Received a request: {:?}", request.get_ref());
|
||||
|
||||
let metadata = request.metadata();
|
||||
let macaroon = check_auth_metadata(metadata)?;
|
||||
let creds = Creds::String {
|
||||
cert: self.lnd_cert.clone(),
|
||||
macaroon,
|
||||
};
|
||||
let lnd_cfg = LndCfg::new(self.address.clone(), creds);
|
||||
let mut client = get_lnd_client(lnd_cfg)?;
|
||||
|
||||
let inner_request = request.get_ref();
|
||||
let reply_path = None;
|
||||
let info = client
|
||||
.lightning()
|
||||
.get_info(GetInfoRequest {})
|
||||
.await
|
||||
.map_err(|e| LndError::ServiceUnavailable(e.message().to_string()))?
|
||||
.into_inner();
|
||||
let network = get_network(info).await?;
|
||||
|
||||
let fee_limit = create_fee_limit(inner_request.fee_limit, inner_request.fee_limit_percent);
|
||||
|
||||
let cfg = PayHumanReadableAddressParams {
|
||||
name: inner_request.name.clone(),
|
||||
amount: inner_request.amount,
|
||||
payer_note: inner_request.payer_note.clone(),
|
||||
network,
|
||||
client,
|
||||
reply_path,
|
||||
response_invoice_timeout: inner_request.response_invoice_timeout,
|
||||
fee_limit,
|
||||
};
|
||||
|
||||
let payment = self.offer_handler.pay_hrn(cfg).await?;
|
||||
log::info!(
|
||||
"Payment succeeded with preimage: {}",
|
||||
payment.payment_preimage
|
||||
);
|
||||
|
||||
let reply = PayOfferResponse {
|
||||
payment_preimage: payment.payment_preimage,
|
||||
};
|
||||
|
||||
Ok(Response::new(reply))
|
||||
}
|
||||
|
||||
async fn decode_invoice(
|
||||
&self,
|
||||
request: Request<DecodeInvoiceRequest>,
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ use std::net::SocketAddr;
|
|||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::time;
|
||||
use tokio::time::Duration;
|
||||
|
||||
use tokio::{select, try_join};
|
||||
use tonic_lnd::Client;
|
||||
|
||||
|
|
@ -1440,3 +1440,116 @@ async fn test_receive_payment_from_offer_with_multiple_blinded_paths() {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct MockHrnResolver {
|
||||
response_uri: String,
|
||||
}
|
||||
|
||||
impl bitcoin_payment_instructions::hrn_resolution::HrnResolver for MockHrnResolver {
|
||||
fn resolve_hrn<'a>(
|
||||
&'a self,
|
||||
_hrn: &'a bitcoin_payment_instructions::hrn_resolution::HumanReadableName,
|
||||
) -> bitcoin_payment_instructions::hrn_resolution::HrnResolutionFuture<'a> {
|
||||
let uri = self.response_uri.clone();
|
||||
Box::pin(async move {
|
||||
Ok(
|
||||
bitcoin_payment_instructions::hrn_resolution::HrnResolution::DNSSEC {
|
||||
proof: None,
|
||||
result: uri,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_lnurl<'a>(
|
||||
&'a self,
|
||||
_url: &'a str,
|
||||
) -> bitcoin_payment_instructions::hrn_resolution::HrnResolutionFuture<'a> {
|
||||
unimplemented!("LNURL resolution not supported in mock")
|
||||
}
|
||||
|
||||
fn resolve_lnurl_to_invoice<'a>(
|
||||
&'a self,
|
||||
_callback_url: String,
|
||||
_amount: bitcoin_payment_instructions::amount::Amount,
|
||||
_expected_description_hash: [u8; 32],
|
||||
) -> bitcoin_payment_instructions::hrn_resolution::LNURLResolutionFuture<'a> {
|
||||
unimplemented!("LNURL invoice resolution not supported in mock")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_pay_offer_with_name_and_dns() {
|
||||
use lndk::offers::handler::PayHumanReadableAddressParams;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
let test_name = "lndk_pay_offer_with_name";
|
||||
let (bitcoind, mut lnd, ldk1, ldk2, lndk_dir, _) =
|
||||
common::setup_test_infrastructure(test_name).await;
|
||||
|
||||
let (ldk1_pubkey, ldk2_pubkey, _) =
|
||||
common::connect_network(&ldk1, &ldk2, false, true, &mut lnd, &bitcoind).await;
|
||||
|
||||
let path_pubkeys = vec![ldk2_pubkey, ldk1_pubkey];
|
||||
let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
|
||||
let offer = ldk1
|
||||
.create_offer(
|
||||
&path_pubkeys,
|
||||
Network::Regtest,
|
||||
20_000,
|
||||
Quantity::One,
|
||||
expiration,
|
||||
)
|
||||
.await
|
||||
.expect("should create offer");
|
||||
|
||||
let offer_string = offer.to_string();
|
||||
let human_readable_name = "satoshi@bip353.test";
|
||||
|
||||
let mock_resolver = MockHrnResolver {
|
||||
response_uri: format!("bitcoin:?lno={}", offer_string),
|
||||
};
|
||||
|
||||
let handler = Arc::new(lndk::offers::handler::OfferHandler::with_hrn_resolver(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
mock_resolver,
|
||||
));
|
||||
|
||||
let (lndk_cfg, _, messenger, shutdown) = common::setup_lndk(
|
||||
&lnd.cert_path,
|
||||
&lnd.macaroon_path,
|
||||
lnd.address.clone(),
|
||||
lndk_dir,
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = lnd.client.clone().unwrap();
|
||||
|
||||
let pay_cfg = PayHumanReadableAddressParams {
|
||||
name: human_readable_name.to_string(),
|
||||
amount: Some(20_000),
|
||||
payer_note: None,
|
||||
network: Network::Regtest,
|
||||
client: client.clone(),
|
||||
reply_path: None,
|
||||
response_invoice_timeout: None,
|
||||
fee_limit: None,
|
||||
};
|
||||
|
||||
select! {
|
||||
val = messenger.run(lndk_cfg, Arc::clone(&handler)) => {
|
||||
panic!("messenger should not complete first {:?}", val);
|
||||
},
|
||||
res = handler.pay_hrn(pay_cfg) => {
|
||||
assert!(res.is_ok(), "pay_hrn failed: {:?}", res.err());
|
||||
let payment = res.unwrap();
|
||||
assert!(!payment.payment_preimage.is_empty(), "Payment should have a preimage");
|
||||
|
||||
shutdown.trigger();
|
||||
ldk1.stop().await;
|
||||
ldk2.stop().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue