add support for holdinvoices

This commit is contained in:
daywalker90 2025-11-08 13:41:11 +01:00 committed by daywalker90
parent 140d295eec
commit c956338bcd
18 changed files with 2192 additions and 207 deletions

View file

@ -1,5 +1,11 @@
# Changelog # Changelog
## Unreleased
### Added
- holdinvoice methods: ``make_hold_invoice``, ``cancel_hold_invoice``, ``settle_hold_invoice``
- holdinvoice notification: ``hold_invoice_accepted``
## [0.1.9] 2026-04-23 ## [0.1.9] 2026-04-23
### Fixed ### Fixed

476
Cargo.lock generated
View file

@ -44,6 +44,17 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "async-utility" name = "async-utility"
version = "0.3.1" version = "0.3.1"
@ -81,6 +92,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "base58ck" name = "base58ck"
version = "0.1.0" version = "0.1.0"
@ -282,10 +299,14 @@ dependencies = [
"log-panics", "log-panics",
"nostr-sdk", "nostr-sdk",
"parking_lot", "parking_lot",
"prost",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"tonic",
"tonic-prost",
"tonic-prost-build",
"uuid", "uuid",
] ]
@ -386,12 +407,40 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
@ -555,6 +604,25 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "h2"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"
@ -566,9 +634,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.0" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "heck" name = "heck"
@ -616,12 +684,96 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-timeout"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"libc",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.1.1" version = "2.1.1"
@ -737,7 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.0", "hashbrown 0.17.1",
"serde", "serde",
"serde_core", "serde_core",
] ]
@ -764,6 +916,15 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@ -772,9 +933,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.94" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@ -800,6 +961,12 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.2" version = "0.8.2"
@ -862,6 +1029,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]] [[package]]
name = "negentropy" name = "negentropy"
version = "0.5.0" version = "0.5.0"
@ -1017,6 +1190,37 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap",
]
[[package]]
name = "pin-project"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -1071,6 +1275,79 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "prost"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"pulldown-cmark",
"pulldown-cmark-to-cmark",
"regex",
"syn",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "prost-types"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
dependencies = [
"prost",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad"
dependencies = [
"bitflags",
"memchr",
"unicase",
]
[[package]]
name = "pulldown-cmark-to-cmark"
version = "22.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90"
dependencies = [
"pulldown-cmark",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@ -1105,9 +1382,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.2" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.5", "rand_core 0.9.5",
@ -1203,12 +1480,26 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.40" version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [ dependencies = [
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -1422,6 +1713,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.13.2" version = "0.13.2"
@ -1433,6 +1730,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@ -1595,6 +1905,105 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tonic"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
dependencies = [
"async-trait",
"base64",
"bytes",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"socket2",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-build"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734"
dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tonic-prost"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
dependencies = [
"bytes",
"prost",
"tonic",
]
[[package]]
name = "tonic-prost-build"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a"
dependencies = [
"prettyplease",
"proc-macro2",
"prost-build",
"prost-types",
"quote",
"syn",
"tempfile",
"tonic-build",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"pin-project-lite",
"slab",
"sync_wrapper",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@ -1657,6 +2066,12 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.26.2" version = "0.26.2"
@ -1668,7 +2083,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"log", "log",
"rand 0.9.2", "rand 0.9.4",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
@ -1682,6 +2097,12 @@ version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@ -1767,6 +2188,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.1+wasi-snapshot-preview1" version = "0.11.1+wasi-snapshot-preview1"
@ -1793,9 +2223,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.117" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -1806,9 +2236,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.67" version = "0.4.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -1816,9 +2246,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.117" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -1826,9 +2256,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.117" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@ -1839,9 +2269,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.117" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1882,9 +2312,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.94" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -1896,14 +2326,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [ dependencies = [
"webpki-roots 1.0.6", "webpki-roots 1.0.7",
] ]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.6" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]

View file

@ -27,6 +27,17 @@ hex = "0.4"
regex = "1" regex = "1"
tonic = { version = "0.14", default-features = false, features = [
"codegen",
"transport",
"tls-ring",
] }
prost = "0.14"
tonic-prost = "0.14"
[build-dependencies]
tonic-prost-build = "0.14"
[profile.optimized] [profile.optimized]
inherits = "release" inherits = "release"

View file

@ -127,6 +127,9 @@ For a private relay you can for example use [nostr-rs-relay](https://github.com/
* list all NWC configurations or just the one with ``label`` * list all NWC configurations or just the one with ``label``
* ***label***: optional. The label the NWC was created with * ***label***: optional. The label the NWC was created with
## Holdinvoice support
For methods or notifications related to holdinvoices you need v0.3.2+ of [hold](https://github.com/BoltzExchange/hold) with enabled grpc (which is the default just make sure the port is free)
## Supported NWC methods ## Supported NWC methods
* ``pay_invoice`` * ``pay_invoice``
* ``multi_pay_invoice`` * ``multi_pay_invoice``
@ -137,10 +140,14 @@ For a private relay you can for example use [nostr-rs-relay](https://github.com/
* ``list_transactions`` * ``list_transactions``
* ``get_balance`` * ``get_balance``
* ``get_info`` (no ``block_hash``) * ``get_info`` (no ``block_hash``)
* ``make_hold_invoice`` (requires [Holdinvoice support](#holdinvoice_support))
* ``cancel_hold_invoice`` (requires [Holdinvoice support](#holdinvoice_support))
* ``settle_hold_invoice`` (requires [Holdinvoice support](#holdinvoice_support))
## Supported NWC notifications ## Supported NWC notifications
* ``payment_received`` * ``payment_received``
* ``payment_sent`` * ``payment_sent``
* ``hold_invoice_accepted`` (requires [Holdinvoice support](#holdinvoice_support))
## Supported content encryption: ## Supported content encryption:
* [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) * [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md)

6
build.rs Normal file
View file

@ -0,0 +1,6 @@
fn main() {
tonic_prost_build::configure()
.protoc_arg("--experimental_allow_proto3_optional")
.compile_protos(&["protos/hold.proto"], &["protos"])
.unwrap_or_else(|e| panic!("Could not build protos: {e}"));
}

185
protos/hold.proto Normal file
View file

@ -0,0 +1,185 @@
syntax = "proto3";
package hold;
service Hold {
rpc GetInfo (GetInfoRequest) returns (GetInfoResponse);
rpc Invoice (InvoiceRequest) returns (InvoiceResponse) {}
rpc Inject (InjectRequest) returns (InjectResponse) {}
rpc List (ListRequest) returns (ListResponse) {}
rpc Settle (SettleRequest) returns (SettleResponse) {}
rpc Cancel (CancelRequest) returns (CancelResponse) {}
// Cleans cancelled invoices
rpc Clean (CleanRequest) returns (CleanResponse) {}
rpc Track (TrackRequest) returns (stream TrackResponse) {}
rpc TrackAll (TrackAllRequest) returns (stream TrackAllResponse) {}
rpc OnionMessages (stream OnionMessageResponse) returns (stream OnionMessage) {}
}
enum HookAction {
Continue = 0;
Resolve = 1;
}
message GetInfoRequest {}
message GetInfoResponse {
string version = 1;
}
message Hop {
bytes public_key = 1;
uint64 short_channel_id = 2;
uint64 base_fee = 3;
uint64 ppm_fee = 4;
uint64 cltv_expiry_delta = 5;
}
message RoutingHint {
repeated Hop hops = 1;
}
message InvoiceRequest {
bytes payment_hash = 1;
uint64 amount_msat = 2;
oneof description {
string memo = 3;
bytes hash = 4;
}
optional uint64 expiry = 5;
optional uint64 min_final_cltv_expiry = 6;
repeated RoutingHint routing_hints = 7;
}
message InvoiceResponse {
string bolt11 = 1;
}
message InjectRequest {
string invoice = 1;
optional uint64 min_cltv_expiry = 2;
}
message InjectResponse {}
message ListRequest {
message Pagination {
// Inclusive
int64 index_start = 1;
uint64 limit = 2;
}
oneof constraint {
bytes payment_hash = 1;
Pagination pagination = 2;
}
}
enum InvoiceState {
UNPAID = 0;
ACCEPTED = 1;
PAID = 2;
CANCELLED = 3;
}
message Htlc {
int64 id = 1;
InvoiceState state = 2;
string scid = 3;
uint64 channel_id = 4;
uint64 msat = 5;
uint64 created_at = 6;
optional uint64 cltv_expiry = 7;
}
message Invoice {
int64 id = 1;
bytes payment_hash = 2;
optional bytes preimage = 3;
string invoice = 4;
InvoiceState state = 5;
uint64 created_at = 6;
optional uint64 settled_at = 8;
repeated Htlc htlcs = 7;
optional uint64 min_cltv_expiry = 9;
}
message ListResponse {
repeated Invoice invoices = 1;
}
message SettleRequest {
bytes payment_preimage = 1;
}
message SettleResponse {}
message CancelRequest {
bytes payment_hash = 1;
}
message CancelResponse {}
message CleanRequest {
// Clean everything older than age seconds
optional uint64 age = 1;
}
message CleanResponse {
uint64 cleaned = 1;
}
message TrackRequest {
bytes payment_hash = 1;
}
message TrackResponse {
InvoiceState state = 1;
}
message TrackAllRequest {
repeated bytes payment_hashes = 1;
}
message TrackAllResponse {
bytes payment_hash = 1;
string bolt11 = 2;
InvoiceState state = 3;
}
message OnionMessage {
message ReplyBlindedPath {
message Hop {
optional bytes blinded_node_id = 1;
optional bytes encrypted_recipient_data = 2;
}
optional bytes first_node_id = 1;
optional string first_scid = 2;
optional uint64 first_scid_dir = 3;
optional bytes first_path_key = 4;
repeated Hop hops = 5;
}
message UnknownField {
uint64 number = 1;
bytes value = 2;
}
uint64 id = 1;
optional bytes pathsecret = 2;
optional ReplyBlindedPath reply_blindedpath = 3;
optional bytes invoice_request = 4;
optional bytes invoice = 5;
optional bytes invoice_error = 6;
repeated UnknownField unknown_fields = 7;
}
message OnionMessageResponse {
uint64 id = 1;
HookAction action = 2;
}

View file

@ -1,4 +1,8 @@
use std::{path::Path, time::Duration}; use std::{
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
use anyhow::anyhow; use anyhow::anyhow;
use cln_plugin::{ use cln_plugin::{
@ -12,10 +16,17 @@ use nwc::run_nwc;
use nwc_notifications::{payment_received_handler, payment_sent_handler}; use nwc_notifications::{payment_received_handler, payment_sent_handler};
use parse::read_startup_options; use parse::read_startup_options;
use rpc::{nwc_budget, nwc_create, nwc_list, nwc_revoke}; use rpc::{nwc_budget, nwc_create, nwc_list, nwc_revoke};
use serde_json::json;
use structs::PluginState; use structs::PluginState;
use tokio::time; use tokio::time;
use tonic::transport::{Certificate, ClientTlsConfig, Endpoint, Identity};
use util::{load_nwc_store, update_nwc_store}; use util::{load_nwc_store, update_nwc_store};
use crate::{
hold::{hold_client::HoldClient, InvoiceState, ListRequest},
nwc_notifications::holdinvoice_accepted_handler,
};
mod nwc; mod nwc;
mod nwc_balance; mod nwc_balance;
mod nwc_hold; mod nwc_hold;
@ -32,6 +43,9 @@ mod tasks;
mod util; mod util;
pub const STARTUP_DELAY: u64 = 1; pub const STARTUP_DELAY: u64 = 1;
pub mod hold {
tonic::include_proto!("hold");
}
const OPT_RELAYS: StringArrayConfigOption = ConfigOption::new_str_arr_no_default( const OPT_RELAYS: StringArrayConfigOption = ConfigOption::new_str_arr_no_default(
"nip47-relays", "nip47-relays",
@ -56,10 +70,17 @@ pub const WALLET_PAY_METHODS: [nip47::Method; 4] = [
nip47::Method::PayKeysend, nip47::Method::PayKeysend,
nip47::Method::MultiPayKeysend, nip47::Method::MultiPayKeysend,
]; ];
pub const WALLET_HOLD_METHODS: [nip47::Method; 3] = [
nip47::Method::MakeHoldInvoice,
nip47::Method::CancelHoldInvoice,
nip47::Method::SettleHoldInvoice,
];
pub const WALLET_NOTIFICATIONS: [nip47::NotificationType; 2] = [ pub const WALLET_NOTIFICATIONS: [nip47::NotificationType; 2] = [
nip47::NotificationType::PaymentReceived, nip47::NotificationType::PaymentReceived,
nip47::NotificationType::PaymentSent, nip47::NotificationType::PaymentSent,
]; ];
pub const WALLET_HOLD_NOTIFICATIONS: [nip47::NotificationType; 1] =
[nip47::NotificationType::HoldInvoiceAccepted];
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), anyhow::Error> { async fn main() -> Result<(), anyhow::Error> {
@ -109,6 +130,16 @@ async fn main() -> Result<(), anyhow::Error> {
}; };
let plugin = confplugin.start(state).await?; let plugin = confplugin.start(state).await?;
match check_hold_support(plugin.clone()).await {
Ok(()) => {
log::info!("Hold support activated, loading pending invoices...");
if let Err(e) = load_pending_hold_invoices(plugin.clone()).await {
log::error!("Error loading pending hold invoices: {e}");
}
}
Err(e) => log::info!("Hold support not activated: {e}"),
}
{ {
let mut rpc = plugin.state().rpc_lock.lock().await; let mut rpc = plugin.state().rpc_lock.lock().await;
@ -167,3 +198,94 @@ async fn load_nwcs(plugin: Plugin<PluginState>, rpc: &mut ClnRpc) -> Result<(),
} }
Ok(()) Ok(())
} }
async fn load_pending_hold_invoices(plugin: Plugin<PluginState>) -> Result<(), anyhow::Error> {
let mut hold_client = plugin.state().hold_client.lock().clone().unwrap();
let invoices_request = ListRequest { constraint: None };
let invoices = hold_client
.list(invoices_request)
.await?
.into_inner()
.invoices;
for invoice in invoices {
if invoice.state() == InvoiceState::Accepted || invoice.state() == InvoiceState::Unpaid {
log::debug!(
"Starting holdinvoice accepted handler for {}",
hex::encode(&invoice.payment_hash)
);
tokio::spawn(holdinvoice_accepted_handler(
plugin.clone(),
invoice.payment_hash,
));
}
}
Ok(())
}
async fn check_hold_support(plugin: Plugin<PluginState>) -> Result<(), anyhow::Error> {
let mut rpc = plugin.state().rpc_lock.lock().await;
let hold_grpc_host_response: serde_json::Value = rpc
.call_raw("listconfigs", &json!({"config": "hold-grpc-host"}))
.await?;
let Some(hold_grpc_host_configs) = hold_grpc_host_response.get("configs") else {
return Err(anyhow!("Unsopprted listconfigs response!"));
};
let Some(hold_grpc_host_config) = hold_grpc_host_configs.get("hold-grpc-host") else {
return Err(anyhow!("hold-grpc-host config not found"));
};
let Some(hold_grpc_host_value) = hold_grpc_host_config.get("value_str") else {
return Err(anyhow!("hold-grpc-host config not a string"));
};
let Some(hold_grpc_host) = hold_grpc_host_value.as_str() else {
return Err(anyhow!("hold-grpc-host config not convertable to string"));
};
let hold_grpc_port_response: serde_json::Value = rpc
.call_raw("listconfigs", &json!({"config": "hold-grpc-port"}))
.await?;
let Some(hold_grpc_port_configs) = hold_grpc_port_response.get("configs") else {
return Err(anyhow!("Unsopprted listconfigs response!"));
};
let Some(hold_grpc_port_config) = hold_grpc_port_configs.get("hold-grpc-port") else {
return Err(anyhow!("hold-grpc-port config not found"));
};
let Some(hold_grpc_port_value) = hold_grpc_port_config.get("value_int") else {
return Err(anyhow!("hold-grpc-port config not a number"));
};
let hold_grpc_port = if let Some(hgh) = hold_grpc_port_value.as_u64() {
u16::try_from(hgh)?
} else {
return Err(anyhow!("hold-grpc-port config not convertable to integer"));
};
let cert_dir = PathBuf::from_str(&plugin.configuration().lightning_dir)?.join("hold");
log::debug!(
"Searching {} for hold plugin certs",
cert_dir.to_str().unwrap()
);
let ca_cert = tokio::fs::read(cert_dir.join("ca.pem")).await?;
let client_cert = tokio::fs::read(cert_dir.join("client.pem")).await?;
let client_key = tokio::fs::read(cert_dir.join("client-key.pem")).await?;
let identity = Identity::from_pem(client_cert, client_key);
let ca = Certificate::from_pem(ca_cert);
let tls_config = ClientTlsConfig::new()
.ca_certificate(ca)
.identity(identity)
.domain_name("hold");
let hold_channel = Endpoint::from_shared(format!("https://{hold_grpc_host}:{hold_grpc_port}"))?
.tls_config(tls_config)?
.keep_alive_while_idle(true)
.connect_lazy();
*plugin.state().hold_client.lock() = Some(HoldClient::new(hold_channel));
Ok(())
}

View file

@ -55,29 +55,29 @@ pub async fn run_nwc(
); );
let client_pubkey = Keys::new(nwc_store.uri.secret.clone()).public_key(); let client_pubkey = Keys::new(nwc_store.uri.secret.clone()).public_key();
let client = Client::new(wallet_keys.clone()); let nostr_client = Client::new(wallet_keys.clone());
log::debug!("relay_count:{}", nwc_store.uri.relays.len()); log::debug!("relay_count:{}", nwc_store.uri.relays.len());
for relay in &nwc_store.uri.relays { for relay in &nwc_store.uri.relays {
log::debug!("Adding relay: {relay}"); log::debug!("Adding relay: {relay}");
client.add_relay(relay).await?; nostr_client.add_relay(relay).await?;
} }
if nwc_store.interval_config.is_some() { if nwc_store.interval_config.is_some() {
start_nwc_budget_job(&plugin, label.clone()); start_nwc_budget_job(&plugin, label.clone());
} }
let client_clone = client.clone(); let nostr_client_clone = nostr_client.clone();
let plugin_clone = plugin.clone(); let plugin_clone = plugin.clone();
let label_clone = label.clone(); let label_clone = label.clone();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
client_clone.connect().await; nostr_client_clone.connect().await;
client_clone nostr_client_clone
.wait_for_connection(Duration::from_secs(30)) .wait_for_connection(Duration::from_secs(30))
.await; .await;
let relays = client_clone.relays().await; let relays = nostr_client_clone.relays().await;
if relays.is_empty() { if relays.is_empty() {
log::info!("No more relays left, we probably shut down. Exiting..."); log::info!("No more relays left, we probably shut down. Exiting...");
break; break;
@ -98,14 +98,14 @@ pub async fn run_nwc(
if let Err(e) = send_nwc_info_event( if let Err(e) = send_nwc_info_event(
plugin_clone.clone(), plugin_clone.clone(),
client_clone.clone(), nostr_client_clone.clone(),
method_capabilities.clone(), method_capabilities.clone(),
wallet_keys.clone(), wallet_keys.clone(),
) )
.await .await
{ {
log::warn!("{e}"); log::warn!("{e}");
client_clone.disconnect().await; nostr_client_clone.disconnect().await;
time::sleep(Duration::from_secs(5)).await; time::sleep(Duration::from_secs(5)).await;
continue; continue;
} }
@ -115,15 +115,15 @@ pub async fn run_nwc(
.author(client_pubkey) .author(client_pubkey)
.since(Timestamp::now() - STARTUP_DELAY - 1); .since(Timestamp::now() - STARTUP_DELAY - 1);
if let Err(e) = client_clone.subscribe(filter, None).await { if let Err(e) = nostr_client_clone.subscribe(filter, None).await {
log::warn!("Could not subscribe to nwc events! {e}"); log::warn!("Could not subscribe to nwc events! {e}");
client_clone.disconnect().await; nostr_client_clone.disconnect().await;
time::sleep(Duration::from_secs(5)).await; time::sleep(Duration::from_secs(5)).await;
continue; continue;
} }
let client_clone_handler = client_clone.clone(); let client_clone_handler = nostr_client_clone.clone();
match client_clone match nostr_client_clone
.handle_notifications(|notification| { .handle_notifications(|notification| {
let client_clone_handler = client_clone_handler.clone(); let client_clone_handler = client_clone_handler.clone();
let plugin_clone = plugin_clone.clone(); let plugin_clone = plugin_clone.clone();
@ -152,7 +152,7 @@ pub async fn run_nwc(
let mut locked_handles = plugin.state().handles.lock().await; let mut locked_handles = plugin.state().handles.lock().await;
locked_handles.insert( locked_handles.insert(
label.clone(), label.clone(),
(client, Keys::new(nwc_store.uri.secret).public_key()), (nostr_client, Keys::new(nwc_store.uri.secret).public_key()),
); );
Ok(()) Ok(())
} }
@ -227,7 +227,7 @@ pub fn stop_nwc_budget_job(plugin: &Plugin<PluginState>, label: &String) {
async fn nwc_request_handler( async fn nwc_request_handler(
notification: RelayPoolNotification, notification: RelayPoolNotification,
client: client::Client, nostr_client: client::Client,
plugin: Plugin<PluginState>, plugin: Plugin<PluginState>,
label: String, label: String,
wallet_keys: Keys, wallet_keys: Keys,
@ -281,13 +281,13 @@ async fn nwc_request_handler(
nip47::RequestParams::GetBalance => get_balance_response(plugin.clone(), &label).await, nip47::RequestParams::GetBalance => get_balance_response(plugin.clone(), &label).await,
nip47::RequestParams::GetInfo => get_info_response(plugin.clone(), &label).await, nip47::RequestParams::GetInfo => get_info_response(plugin.clone(), &label).await,
nip47::RequestParams::MakeHoldInvoice(make_hold_invoice_request) => { nip47::RequestParams::MakeHoldInvoice(make_hold_invoice_request) => {
make_hold_invoice_response(plugin.clone(), make_hold_invoice_request, &label).await make_hold_invoice_response(plugin.clone(), make_hold_invoice_request).await
} }
nip47::RequestParams::CancelHoldInvoice(cancel_hold_invoice_request) => { nip47::RequestParams::CancelHoldInvoice(cancel_hold_invoice_request) => {
cancel_hold_invoice_response(plugin.clone(), cancel_hold_invoice_request, &label).await cancel_hold_invoice_response(plugin.clone(), cancel_hold_invoice_request).await
} }
nip47::RequestParams::SettleHoldInvoice(settle_hold_invoice_request) => { nip47::RequestParams::SettleHoldInvoice(settle_hold_invoice_request) => {
settle_hold_invoice_response(plugin.clone(), settle_hold_invoice_request, &label).await settle_hold_invoice_response(plugin.clone(), settle_hold_invoice_request).await
} }
}; };
for (response, id) in responses { for (response, id) in responses {
@ -309,7 +309,7 @@ async fn nwc_request_handler(
} }
}; };
let send_result = match client.send_event(&response_event).await { let send_result = match nostr_client.send_event(&response_event).await {
Ok(o) => o, Ok(o) => o,
Err(e) => { Err(e) => {
log::warn!("Error sending response event! {e}"); log::warn!("Error sending response event! {e}");

View file

@ -1,58 +1,242 @@
use cln_plugin::Plugin; use std::str::FromStr;
use nostr_sdk::nips::nip47;
use crate::structs::PluginState; use cln_plugin::Plugin;
use cln_rpc::primitives::Sha256;
use nostr_sdk::{nips::nip47, Timestamp};
use crate::{
hold::{invoice_request::Description, CancelRequest, InvoiceRequest, SettleRequest},
nwc_notifications::holdinvoice_accepted_handler,
structs::PluginState,
};
pub async fn make_hold_invoice_response( pub async fn make_hold_invoice_response(
_plugin: Plugin<PluginState>, plugin: Plugin<PluginState>,
_params: nip47::MakeHoldInvoiceRequest, params: nip47::MakeHoldInvoiceRequest,
_label: &str,
) -> Vec<(nip47::Response, Option<String>)> { ) -> Vec<(nip47::Response, Option<String>)> {
vec![( vec![match make_hold_invoice(plugin, params).await {
nip47::Response { Ok(o) => (
result_type: nip47::Method::MakeHoldInvoice, nip47::Response {
error: Some(nip47::NIP47Error { result_type: nip47::Method::MakeHoldInvoice,
code: nip47::ErrorCode::NotImplemented, error: None,
message: "Not implemented".to_owned(), result: Some(nip47::ResponseResult::MakeHoldInvoice(o)),
}), },
result: None, None,
}, ),
None, Err(e) => (
)] nip47::Response {
result_type: nip47::Method::MakeHoldInvoice,
error: Some(e),
result: None,
},
None,
),
}]
}
async fn make_hold_invoice(
plugin: Plugin<PluginState>,
params: nip47::MakeHoldInvoiceRequest,
) -> Result<nip47::MakeHoldInvoiceResponse, nip47::NIP47Error> {
let Some(mut hold_client) = plugin.state().hold_client.lock().clone() else {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::NotImplemented,
message: "No hold plugin found".to_owned(),
});
};
let description: Option<Description> = if let Some(d_hash) = &params.description_hash {
if let Some(description) = &params.description {
let my_description_hash = Sha256::const_hash(description.as_bytes());
let description_hash = Sha256::from_str(d_hash).map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
if my_description_hash != description_hash {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "description_hash not matching description".to_owned(),
});
}
}
let desc_hash_bytes = match hex::decode(d_hash) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Could not convert description hash to bytes".to_owned(),
})
}
};
Some(Description::Hash(desc_hash_bytes))
} else {
params
.description
.as_ref()
.map(|desc| Description::Memo(desc.clone()))
};
let payment_hash = match hex::decode(&params.payment_hash) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash".to_owned(),
})
}
};
let expiry = params.expiry.unwrap_or(60 * 60);
let holdinvoice_request = InvoiceRequest {
payment_hash: payment_hash.clone(),
amount_msat: params.amount,
expiry: Some(expiry),
min_final_cltv_expiry: params.cltv_expiry_delta.map(u64::from),
routing_hints: Vec::new(),
description,
};
let holdinvoice = hold_client
.invoice(holdinvoice_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Error creating hold invoice: {e}"),
})?
.into_inner();
let response = nip47::MakeHoldInvoiceResponse {
invoice: Some(holdinvoice.bolt11),
transaction_type: nip47::TransactionType::Incoming,
description: params.description,
description_hash: params.description_hash,
amount: params.amount,
created_at: Timestamp::now(),
expires_at: Timestamp::now() + expiry,
metadata: None,
payment_hash: params.payment_hash,
};
tokio::spawn(holdinvoice_accepted_handler(plugin, payment_hash));
Ok(response)
} }
pub async fn cancel_hold_invoice_response( pub async fn cancel_hold_invoice_response(
_plugin: Plugin<PluginState>, plugin: Plugin<PluginState>,
_params: nip47::CancelHoldInvoiceRequest, params: nip47::CancelHoldInvoiceRequest,
_label: &str,
) -> Vec<(nip47::Response, Option<String>)> { ) -> Vec<(nip47::Response, Option<String>)> {
vec![( vec![match cancel_hold_invoice(plugin, params).await {
nip47::Response { Ok(o) => (
result_type: nip47::Method::MakeHoldInvoice, nip47::Response {
error: Some(nip47::NIP47Error { result_type: nip47::Method::CancelHoldInvoice,
code: nip47::ErrorCode::NotImplemented, error: None,
message: "Not implemented".to_owned(), result: Some(nip47::ResponseResult::CancelHoldInvoice(o)),
}), },
result: None, None,
}, ),
None, Err(e) => (
)] nip47::Response {
result_type: nip47::Method::CancelHoldInvoice,
error: Some(e),
result: None,
},
None,
),
}]
}
async fn cancel_hold_invoice(
plugin: Plugin<PluginState>,
params: nip47::CancelHoldInvoiceRequest,
) -> Result<nip47::CancelHoldInvoiceResponse, nip47::NIP47Error> {
let Some(mut hold_client) = plugin.state().hold_client.lock().clone() else {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::NotImplemented,
message: "No hold plugin found".to_owned(),
});
};
let payment_hash = match hex::decode(&params.payment_hash) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash".to_owned(),
})
}
};
let hold_cancel_request = CancelRequest { payment_hash };
let hold_cancel_response = hold_client.cancel(hold_cancel_request).await;
match hold_cancel_response {
Ok(_o) => Ok(nip47::CancelHoldInvoiceResponse {}),
Err(e) => Err(nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
}),
}
} }
pub async fn settle_hold_invoice_response( pub async fn settle_hold_invoice_response(
_plugin: Plugin<PluginState>, plugin: Plugin<PluginState>,
_params: nip47::SettleHoldInvoiceRequest, params: nip47::SettleHoldInvoiceRequest,
_label: &str,
) -> Vec<(nip47::Response, Option<String>)> { ) -> Vec<(nip47::Response, Option<String>)> {
vec![( vec![match settle_hold_invoice(plugin, params).await {
nip47::Response { Ok(o) => (
result_type: nip47::Method::MakeHoldInvoice, nip47::Response {
error: Some(nip47::NIP47Error { result_type: nip47::Method::SettleHoldInvoice,
code: nip47::ErrorCode::NotImplemented, error: None,
message: "Not implemented".to_owned(), result: Some(nip47::ResponseResult::SettleHoldInvoice(o)),
}), },
result: None, None,
}, ),
None, Err(e) => (
)] nip47::Response {
result_type: nip47::Method::SettleHoldInvoice,
error: Some(e),
result: None,
},
None,
),
}]
}
async fn settle_hold_invoice(
plugin: Plugin<PluginState>,
params: nip47::SettleHoldInvoiceRequest,
) -> Result<nip47::SettleHoldInvoiceResponse, nip47::NIP47Error> {
let Some(mut hold_client) = plugin.state().hold_client.lock().clone() else {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::NotImplemented,
message: "No hold plugin found".to_owned(),
});
};
let preimage = match hex::decode(&params.preimage) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid preimage".to_owned(),
})
}
};
let hold_settle_request = SettleRequest {
payment_preimage: preimage,
};
let hold_settle_response = hold_client.settle(hold_settle_request).await;
match hold_settle_response {
Ok(_o) => Ok(nip47::SettleHoldInvoiceResponse {}),
Err(e) => Err(nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
}),
}
} }

View file

@ -5,6 +5,7 @@ use cln_rpc::{
model::{ model::{
requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest}, requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest},
responses::{ responses::{
DecodeType,
ListinvoicesInvoices, ListinvoicesInvoices,
ListinvoicesInvoicesStatus, ListinvoicesInvoicesStatus,
ListpaysPays, ListpaysPays,
@ -16,7 +17,10 @@ use cln_rpc::{
}; };
use nostr_sdk::{nips::nip47, Timestamp}; use nostr_sdk::{nips::nip47, Timestamp};
use crate::structs::{PluginState, NOT_INV_ERR}; use crate::{
hold::{self, list_request::Constraint, ListRequest},
structs::{PluginState, NOT_INV_ERR},
};
pub async fn lookup_invoice_response( pub async fn lookup_invoice_response(
plugin: Plugin<PluginState>, plugin: Plugin<PluginState>,
@ -58,7 +62,7 @@ async fn lookup_invoice(
let invoice = if params.payment_hash.is_some() && params.invoice.is_some() { let invoice = if params.payment_hash.is_some() && params.invoice.is_some() {
None None
} else { } else {
params.invoice params.invoice.clone()
}; };
let invoices = rpc let invoices = rpc
@ -81,47 +85,234 @@ async fn lookup_invoice(
if invoices.len() == 1 { if invoices.len() == 1 {
let invoice_response = invoices.into_iter().next().unwrap(); let invoice_response = invoices.into_iter().next().unwrap();
make_lookup_response_from_listinvoices(&mut rpc, invoice_response).await return make_lookup_response_from_listinvoices(&mut rpc, invoice_response).await;
}
let payment_hash_hash = if let Some(hash) = &params.payment_hash {
if let Ok(res) = Sha256::from_str(hash) {
Some(res)
} else {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: "Could not convert payment hash".to_owned(),
});
}
} else { } else {
let payment_hash_hash = if let Some(hash) = params.payment_hash { None
if let Ok(res) = Sha256::from_str(&hash) { };
Some(res)
} else { let pays = rpc
.call_typed(&ListpaysRequest {
bolt11: invoice,
index: None,
limit: None,
payment_hash: payment_hash_hash,
start: None,
status: None,
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?
.pays;
if pays.len() == 1 {
let list_pay = pays.into_iter().next().unwrap();
return make_lookup_response_from_listpays(&mut rpc, list_pay).await;
}
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
if holdinvoice_support {
let mut hold_client = plugin.state().hold_client.lock().clone().unwrap();
let (hold_invoice, invoice_decoded) = if let Some(ph) = &params.payment_hash {
let payment_hash_hash = match hex::decode(ph) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash".to_owned(),
})
}
};
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash_hash)),
};
let hold_lookup = hold_client
.list(list_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoice: {e}"),
})?
.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(nip47::NIP47Error { return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Internal, code: nip47::ErrorCode::Other,
message: "Could not convert payment hash".to_owned(), message: "Transaction not found".to_owned(),
}); });
} }
let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap();
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
(hold_invoice, invoice_decoded)
} else {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: params.invoice.unwrap(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
let ph = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_payment_hash.unwrap(),
DecodeType::BOLT11_INVOICE => invoice_decoded.payment_hash.unwrap().to_string(),
_ => todo!(),
};
let payment_hash_hash = match hex::decode(&ph) {
Ok(p) => p,
Err(_e) => {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Invalid payment hash in invoice".to_owned(),
})
}
};
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash_hash)),
};
let hold_lookup = hold_client
.list(list_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoice: {e}"),
})?
.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: "Transaction not found".to_owned(),
});
}
let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap();
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
(hold_invoice, invoice_decoded)
};
let not_invoice_err = Err(nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: NOT_INV_ERR.to_owned(),
});
let description = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description,
DecodeType::BOLT11_INVOICE => invoice_decoded.description,
_ => return not_invoice_err,
};
let description_hash = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => None,
DecodeType::BOLT11_INVOICE => invoice_decoded.description_hash.map(|h| h.to_string()),
_ => return not_invoice_err,
};
let created_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
DecodeType::BOLT11_INVOICE => Timestamp::from_secs(invoice_decoded.created_at.unwrap()),
_ => return not_invoice_err,
};
let amount = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_amount_msat.unwrap().msat(),
DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => return not_invoice_err,
};
let expires_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded
.invoice_relative_expiry
.map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))),
DecodeType::BOLT11_INVOICE => invoice_decoded
.expiry
.map(|e_at| created_at + Timestamp::from_secs(e_at)),
_ => return not_invoice_err,
};
let state = match hold_invoice.state() {
hold::InvoiceState::Unpaid => nip47::TransactionState::Pending,
hold::InvoiceState::Accepted => nip47::TransactionState::Pending, // TODO ACCEPTED STATE
hold::InvoiceState::Paid => nip47::TransactionState::Settled,
hold::InvoiceState::Cancelled => nip47::TransactionState::Expired,
};
let settled_at = if hold_invoice.settled_at() != 0 {
Some(Timestamp::from_secs(hold_invoice.settled_at()))
} else { } else {
None None
}; };
let pays = rpc let preimage = if hold_invoice.state() == hold::InvoiceState::Paid {
.call_typed(&ListpaysRequest { Some(hex::encode(hold_invoice.preimage()))
bolt11: invoice, } else {
index: None, None
limit: None, };
payment_hash: payment_hash_hash,
start: None,
status: None,
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?
.pays;
if pays.len() != 1 { return Ok(nip47::LookupInvoiceResponse {
return Err(nip47::NIP47Error { transaction_type: Some(nip47::TransactionType::Incoming),
code: nip47::ErrorCode::NotFound, invoice: Some(hold_invoice.invoice.clone()),
message: "Transaction not found".to_owned(), description,
}); description_hash,
} preimage,
let list_pay = pays.into_iter().next().unwrap(); payment_hash: hex::encode(hold_invoice.payment_hash),
amount,
make_lookup_response_from_listpays(&mut rpc, list_pay).await fees_paid: 0,
created_at,
expires_at,
settled_at,
metadata: None,
state: Some(state),
});
} }
Err(nip47::NIP47Error {
code: nip47::ErrorCode::NotFound,
message: "Transaction not found".to_owned(),
})
} }
pub async fn list_transactions_response( pub async fn list_transactions_response(
@ -193,6 +384,118 @@ async fn list_transactions(
Err(_e) => (), Err(_e) => (),
} }
} }
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
if holdinvoice_support {
let mut hold_client = plugin.state().hold_client.lock().clone().unwrap();
let lookup_request = ListRequest { constraint: None };
let hold_lookup = hold_client
.list(lookup_request)
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: format!("Could not fetch hold invoices: {e}"),
})?
.into_inner();
for hold_invoice in hold_lookup.invoices {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await
.map_err(|e| nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
})?;
let description = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description,
DecodeType::BOLT11_INVOICE => invoice_decoded.description,
_ => continue,
};
let description_hash = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => None,
DecodeType::BOLT11_INVOICE => {
invoice_decoded.description_hash.map(|h| h.to_string())
}
_ => continue,
};
let created_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
DecodeType::BOLT11_INVOICE => {
Timestamp::from_secs(invoice_decoded.created_at.unwrap())
}
_ => continue,
};
let amount = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => {
invoice_decoded.invoice_amount_msat.unwrap().msat()
}
DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => continue,
};
let expires_at = match invoice_decoded.item_type {
DecodeType::BOLT12_INVOICE => invoice_decoded
.invoice_relative_expiry
.map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))),
DecodeType::BOLT11_INVOICE => invoice_decoded
.expiry
.map(|e_at| created_at + Timestamp::from_secs(e_at)),
_ => continue,
};
let state = match hold_invoice.state() {
hold::InvoiceState::Unpaid => nip47::TransactionState::Pending,
hold::InvoiceState::Accepted => nip47::TransactionState::Pending, // TODO: ACCEPTED STATE
hold::InvoiceState::Paid => nip47::TransactionState::Settled,
hold::InvoiceState::Cancelled => nip47::TransactionState::Expired,
};
let settled_at = if hold_invoice.settled_at() != 0 {
Some(Timestamp::from_secs(hold_invoice.settled_at()))
} else {
None
};
let preimage = if hold_invoice.state() == hold::InvoiceState::Paid {
Some(hex::encode(hold_invoice.preimage()))
} else {
None
};
transactions.push(nip47::LookupInvoiceResponse {
transaction_type: Some(nip47::TransactionType::Incoming),
invoice: Some(hold_invoice.invoice.clone()),
description,
description_hash,
preimage,
payment_hash: hex::encode(hold_invoice.payment_hash),
amount,
fees_paid: 0,
created_at,
expires_at,
settled_at,
metadata: None,
state: Some(state),
});
}
}
} }
if query_payments { if query_payments {

View file

@ -4,7 +4,7 @@ use anyhow::anyhow;
use cln_plugin::Plugin; use cln_plugin::Plugin;
use cln_rpc::{ use cln_rpc::{
model::{ model::{
requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest}, requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest, ListpeerchannelsRequest},
responses::{ responses::{
DecodeResponse, DecodeResponse,
ListinvoicesInvoices, ListinvoicesInvoices,
@ -24,6 +24,7 @@ use nostr_sdk::{
}; };
use crate::{ use crate::{
hold::{list_request::Constraint, InvoiceState, ListRequest, TrackRequest},
structs::{PluginState, NOT_INV_ERR}, structs::{PluginState, NOT_INV_ERR},
OPT_NOTIFICATIONS, OPT_NOTIFICATIONS,
}; };
@ -380,3 +381,129 @@ async fn send_notification(
Ok(()) Ok(())
} }
pub async fn holdinvoice_accepted_handler(
plugin: Plugin<PluginState>,
payment_hash: Vec<u8>,
) -> Result<(), anyhow::Error> {
let mut hold_client = plugin.state().hold_client.lock().clone().unwrap();
let track_request = TrackRequest {
payment_hash: payment_hash.clone(),
};
let mut track_stream = hold_client.track(track_request).await?.into_inner();
while let Some(response) = track_stream.message().await? {
log::debug!("Invoice status: {}", response.state().as_str_name());
if response.state() == InvoiceState::Accepted {
break;
}
}
let list_request = ListRequest {
constraint: Some(Constraint::PaymentHash(payment_hash.clone())),
};
let hold_lookup = hold_client.list(list_request).await?.into_inner();
if hold_lookup.invoices.len() != 1 {
return Err(anyhow!("hold plugin did not return exactly one invoice"));
}
let hold_invoice = hold_lookup.invoices.first().unwrap();
let mut rpc = plugin.state().rpc_lock.lock().await;
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: hold_invoice.invoice.clone(),
})
.await?;
let amount = match invoice_decoded.item_type {
cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => {
invoice_decoded.invoice_amount_msat.unwrap().msat()
}
cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => {
if let Some(amt) = invoice_decoded.amount_msat {
amt.msat()
} else {
// amount: `any` but have to put a value...
0
}
}
_ => return Err(anyhow!("hold plugin did not return an invoice string")),
};
let created_at = match invoice_decoded.item_type {
cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => {
Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap())
}
cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => {
Timestamp::from_secs(invoice_decoded.created_at.unwrap())
}
_ => return Err(anyhow!("hold plugin did not return an invoice string")),
};
let expires_at = match invoice_decoded.item_type {
cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => {
created_at
+ Timestamp::from_secs(u64::from(invoice_decoded.invoice_relative_expiry.unwrap()))
}
cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => {
created_at + Timestamp::from_secs(invoice_decoded.expiry.unwrap())
}
_ => return Err(anyhow!("hold plugin did not return an invoice string")),
};
let list_peer_channels = rpc
.call_typed(&ListpeerchannelsRequest {
id: None,
short_channel_id: None,
})
.await?
.channels;
let payment_hash_hash = Sha256::from_str(&hex::encode(payment_hash))?;
let mut lowest_htlc_expiry = 0;
for peer in list_peer_channels {
if let Some(htlcs) = peer.htlcs {
for htlc in htlcs {
if htlc.payment_hash != payment_hash_hash {
continue;
}
if htlc.expiry < lowest_htlc_expiry {
lowest_htlc_expiry = htlc.expiry;
}
}
}
}
let clients = plugin.state().handles.lock().await;
let content = nip47::Notification {
notification_type: nip47::NotificationType::HoldInvoiceAccepted,
notification: nip47::NotificationResult::HoldInvoiceAccepted(
nip47::HoldInvoiceAcceptedNotification {
transaction_type: nip47::TransactionType::Incoming,
invoice: hold_invoice.invoice.clone(),
description: None,
description_hash: None,
payment_hash: hex::encode(&hold_invoice.payment_hash),
amount,
created_at,
expires_at,
settle_deadline: lowest_htlc_expiry,
metadata: None,
},
),
};
let notification = serde_json::to_string(&content).unwrap();
for (client, client_pubkey) in clients.values() {
send_notification(&notification, client, client_pubkey).await?;
}
Ok(())
}

View file

@ -5,6 +5,9 @@ use nostr_sdk::{client, nips::nip47, nostr};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tonic::transport::Channel;
use crate::hold::hold_client::HoldClient;
pub const NOT_INV_ERR: &str = "Not an invoice or invalid invoice"; pub const NOT_INV_ERR: &str = "Not an invoice or invalid invoice";
@ -14,6 +17,7 @@ pub struct PluginState {
pub handles: Arc<tokio::sync::Mutex<HashMap<String, (client::Client, nostr::PublicKey)>>>, pub handles: Arc<tokio::sync::Mutex<HashMap<String, (client::Client, nostr::PublicKey)>>>,
pub rpc_lock: Arc<tokio::sync::Mutex<ClnRpc>>, pub rpc_lock: Arc<tokio::sync::Mutex<ClnRpc>>,
pub budget_jobs: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>, pub budget_jobs: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
pub hold_client: Arc<Mutex<Option<HoldClient<Channel>>>>,
} }
impl PluginState { impl PluginState {
pub async fn new(path: PathBuf) -> Result<PluginState, anyhow::Error> { pub async fn new(path: PathBuf) -> Result<PluginState, anyhow::Error> {
@ -22,6 +26,7 @@ impl PluginState {
handles: Arc::new(tokio::sync::Mutex::new(HashMap::new())), handles: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
rpc_lock: Arc::new(tokio::sync::Mutex::new(ClnRpc::new(path).await?)), rpc_lock: Arc::new(tokio::sync::Mutex::new(ClnRpc::new(path).await?)),
budget_jobs: Arc::new(Mutex::new(HashMap::new())), budget_jobs: Arc::new(Mutex::new(HashMap::new())),
hold_client: Arc::new(Mutex::new(None)),
}) })
} }
} }

View file

@ -10,6 +10,8 @@ use crate::{
structs::{NwcStore, PluginState}, structs::{NwcStore, PluginState},
OPT_NOTIFICATIONS, OPT_NOTIFICATIONS,
PLUGIN_NAME, PLUGIN_NAME,
WALLET_HOLD_METHODS,
WALLET_HOLD_NOTIFICATIONS,
WALLET_NOTIFICATIONS, WALLET_NOTIFICATIONS,
WALLET_PAY_METHODS, WALLET_PAY_METHODS,
WALLET_READ_METHODS, WALLET_READ_METHODS,
@ -124,11 +126,22 @@ pub fn at_or_above_version(my_version: &str, min_version: &str) -> Result<bool,
} }
pub fn build_capabilities(is_read_only: bool, plugin: &Plugin<PluginState>) -> (String, String) { pub fn build_capabilities(is_read_only: bool, plugin: &Plugin<PluginState>) -> (String, String) {
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
let mut methods = WALLET_READ_METHODS.map(|m| m.to_string()).join(" "); let mut methods = WALLET_READ_METHODS.map(|m| m.to_string()).join(" ");
if !is_read_only { if !is_read_only {
methods.push(' '); methods.push(' ');
methods.push_str(WALLET_PAY_METHODS.map(|m| m.to_string()).join(" ").as_str()); methods.push_str(WALLET_PAY_METHODS.map(|m| m.to_string()).join(" ").as_str());
} }
if holdinvoice_support {
methods.push(' ');
methods.push_str(
WALLET_HOLD_METHODS
.map(|m| m.to_string())
.join(" ")
.as_str(),
);
}
let mut notifications = String::new(); let mut notifications = String::new();
if plugin.option(&OPT_NOTIFICATIONS).unwrap() { if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
@ -138,23 +151,41 @@ pub fn build_capabilities(is_read_only: bool, plugin: &Plugin<PluginState>) -> (
.join(" ") .join(" ")
.as_str(), .as_str(),
); );
if holdinvoice_support {
notifications.push(' ');
notifications.push_str(
WALLET_HOLD_NOTIFICATIONS
.map(|m| m.to_string())
.join(" ")
.as_str(),
);
}
} }
(methods, notifications) (methods, notifications)
} }
pub fn build_methods_vec(is_read_only: bool, _plugin: &Plugin<PluginState>) -> Vec<nip47::Method> { pub fn build_methods_vec(is_read_only: bool, plugin: &Plugin<PluginState>) -> Vec<nip47::Method> {
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
let mut methods = WALLET_READ_METHODS.to_vec(); let mut methods = WALLET_READ_METHODS.to_vec();
if !is_read_only { if !is_read_only {
methods.extend_from_slice(&WALLET_PAY_METHODS); methods.extend_from_slice(&WALLET_PAY_METHODS);
} }
if holdinvoice_support {
methods.extend_from_slice(&WALLET_HOLD_METHODS);
}
methods methods
} }
pub fn build_notifications_vec(plugin: &Plugin<PluginState>) -> Vec<String> { pub fn build_notifications_vec(plugin: &Plugin<PluginState>) -> Vec<String> {
let holdinvoice_support = plugin.state().hold_client.lock().is_some();
let mut notifications = Vec::new(); let mut notifications = Vec::new();
if plugin.option(&OPT_NOTIFICATIONS).unwrap() { if plugin.option(&OPT_NOTIFICATIONS).unwrap() {
notifications.extend_from_slice(&WALLET_NOTIFICATIONS.map(|m| m.to_string())); notifications.extend_from_slice(&WALLET_NOTIFICATIONS.map(|m| m.to_string()));
if holdinvoice_support {
notifications.extend_from_slice(&WALLET_HOLD_NOTIFICATIONS.map(|m| m.to_string()));
}
} }
notifications notifications
} }

View file

@ -9,4 +9,4 @@ reject_future_seconds = 1800
[limits] [limits]
limit_scrapers = false limit_scrapers = false
[network] [network]
address = "0.0.0.0" address = "127.0.0.1"

View file

@ -81,3 +81,17 @@ if [ -d "$proto_path" ]; then
exit 1 exit 1
fi fi
fi fi
hold_url="https://github.com/BoltzExchange/hold/releases/download/v0.3.3/hold-linux-amd64.tar.gz"
if ! curl -L "$hold_url" -o "$script_dir/hold-linux-amd64.tar.gz"; then
echo "Error downloading the file from $hold_url" >&2
exit 1
fi
if ! tar -xzvf "$script_dir/hold-linux-amd64.tar.gz" -C "$script_dir"; then
echo "Error extracting the contents of hold-linux-amd64.tar.gz" >&2
exit 1
fi
mv "$script_dir/build/hold-linux-amd64" "$script_dir/hold"

View file

@ -4,13 +4,15 @@ import json
import logging import logging
import time import time
import asyncio import asyncio
import secrets
import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Awaitable, Callable, Union from typing import Any, Awaitable, Callable, Union
import pytest import pytest
from pyln.testing.fixtures import * # noqa: F403 from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import RpcError, wait_for, TIMEOUT from pyln.testing.utils import RpcError, wait_for, TIMEOUT
from util import generate_random_label, get_plugin # noqa: F401 from util import generate_random_label, get_plugin, get_hold # noqa: F401
from nostr_sdk import ( from nostr_sdk import (
Alphabet, Alphabet,
@ -75,7 +77,10 @@ async def fetch_event_responses(
) -> tuple[list[Event], Any]: ) -> tuple[list[Event], Any]:
events = [] events = []
response_filter = Filter().kind(Kind(event_kind)).pubkey(client_pubkey) response_filter = Filter().kind(Kind(event_kind)).pubkey(client_pubkey)
await client.subscribe(response_filter)
id = uuid.uuid4().hex
LOGGER.info(f"Subscribing with id {id} to {response_filter}")
await client.subscribe_with_id(id, response_filter)
handler = NotificationHandler(events, stop_after) handler = NotificationHandler(events, stop_after)
task = asyncio.create_task(client.handle_notifications(handler)) task = asyncio.create_task(client.handle_notifications(handler))
@ -1454,3 +1459,551 @@ async def test_budget_command(nostr_relay, node_factory, get_plugin): # noqa: F
info_event.tags().find(TagKind.UNKNOWN("notifications")).content() info_event.tags().find(TagKind.UNKNOWN("notifications")).content()
== "payment_received payment_sent" == "payment_received payment_sent"
) )
@pytest.mark.asyncio
async def test_hold_invoice(
node_factory,
executor,
get_plugin, # noqa: F811
get_hold, # noqa: F811
nostr_relay,
):
url = nostr_relay
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"may_reconnect": True,
},
{
"log-level": "debug",
"plugin": get_plugin,
"important-plugin": get_hold,
"nip47-relays": url,
"may_reconnect": True,
},
],
)
uri_res = l2.rpc.call("nip47-create", ["test1", 3010])
uri_str = uri_res["uri"]
client_pubkey = PublicKey.parse(uri_res["clientkey_public"])
LOGGER.info(uri_str)
uri = NostrWalletConnectUri.parse(uri_str)
nwc = Nwc(uri)
preimage = secrets.token_hex(32)
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
LOGGER.info(f"preimage: {preimage}")
LOGGER.info(f"payment_hash: {payment_hash}")
content = {
"method": "make_hold_invoice",
"params": {
"amount": 5000,
"payment_hash": payment_hash,
},
}
content = json.dumps(content)
signer = NostrSigner.keys(Keys(uri.secret()))
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
client = Client(signer)
relay_url = RelayUrl.parse(url)
await client.add_relay(relay_url)
await client.connect()
await fetch_info_event(client, uri)
(responses1, _res) = await fetch_event_responses(
client, client_pubkey, 23195, client.send_event(event), 1
)
error_events = []
success_events = []
for event in responses1:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if "result" in content and content["result"] is not None:
success_events.append(content)
if "error" in content and content["error"] is not None:
error_events.append(content)
assert len(success_events) == 1
assert len(error_events) == 0
assert success_events[0]["result_type"] == "make_hold_invoice"
assert success_events[0]["result"]["payment_hash"] == payment_hash
assert success_events[0]["result"]["type"] == "incoming"
assert success_events[0]["result"]["invoice"] is not None
invoice1 = success_events[0]["result"]["invoice"]
assert "description" not in success_events[0]["result"]
assert "description_hash" not in success_events[0]["result"]
assert success_events[0]["result"]["amount"] == 5000
invoice1_created_at = pytest.approx(int(time.time()), abs=1)
assert success_events[0]["result"]["created_at"] == invoice1_created_at
invoice1_expires_at = pytest.approx(int(time.time()) + 3600, abs=1)
assert success_events[0]["result"]["expires_at"] == invoice1_expires_at
assert "metadata" not in success_events[0]["result"]
lookup_hold = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=None,
)
)
assert lookup_hold.invoice == success_events[0]["result"]["invoice"]
assert lookup_hold.amount == 5000
assert lookup_hold.description is None
assert lookup_hold.created_at.as_secs() == success_events[0]["result"]["created_at"]
assert lookup_hold.description_hash is None
assert lookup_hold.expires_at.as_secs() == success_events[0]["result"]["expires_at"]
assert lookup_hold.fees_paid == 0
assert lookup_hold.metadata is None
assert lookup_hold.preimage is None
assert lookup_hold.payment_hash == payment_hash
assert lookup_hold.transaction_type.name == "INCOMING"
assert lookup_hold.state.name == "PENDING"
assert lookup_hold.settled_at is None
(responses2, _res) = await fetch_event_responses(
client,
client_pubkey,
23196,
lambda: executor.submit(
l1.rpc.call, "xpay", [success_events[0]["result"]["invoice"]]
),
1,
)
hold_events = []
for event in responses2:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if content["notification_type"] == "hold_invoice_accepted":
hold_events.append(content)
assert content["notification"]["payment_hash"] == payment_hash
lookup_hold = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=None,
invoice=invoice1,
)
)
assert lookup_hold.invoice == invoice1
assert lookup_hold.amount == 5000
assert lookup_hold.description is None
assert lookup_hold.created_at.as_secs() == invoice1_created_at
assert lookup_hold.description_hash is None
assert lookup_hold.expires_at.as_secs() == invoice1_expires_at
assert lookup_hold.fees_paid == 0
assert lookup_hold.metadata is None
assert lookup_hold.preimage is None
assert lookup_hold.payment_hash == payment_hash
assert lookup_hold.transaction_type.name == "INCOMING"
assert lookup_hold.state.name == "PENDING" # TODO ACCEPTED STATE
assert lookup_hold.settled_at is None
content = {
"method": "settle_hold_invoice",
"params": {
"preimage": preimage,
},
}
content = json.dumps(content)
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
(responses3, _res) = await fetch_event_responses(
client, client_pubkey, 23195, client.send_event(event), 1
)
error_events = []
success_events = []
for event in responses3:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if (
"result" in content
and content["result"] is not None
and content["result_type"] == "settle_hold_invoice"
):
success_events.append(content)
if "error" in content and content["error"] is not None:
error_events.append(content)
assert len(success_events) == 1
assert len(error_events) == 0
for content in success_events:
assert content["result_type"] == "settle_hold_invoice"
lookup_hold = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=None,
invoice=invoice1,
)
)
assert lookup_hold.invoice == invoice1
assert lookup_hold.amount == 5000
assert lookup_hold.description is None
assert lookup_hold.created_at.as_secs() == invoice1_created_at
assert lookup_hold.description_hash is None
assert lookup_hold.expires_at.as_secs() == invoice1_expires_at
assert lookup_hold.fees_paid == 0
assert lookup_hold.metadata is None
assert lookup_hold.preimage == preimage
assert lookup_hold.payment_hash == payment_hash
assert lookup_hold.transaction_type.name == "INCOMING"
assert lookup_hold.state.name == "SETTLED"
assert lookup_hold.settled_at.as_secs() == pytest.approx(
int(time.time()), abs=1
)
wait_for(
lambda: l1.rpc.call("listpays", {"payment_hash": payment_hash})["pays"][0][
"status"
]
== "complete"
)
preimage = secrets.token_hex(32)
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
content = {
"method": "make_hold_invoice",
"params": {
"amount": 5000,
"payment_hash": payment_hash,
"description": "cancel_hold",
"expiry": 1000,
"cltv_expiry_delta": 200,
},
}
content = json.dumps(content)
signer = NostrSigner.keys(Keys(uri.secret()))
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
(responses4, _res) = await fetch_event_responses(
client, client_pubkey, 23195, client.send_event(event), 1
)
error_events = []
success_events = []
for event in responses4:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if (
"result" in content
and content["result"] is not None
and content["result_type"] == "make_hold_invoice"
and content["result"]["payment_hash"] == payment_hash
):
success_events.append(content)
if "error" in content and content["error"] is not None:
error_events.append(content)
assert len(success_events) == 1
assert len(error_events) == 0
for content in success_events:
assert content["result_type"] == "make_hold_invoice"
assert content["result"]["payment_hash"] == payment_hash
invoice2 = content["result"]["invoice"]
invoice2_created_at = pytest.approx(int(time.time()), abs=1)
invoice2_expires_at = pytest.approx(int(time.time()) + 1000, abs=1)
(responses5, _res) = await fetch_event_responses(
client,
client_pubkey,
23196,
lambda: executor.submit(
l1.rpc.call, "xpay", [success_events[0]["result"]["invoice"]]
),
1,
)
hold_events = []
for event in responses5:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if (
content["notification_type"] == "hold_invoice_accepted"
and content["notification"]["payment_hash"] == payment_hash
):
hold_events.append(content)
assert len(hold_events) == 1
content = {
"method": "cancel_hold_invoice",
"params": {
"payment_hash": payment_hash,
},
}
content = json.dumps(content)
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
(responses6, _res) = await fetch_event_responses(
client,
client_pubkey,
23195,
client.send_event(event),
1,
)
error_events = []
success_events = []
for event in responses6:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if (
"result" in content
and content["result"] is not None
and content["result_type"] == "cancel_hold_invoice"
):
success_events.append(content)
if "error" in content and content["error"] is not None:
error_events.append(content)
assert len(success_events) == 1
assert len(error_events) == 0
for content in success_events:
assert content["result_type"] == "cancel_hold_invoice"
lookup_hold = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=None,
invoice=invoice2,
)
)
assert lookup_hold.invoice == invoice2
assert lookup_hold.amount == 5000
assert lookup_hold.description == "cancel_hold"
assert lookup_hold.created_at.as_secs() == invoice2_created_at
assert lookup_hold.description_hash is None
assert lookup_hold.expires_at.as_secs() == invoice2_expires_at
assert lookup_hold.fees_paid == 0
assert lookup_hold.metadata is None
assert lookup_hold.preimage is None
assert lookup_hold.payment_hash == payment_hash
assert lookup_hold.transaction_type.name == "INCOMING"
assert lookup_hold.state.name == "EXPIRED"
assert lookup_hold.settled_at is None
invoice2_decoded = l1.rpc.call("decode", [invoice2])
assert invoice2_decoded["min_final_cltv_expiry"] == 200
wait_for(
lambda: l1.rpc.call("listpays", {"payment_hash": payment_hash})["pays"][0][
"status"
]
== "failed"
)
nwc = Nwc(uri)
invoice_lookup1 = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=None,
)
)
invoice_lookup2 = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=None,
invoice=invoice2,
)
)
invoice_lookup3 = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=invoice2,
)
)
assert invoice_lookup1 == invoice_lookup2
assert invoice_lookup1 == invoice_lookup3
invoice_lookup4 = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=None,
invoice=invoice1,
)
)
result = await nwc.list_transactions(
ListTransactionsRequest(
_from=None,
until=None,
limit=None,
offset=None,
unpaid=True,
transaction_type=None,
)
)
assert len(result) == 2
assert result == [invoice_lookup1, invoice_lookup4] or result == [
invoice_lookup4,
invoice_lookup1,
]
description3 = "test3"
description_hash3 = hashlib.sha256(description3.encode()).hexdigest()
preimage = secrets.token_hex(32)
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
content = {
"method": "make_hold_invoice",
"params": {
"amount": 5001,
"payment_hash": payment_hash,
"description": description3,
"description_hash": description_hash3,
},
}
content = json.dumps(content)
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
await client.send_event(event)
start_time = datetime.now()
while (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
try:
await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=None,
)
)
break
except Exception:
continue
lookup_hold = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=None,
)
)
assert lookup_hold.amount == 5001
assert lookup_hold.description is None
assert lookup_hold.description_hash == description_hash3
assert lookup_hold.metadata is None
assert lookup_hold.preimage is None
assert lookup_hold.payment_hash == payment_hash
assert lookup_hold.transaction_type.name == "INCOMING"
assert lookup_hold.state.name == "PENDING"
assert lookup_hold.settled_at is None
description4 = "test4"
description_hash4 = hashlib.sha256(description4.encode()).hexdigest()
preimage = secrets.token_hex(32)
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
content = {
"method": "make_hold_invoice",
"params": {
"amount": 5002,
"payment_hash": payment_hash,
"description_hash": description_hash4,
},
}
content = json.dumps(content)
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
await client.send_event(event)
start_time = datetime.now()
while (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
try:
await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=None,
)
)
break
except Exception:
continue
lookup_hold = await nwc.lookup_invoice(
LookupInvoiceRequest(
payment_hash=payment_hash,
invoice=None,
)
)
assert lookup_hold.amount == 5002
assert lookup_hold.description is None
assert lookup_hold.description_hash == description_hash4
assert lookup_hold.metadata is None
assert lookup_hold.preimage is None
assert lookup_hold.payment_hash == payment_hash
assert lookup_hold.transaction_type.name == "INCOMING"
assert lookup_hold.state.name == "PENDING"
assert lookup_hold.settled_at is None
info_event = await fetch_info_event(client, uri)
assert (
info_event.content()
== "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_hold_invoice cancel_hold_invoice settle_hold_invoice notifications"
)
assert (
info_event.tags().find(TagKind.UNKNOWN("encryption")).content()
== "nip44_v2 nip04"
)
assert (
info_event.tags().find(TagKind.UNKNOWN("notifications")).content()
== "payment_received payment_sent hold_invoice_accepted"
)
l2.restart()
l1.rpc.connect(l2.info["id"], "localhost", l2.port)
(responses7, _res) = await fetch_event_responses(
client,
client_pubkey,
23196,
lambda: executor.submit(l1.rpc.call, "xpay", [lookup_hold.invoice]),
1,
)
hold_events = []
for event in responses7:
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(content)
LOGGER.info(content)
if (
content["notification_type"] == "hold_invoice_accepted"
and content["notification"]["payment_hash"] == payment_hash
):
hold_events.append(content)
assert len(hold_events) == 1

View file

@ -1,8 +1,8 @@
import logging
import os import os
import random import random
import string import string
from pathlib import Path from pathlib import Path
from hashlib import sha256
import pytest import pytest
@ -11,9 +11,11 @@ plugin_dir = Path(__file__).parent.parent.resolve()
COMPILED_PATH = plugin_dir / "target" / RUST_PROFILE / "cln-nip47" COMPILED_PATH = plugin_dir / "target" / RUST_PROFILE / "cln-nip47"
DOWNLOAD_PATH = plugin_dir / "tests" / "cln-nip47" DOWNLOAD_PATH = plugin_dir / "tests" / "cln-nip47"
DOWNLOAD_HOLD_PATH = plugin_dir / "tests" / "hold"
@pytest.fixture @pytest.fixture
def get_plugin(directory): def get_plugin():
if COMPILED_PATH.is_file(): if COMPILED_PATH.is_file():
return COMPILED_PATH return COMPILED_PATH
elif DOWNLOAD_PATH.is_file(): elif DOWNLOAD_PATH.is_file():
@ -22,6 +24,14 @@ def get_plugin(directory):
raise ValueError("No files were found.") raise ValueError("No files were found.")
@pytest.fixture
def get_hold():
if DOWNLOAD_HOLD_PATH.is_file():
return DOWNLOAD_HOLD_PATH
else:
raise ValueError("No files were found.")
def generate_random_label(): def generate_random_label():
label_length = 8 label_length = 8
random_label = "".join( random_label = "".join(
@ -34,15 +44,6 @@ def generate_random_number():
return random.randint(1, 20_000_000_000_000_00_000) return random.randint(1, 20_000_000_000_000_00_000)
def pay_with_thread(rpc, bolt11):
LOGGER = logging.getLogger(__name__)
try:
rpc.dev_pay(bolt11, dev_use_shadow=False)
except Exception as e:
LOGGER.info(f"holdinvoice: Error paying payment hash:{e}")
pass
def update_config_file_option(lightning_dir, option_name, option_value): def update_config_file_option(lightning_dir, option_name, option_value):
with open(lightning_dir + "/config", "r") as file: with open(lightning_dir + "/config", "r") as file:
lines = file.readlines() lines = file.readlines()

142
tests/uv.lock generated
View file

@ -451,62 +451,62 @@ wheels = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.7" version = "48.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
{ url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" },
{ url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" },
{ url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" },
{ url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" },
{ url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" },
{ url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" },
] ]
[[package]] [[package]]
@ -558,11 +558,11 @@ wheels = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.13" version = "3.14"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" },
] ]
[[package]] [[package]]
@ -759,11 +759,11 @@ wheels = [
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.1" version = "26.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
] ]
[[package]] [[package]]
@ -895,20 +895,20 @@ wheels = [
[[package]] [[package]]
name = "pyln-client" name = "pyln-client"
version = "26.4" version = "26.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyln-bolt7" }, { name = "pyln-bolt7" },
{ name = "pyln-proto" }, { name = "pyln-proto" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8a/14/0faf6b8b04f368629897a010357d3b8d21c8a800b972b433a358caebe32b/pyln_client-26.4.tar.gz", hash = "sha256:e696076fcf1329ee22198097b8139a096f653cbbfc6a03e471455ca6f5d68a46", size = 92160, upload-time = "2026-04-20T04:58:12.666Z" } sdist = { url = "https://files.pythonhosted.org/packages/28/fe/e1e19c59dd7520dec2de992f0a3da6292db24162c9830be279616a7b8810/pyln_client-26.4.1.tar.gz", hash = "sha256:26dfb21ac39c61b7ab7a40db18798b002f3f19a179ff2ec40374a9419d032050", size = 92170, upload-time = "2026-04-24T04:53:08.46Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/41/f67df5064b4b9d258fb30e96e1d3ca8f0395c7c0098b24091dde6238fb8e/pyln_client-26.4-py3-none-any.whl", hash = "sha256:d5470e1ecccc1b6e46e39658c1019c62868c5aed4a0203635052803ddedcce2e", size = 37655, upload-time = "2026-04-20T04:58:11.429Z" }, { url = "https://files.pythonhosted.org/packages/65/90/d3b5e11f85cf076b1011a1ea6031ced3745abf451e47a1697216e1ea7c29/pyln_client-26.4.1-py3-none-any.whl", hash = "sha256:a9afba9b117818023bf4567a94c34633e9f2e89f0e1ca11e433329a2c678a685", size = 37673, upload-time = "2026-04-24T04:53:07.187Z" },
] ]
[[package]] [[package]]
name = "pyln-proto" name = "pyln-proto"
version = "26.4" version = "26.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "base58" }, { name = "base58" },
@ -917,14 +917,14 @@ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
{ name = "pysocks" }, { name = "pysocks" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0d/35/5d30332fcaa8d77be87290c5534d45c48822310611ae6ea6d3e48c242ded/pyln_proto-26.4.tar.gz", hash = "sha256:dde9e243c7bf6fbcf7904e0dbd18c4f05dc17e668b074bfab090a7376e3de6cf", size = 44861, upload-time = "2026-04-20T04:58:09.361Z" } sdist = { url = "https://files.pythonhosted.org/packages/8c/4b/c8d676ad84d6066553b203668c0e36f45cfae0a4d7140eb88aed6d65b38b/pyln_proto-26.4.1.tar.gz", hash = "sha256:d73f261bacc06a6a613dcece05fc429699b740a4673f24dffb7c41bd3542d589", size = 44864, upload-time = "2026-04-24T04:53:10.196Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/8a34960e1e9550f87697d1f05208d9cc916c14419f18f2d6013801079fc1/pyln_proto-26.4-py3-none-any.whl", hash = "sha256:97772d285aa0d0c8a1545577aeef8821e08d59676f63ae760d270e1b3015bc9d", size = 31811, upload-time = "2026-04-20T04:58:08.369Z" }, { url = "https://files.pythonhosted.org/packages/a3/f1/6541b6cd8a017d3505fcb49885d3f01b8e6bb09e40db18e686edb9edeb99/pyln_proto-26.4.1-py3-none-any.whl", hash = "sha256:b5d10d5e69cf3debd9b289e2ffa49713fca6c349a71cb0eaf9e733024f15b3da", size = 31833, upload-time = "2026-04-24T04:53:08.686Z" },
] ]
[[package]] [[package]]
name = "pyln-testing" name = "pyln-testing"
version = "26.4" version = "26.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cheroot" }, { name = "cheroot" },
@ -939,9 +939,9 @@ dependencies = [
{ name = "python-bitcoinlib" }, { name = "python-bitcoinlib" },
{ name = "requests" }, { name = "requests" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/17/7e/275f0416e30faa3093ad31703f3d8702dc06569164a185813b23672ba6d9/pyln_testing-26.4.tar.gz", hash = "sha256:cca1f4fec65f12bfda809674ea7c231285a96ef0b8bc2c69ef4919f1b465d2ae", size = 52609, upload-time = "2026-04-20T04:58:07.746Z" } sdist = { url = "https://files.pythonhosted.org/packages/64/73/d969847564591fe16633b11bf8be9f8184ccd885e367acee171e9fd6d2d8/pyln_testing-26.4.1.tar.gz", hash = "sha256:0a40d66ec2f0840df95bdbc4845c214aede01e9d7a89a422f5b6b00a89e63690", size = 52617, upload-time = "2026-04-24T04:53:10.904Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/c4/3cb55bf86a2499510a24b310897903efafce847869eb1ec1c7b67b7fcfa4/pyln_testing-26.4-py3-none-any.whl", hash = "sha256:ccb7641bbbb1b6b650f1b8238aacf04d7ebeea944ebd2855a901e25d18846804", size = 54365, upload-time = "2026-04-20T04:58:06.793Z" }, { url = "https://files.pythonhosted.org/packages/c6/ad/603ff35f7b60bcc6cf364a101c58a73887132063f0773767108946361ed2/pyln_testing-26.4.1-py3-none-any.whl", hash = "sha256:dd0e9d8a865593ddc9a14ef43e9aaec0fe0d348ce9ec2fcd23807a35c98c9e8c", size = 54381, upload-time = "2026-04-24T04:53:09.599Z" },
] ]
[[package]] [[package]]
@ -1035,7 +1035,7 @@ wheels = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.33.1" version = "2.34.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@ -1043,9 +1043,9 @@ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" },
] ]
[[package]] [[package]]
@ -1273,11 +1273,11 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.3" version = "2.7.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
] ]
[[package]] [[package]]