diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index 5d05e9331..dfefecf72 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -25,7 +25,7 @@ jobs: distribution: temurin java-version: 21 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.88.0 with: targets: x86_64-linux-android, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios diff --git a/Cargo.lock b/Cargo.lock index 5e5b9816b..2350cc829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "aes-gcm" version = "0.9.2" @@ -55,7 +66,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead 0.4.3", - "aes", + "aes 0.7.5", "cipher 0.3.0", "ctr", "ghash", @@ -94,9 +105,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -109,15 +120,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -144,9 +155,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arrayvec" @@ -187,7 +198,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -241,6 +252,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -260,7 +284,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -271,7 +295,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -381,9 +405,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "basic-toml" @@ -424,11 +448,11 @@ checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bip39" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.1", "rand", "rand_core", "serde", @@ -443,13 +467,13 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitcoin" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +checksum = "69197dee21fe23b45f5239bf88086efaa0cb8679f3e704906eb818e8ea169c14" dependencies = [ "bech32 0.10.0-beta", - "bitcoin-internals 0.2.0", - "bitcoin_hashes 0.13.0", + "bitcoin-internals 0.2.1", + "bitcoin_hashes 0.13.1", "hex-conservative 0.1.2", "hex_lit", "secp256k1 0.28.2", @@ -483,14 +507,14 @@ checksum = "0468a0cbe4fb594b40940e107694c4edfc316a9d83bbeeff14ba1acbdfd291c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "bitcoin-internals" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +checksum = "994dc6fcc13751c85370b7de118e672b193b9b65167bf09e258f124c97fb9685" dependencies = [ "serde", ] @@ -523,9 +547,9 @@ dependencies = [ [[package]] name = "bitcoin-units" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" dependencies = [ "bitcoin-internals 0.3.0", "serde", @@ -533,11 +557,10 @@ dependencies = [ [[package]] name = "bitcoin_hashes" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +checksum = "446819536d8121575eeb7e89efdbadb3f055e87e4bb66c6679a6d5cc2f4b64fd" dependencies = [ - "bitcoin-internals 0.2.0", "hex-conservative 0.1.2", "serde", ] @@ -570,9 +593,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -580,7 +603,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding", + "block-padding 0.2.1", "generic-array", ] @@ -599,6 +622,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -616,14 +648,14 @@ checksum = "a6836000a836c3fc91ad5db2c95552c556b8347a6f6782a8f77a00d3fd989122" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -633,18 +665,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -669,14 +701,23 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", ] [[package]] name = "cc" -version = "1.2.46" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -720,9 +761,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -760,9 +801,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.52" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -770,9 +811,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.52" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -782,21 +823,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clightningrpc" @@ -816,7 +857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3a50be955808d07d6f33f05bb11ce5a3875d92b4d1fff5623f9458feb5164c" dependencies = [ "anyhow", - "bitcoin 0.31.2", + "bitcoin 0.31.3", "cln-rpc", "futures-core", "hex", @@ -856,7 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4436e58f1fccb1faf69df9ac436ae9304b5c200c7d92e6c4229826bdd0a8d0d" dependencies = [ "anyhow", - "bitcoin 0.31.2", + "bitcoin 0.31.3", "bytes", "futures-util", "hex", @@ -869,9 +910,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "const-oid" @@ -973,7 +1014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -992,15 +1033,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.31.2", + "nix", "windows-sys 0.61.2", ] [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1008,34 +1049,33 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -1063,9 +1103,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -1122,7 +1162,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -1136,7 +1176,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1168,9 +1208,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -1204,9 +1244,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -1233,27 +1273,26 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -1276,6 +1315,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1287,9 +1332,12 @@ dependencies = [ [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "fs-err" @@ -1312,9 +1360,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1327,9 +1375,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1337,15 +1385,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1354,32 +1402,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1389,9 +1437,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1401,7 +1449,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1426,9 +1473,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1437,14 +1484,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", + "wasip3", ] [[package]] @@ -1470,11 +1518,11 @@ dependencies = [ "bip39", "clap", "dirs", - "env_logger 0.11.8", + "env_logger 0.11.10", "futures", "gl-client", "hex", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "vls-core", ] @@ -1483,12 +1531,14 @@ dependencies = [ name = "gl-client" version = "0.4.0" dependencies = [ + "aes 0.8.4", "anyhow", "async-stream", "async-trait", "base64 0.21.7", "bech32 0.9.1", "bytes", + "cbc", "chacha20poly1305", "chrono", "cln-grpc", @@ -1500,7 +1550,7 @@ dependencies = [ "mockall", "picky", "picky-asn1-der 0.4.1", - "picky-asn1-x509 0.15.2", + "picky-asn1-x509 0.15.4", "pin-project", "prost 0.12.6", "prost-derive 0.12.6", @@ -1586,7 +1636,7 @@ dependencies = [ "lazy_static", "linemux", "log", - "nix 0.30.1", + "nix", "prost 0.12.6", "serde", "serde_json", @@ -1614,11 +1664,12 @@ dependencies = [ "once_cell", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tonic 0.11.0", "tracing", "uniffi", + "url", ] [[package]] @@ -1629,12 +1680,12 @@ dependencies = [ "clap", "ctrlc", "dirs", - "env_logger 0.11.8", + "env_logger 0.11.10", "gl-sdk", "hex", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1724,7 +1775,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.12.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1748,9 +1799,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1916,9 +1976,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1940,12 +2000,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1953,9 +2014,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1966,9 +2027,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1980,15 +2041,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2000,15 +2061,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2019,6 +2080,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2058,12 +2125,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -2098,6 +2167,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding 0.3.3", "generic-array", ] @@ -2112,9 +2182,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is-terminal" @@ -2153,15 +2223,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -2172,30 +2242,32 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -2229,11 +2301,17 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -2247,19 +2325,20 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", - "redox_syscall 0.5.18", + "plain", + "redox_syscall 0.7.4", ] [[package]] @@ -2319,15 +2398,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2340,9 +2419,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "matchers" @@ -2361,9 +2440,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2409,9 +2488,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -2463,7 +2542,7 @@ version = "2.16.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "ctor", "napi-derive", "napi-sys", @@ -2488,7 +2567,7 @@ dependencies = [ "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -2503,7 +2582,7 @@ dependencies = [ "quote", "regex", "semver", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -2515,25 +2594,13 @@ dependencies = [ "libloading", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nix" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -2623,9 +2690,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -2701,9 +2768,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2820,7 +2887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.12.0", + "indexmap 2.14.0", ] [[package]] @@ -2905,9 +2972,9 @@ dependencies = [ [[package]] name = "picky-asn1-der" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b491eb61603cba1ad5c6be0269883538f8d74136c35e3641a840fb0fbcd41efc" +checksum = "d413165e4bf7f808b9a27cbaba657657a2921f0965db833f488c4d4be96dcd2e" dependencies = [ "picky-asn1 0.10.1", "serde", @@ -2930,48 +2997,42 @@ dependencies = [ [[package]] name = "picky-asn1-x509" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c97cd14d567a17755910fa8718277baf39d08682a980b1b1a4b4da7d0bc61a04" +checksum = "859d4117bd1b1dc5646359ee7243c50c5000c0920ea2d1fb120335a2f4c684b8" dependencies = [ "base64 0.22.1", "oid", "picky-asn1 0.10.1", - "picky-asn1-der 0.5.4", + "picky-asn1-der 0.5.6", "serde", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plain" @@ -3004,15 +3065,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -3023,14 +3084,14 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b122a615d72104fb3d8b26523fdf9232cd8ee06949fb37e4ce3ff964d15dffd" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3066,15 +3127,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -3097,14 +3158,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3168,7 +3229,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.110", + "syn 2.0.117", "tempfile", ] @@ -3195,7 +3256,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3285,24 +3346,24 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -3325,7 +3386,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -3356,7 +3417,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.1", ] [[package]] @@ -3365,16 +3435,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3384,9 +3454,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3395,9 +3465,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -3463,7 +3533,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -3500,22 +3570,22 @@ dependencies = [ "crypto", "env_logger 0.10.2", "hex", - "indexmap 2.12.0", + "indexmap 2.14.0", "sha2 0.10.9", "thiserror 1.0.69", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rusticata-macros" @@ -3532,7 +3602,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3541,14 +3611,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3622,9 +3692,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -3658,9 +3728,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3673,9 +3743,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3703,7 +3773,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3731,7 +3801,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.13.1", "secp256k1-sys 0.9.2", "serde", ] @@ -3781,7 +3851,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -3790,9 +3860,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3800,9 +3870,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -3820,9 +3890,9 @@ dependencies = [ [[package]] name = "serde_bolt" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ec23cff5db3388b8549d1f70fdf14bd506b5321d0256300228dd18c05e26e8" +checksum = "8db02c046447759ef62beeede660c4b7abae023a961bd5a8935aa7d4207edd1a" dependencies = [ "bitcoin 0.32.8", "bitcoin-consensus-derive", @@ -3857,20 +3927,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3887,9 +3957,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -3902,14 +3972,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3991,10 +4061,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4027,9 +4098,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "sled" @@ -4071,12 +4142,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4138,9 +4209,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4173,7 +4244,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4205,14 +4276,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4251,11 +4322,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4266,18 +4337,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4291,30 +4362,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4322,9 +4393,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4332,9 +4403,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4347,17 +4418,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", - "mio 1.1.0", + "mio 1.2.0", "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -4374,13 +4445,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4417,9 +4488,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -4429,9 +4500,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4536,7 +4607,7 @@ dependencies = [ "proc-macro2", "prost-build 0.12.6", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4573,9 +4644,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -4591,14 +4662,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -4627,9 +4698,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -4664,15 +4735,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -4685,9 +4756,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -4733,7 +4804,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "once_cell", "serde", "tempfile", @@ -4763,6 +4834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" dependencies = [ "anyhow", + "async-compat", "bytes", "once_cell", "static_assertions", @@ -4775,10 +4847,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" dependencies = [ "anyhow", - "indexmap 2.12.0", + "indexmap 2.14.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4793,7 +4865,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.110", + "syn 2.0.117", "toml", "uniffi_meta", ] @@ -4818,7 +4890,7 @@ checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "tempfile", "uniffi_internal_macros", ] @@ -4875,9 +4947,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -4899,12 +4971,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -4943,7 +5015,7 @@ dependencies = [ "bitcoin-consensus-derive", "bitcoin-push-decoder", "bolt-derive", - "env_logger 0.11.8", + "env_logger 0.11.10", "hashbrown 0.13.2", "hex", "itertools 0.10.5", @@ -5044,18 +5116,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -5066,22 +5147,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5089,31 +5167,65 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5202,7 +5314,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -5213,7 +5325,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -5267,15 +5379,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -5309,30 +5412,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5345,12 +5431,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5363,12 +5443,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5381,24 +5455,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5411,12 +5473,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -5429,12 +5485,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -5447,12 +5497,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -5465,17 +5509,11 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -5492,15 +5530,103 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease 0.2.37", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease 0.2.37", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x509-certificate" @@ -5551,9 +5677,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5562,54 +5688,54 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "synstructure 0.13.2", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "synstructure 0.13.2", ] @@ -5624,20 +5750,20 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5646,9 +5772,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -5657,11 +5783,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/libs/gl-client/Cargo.toml b/libs/gl-client/Cargo.toml index eb397e840..9a63085f1 100644 --- a/libs/gl-client/Cargo.toml +++ b/libs/gl-client/Cargo.toml @@ -16,10 +16,12 @@ permissive = [] export = ["chacha20poly1305", "secp256k1"] [dependencies] +aes = "0.8" anyhow = "1.0.82" async-stream = "0.3.5" base64 = "^0.21" bech32 = "0.9.1" +cbc = { version = "0.1", features = ["alloc"] } bytes = "1.2.1" chrono = "0.4.31" hex = "0.4.3" diff --git a/libs/gl-client/src/lnurl/mod.rs b/libs/gl-client/src/lnurl/mod.rs index ddf506d09..5c63a4084 100644 --- a/libs/gl-client/src/lnurl/mod.rs +++ b/libs/gl-client/src/lnurl/mod.rs @@ -1,19 +1,20 @@ -mod models; -mod pay; -mod utils; -mod withdraw; - -use self::models::{ - LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse, WithdrawRequestResponse, -}; -use self::utils::{parse_invoice, parse_lnurl}; +pub mod models; +pub mod pay; +pub mod utils; +pub mod withdraw; + +use self::models::{LnUrlHttpClient, PayRequestResponse, WithdrawRequestResponse}; +use self::utils::parse_lnurl; use crate::node::ClnClient; use crate::pb::cln::{amount_or_any, Amount, AmountOrAny}; use anyhow::{anyhow, Result}; use models::LnUrlHttpClearnetClient; -use pay::{resolve_lnurl_to_invoice, validate_invoice_from_callback_response}; -use url::Url; -use withdraw::{build_withdraw_request_callback_url, parse_withdraw_request_response_from_url}; + +/// Result of resolving an LNURL endpoint via HTTP. +pub enum LnUrlResponse { + Pay(PayRequestResponse), + Withdraw(WithdrawRequestResponse), +} pub struct LNURL { http_client: T, @@ -29,37 +30,34 @@ impl LNURL { LNURL { http_client } } - pub async fn get_pay_request_response(&self, lnurl: &str) -> Result { - let url = parse_lnurl(lnurl)?; - - let lnurl_pay_request_response: PayRequestResponse = - self.http_client.get_pay_request_response(&url).await?; - - if lnurl_pay_request_response.tag != "payRequest" { - return Err(anyhow!("Expected tag to say 'payRequest'")); + /// Resolve an LNURL to its endpoint data with a single HTTP GET. + /// + /// Decodes the bech32, fetches the URL, inspects the `tag` field, + /// and returns the appropriate typed response. + pub async fn resolve(&self, url: &str) -> Result { + let json = self.http_client.get_json(url).await?; + + let tag = json + .get("tag") + .and_then(|t| t.as_str()) + .unwrap_or(""); + + match tag { + "payRequest" => { + let response: PayRequestResponse = serde_json::from_value(json) + .map_err(|e| anyhow!("Failed to parse payRequest response: {}", e))?; + Ok(LnUrlResponse::Pay(response)) + } + "withdrawRequest" => { + let response: WithdrawRequestResponse = serde_json::from_value(json) + .map_err(|e| anyhow!("Failed to parse withdrawRequest response: {}", e))?; + Ok(LnUrlResponse::Withdraw(response)) + } + _ => Err(anyhow!( + "Unknown LNURL tag: '{}'. Expected 'payRequest' or 'withdrawRequest'.", + tag + )), } - - Ok(lnurl_pay_request_response) - } - - pub async fn get_pay_request_callback_response( - &self, - base_callback_url: &str, - amount_msats: u64, - metadata: &str, - ) -> Result { - let mut url = Url::parse(base_callback_url)?; - url.query_pairs_mut() - .append_pair("amount", &amount_msats.to_string()); - - let callback_response: PayRequestCallbackResponse = self - .http_client - .get_pay_request_callback_response(&url.to_string()) - .await?; - - let invoice = parse_invoice(&callback_response.pr)?; - validate_invoice_from_callback_response(&invoice, amount_msats, metadata)?; - Ok(callback_response) } pub async fn pay( @@ -68,24 +66,27 @@ impl LNURL { amount_msats: u64, node: &mut ClnClient, ) -> Result> { - let invoice = resolve_lnurl_to_invoice(&self.http_client, lnurl, amount_msats).await?; + let (invoice, _success_action) = + pay::resolve_lnurl_to_invoice(&self.http_client, lnurl, amount_msats, None).await?; node.pay(crate::pb::cln::PayRequest { - bolt11: invoice.to_string(), + bolt11: invoice, ..Default::default() }) .await .map_err(|e| anyhow!(e)) } - pub async fn get_withdraw_request_response( + pub async fn withdraw( &self, lnurl: &str, - ) -> Result { + amount_msats: u64, + node: &mut ClnClient, + ) -> Result<()> { let url = parse_lnurl(lnurl)?; - let withdrawal_request_response = parse_withdraw_request_response_from_url(&url); + let withdrawal_request_response = + withdraw::parse_withdraw_request_response_from_url(&url); - //If it's not a quick withdraw, then get the withdrawal_request_response from the web. let withdrawal_request_response = match withdrawal_request_response { Some(w) => w, None => { @@ -95,24 +96,13 @@ impl LNURL { } }; - Ok(withdrawal_request_response) - } - - pub async fn withdraw( - &self, - lnurl: &str, - amount_msats: u64, - node: &mut ClnClient, - ) -> Result<()> { - let withdraw_request_response = self.get_withdraw_request_response(lnurl).await?; - let amount = AmountOrAny { value: Some(amount_or_any::Value::Amount(Amount { msat: amount_msats })), }; let invoice = node .invoice(crate::pb::cln::InvoiceRequest { amount_msat: Some(amount), - description: withdraw_request_response.default_description.clone(), + description: withdrawal_request_response.default_description.clone(), ..Default::default() }) .await @@ -120,7 +110,7 @@ impl LNURL { .into_inner(); let callback_url = - build_withdraw_request_callback_url(&withdraw_request_response, invoice.bolt11)?; + withdrawal_request_response.build_callback_url(&invoice.bolt11)?; let _ = self .http_client diff --git a/libs/gl-client/src/lnurl/models.rs b/libs/gl-client/src/lnurl/models.rs index 326696e68..547cdd5ab 100644 --- a/libs/gl-client/src/lnurl/models.rs +++ b/libs/gl-client/src/lnurl/models.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, ensure, Result}; use async_trait::async_trait; use log::debug; use mockall::automock; @@ -6,7 +6,7 @@ use reqwest::Response; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct PayRequestResponse { pub callback: String, #[serde(rename = "maxSendable")] @@ -15,26 +15,35 @@ pub struct PayRequestResponse { pub min_sendable: u64, pub tag: String, pub metadata: String, + /// Maximum comment length the service accepts (LUD-12). + /// None or 0 means comments are not supported. + #[serde(rename = "commentAllowed")] + #[serde(default)] + pub comment_allowed: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, Clone, Debug)] pub struct PayRequestCallbackResponse { pub pr: String, pub routes: Vec, + /// Optional success action returned by the service (LUD-09). + #[serde(rename = "successAction")] + #[serde(default)] + pub success_action: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct OkResponse { - status: String, + pub status: String, } #[derive(Debug, Deserialize, Serialize)] pub struct ErrorResponse { - status: String, - reason: String, + pub status: String, + pub reason: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct WithdrawRequestResponse { pub tag: String, pub callback: String, @@ -47,6 +56,98 @@ pub struct WithdrawRequestResponse { pub max_withdrawable: u64, } +/// Raw success action from an LNURL-pay callback response (LUD-09/10). +/// +/// Deserialized directly from the service's JSON. For the AES variant, +/// the ciphertext has not yet been decrypted -- use +/// [`process_success_action`] with the payment preimage to produce a +/// [`ProcessedSuccessAction`]. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "tag")] +pub enum SuccessAction { + #[serde(rename = "message")] + Message { message: String }, + #[serde(rename = "url")] + Url { description: String, url: String }, + #[serde(rename = "aes")] + Aes { + description: String, + /// Base64-encoded ciphertext (max 4096 chars). + ciphertext: String, + /// Base64-encoded IV (24 chars = 16 bytes). + iv: String, + }, +} + +/// A success action after client-side processing. +/// +/// For the Message and Url variants this is identical to the raw +/// [`SuccessAction`]. For AES the ciphertext has been decrypted into +/// plaintext using the payment preimage. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ProcessedSuccessAction { + Message { message: String }, + Url { description: String, url: String }, + Aes { description: String, plaintext: String }, +} + +impl SuccessAction { + /// Process this success action, decrypting AES content if needed. + /// + /// `preimage` is the 32-byte payment preimage from the PayResponse. + /// For Message and Url variants this is a simple conversion; for Aes + /// it decrypts the ciphertext using the preimage as the AES-256 key. + /// + /// All payload-shape checks here are required by the LNURL specs: + /// - Message.message ≤ 144 chars (LUD-09) + /// - Url.description ≤ 144 chars (LUD-09) + /// - Aes.description ≤ 144 chars (LUD-10) + /// - Aes.ciphertext ≤ 4096 chars (LUD-10) + /// - Aes.iv == exactly 24 base64 chars / 16 bytes (LUD-10) + pub fn process(self, preimage: &[u8]) -> Result { + match self { + SuccessAction::Message { message } => { + ensure!( + message.len() <= 144, + "Message success action exceeds 144 chars" + ); + Ok(ProcessedSuccessAction::Message { message }) + } + SuccessAction::Url { description, url } => { + ensure!( + description.len() <= 144, + "Url success action description exceeds 144 chars" + ); + Ok(ProcessedSuccessAction::Url { description, url }) + } + SuccessAction::Aes { + description, + ciphertext, + iv, + } => { + ensure!( + description.len() <= 144, + "AES success action description exceeds 144 chars" + ); + ensure!( + ciphertext.len() <= 4096, + "AES success action ciphertext exceeds 4096 chars" + ); + ensure!( + iv.len() == 24, + "AES success action IV must be exactly 24 base64 chars" + ); + let plaintext = + super::pay::decrypt_aes_success_action(preimage, &ciphertext, &iv)?; + Ok(ProcessedSuccessAction::Aes { + description, + plaintext, + }) + } + } + } +} + #[async_trait] #[automock] pub trait LnUrlHttpClient { @@ -57,6 +158,7 @@ pub trait LnUrlHttpClient { ) -> Result; async fn get_withdrawal_request_response(&self, url: &str) -> Result; async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result; + async fn get_json(&self, url: &str) -> Result; } pub struct LnUrlHttpClearnetClient { @@ -99,7 +201,86 @@ impl LnUrlHttpClient for LnUrlHttpClearnetClient { self.get::(url).await } - async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result{ + async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result { self.get::(url).await } + + async fn get_json(&self, url: &str) -> Result { + self.get::(url).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_success_action_message_serde() { + let json = r#"{"tag":"message","message":"Thank you!"}"#; + let action: SuccessAction = serde_json::from_str(json).unwrap(); + match action { + SuccessAction::Message { message } => assert_eq!(message, "Thank you!"), + _ => panic!("Expected Message variant"), + } + } + + #[test] + fn test_success_action_url_serde() { + let json = r#"{"tag":"url","description":"View order","url":"https://example.com/order/123"}"#; + let action: SuccessAction = serde_json::from_str(json).unwrap(); + match action { + SuccessAction::Url { description, url } => { + assert_eq!(description, "View order"); + assert_eq!(url, "https://example.com/order/123"); + } + _ => panic!("Expected Url variant"), + } + } + + #[test] + fn test_success_action_aes_serde() { + let json = r#"{"tag":"aes","description":"Secret","ciphertext":"YWJj","iv":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0"}"#; + let action: SuccessAction = serde_json::from_str(json).unwrap(); + match action { + SuccessAction::Aes { + description, + ciphertext, + iv, + } => { + assert_eq!(description, "Secret"); + assert_eq!(ciphertext, "YWJj"); + assert_eq!(iv, "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0"); + } + _ => panic!("Expected Aes variant"), + } + } + + #[test] + fn test_callback_response_without_success_action() { + let json = r#"{"pr":"lnbc1...","routes":[]}"#; + let resp: PayRequestCallbackResponse = serde_json::from_str(json).unwrap(); + assert!(resp.success_action.is_none()); + } + + #[test] + fn test_callback_response_with_success_action() { + let json = + r#"{"pr":"lnbc1...","routes":[],"successAction":{"tag":"message","message":"Done"}}"#; + let resp: PayRequestCallbackResponse = serde_json::from_str(json).unwrap(); + assert!(resp.success_action.is_some()); + } + + #[test] + fn test_pay_request_response_with_comment_allowed() { + let json = r#"{"callback":"https://example.com/cb","maxSendable":100000,"minSendable":1000,"tag":"payRequest","metadata":"[]","commentAllowed":140}"#; + let resp: PayRequestResponse = serde_json::from_str(json).unwrap(); + assert_eq!(resp.comment_allowed, Some(140)); + } + + #[test] + fn test_pay_request_response_without_comment_allowed() { + let json = r#"{"callback":"https://example.com/cb","maxSendable":100000,"minSendable":1000,"tag":"payRequest","metadata":"[]"}"#; + let resp: PayRequestResponse = serde_json::from_str(json).unwrap(); + assert_eq!(resp.comment_allowed, None); + } } diff --git a/libs/gl-client/src/lnurl/pay/mod.rs b/libs/gl-client/src/lnurl/pay/mod.rs index 9ac4a9cfa..8ece34da4 100644 --- a/libs/gl-client/src/lnurl/pay/mod.rs +++ b/libs/gl-client/src/lnurl/pay/mod.rs @@ -1,7 +1,7 @@ -use super::models; +use super::models::SuccessAction; use super::utils::parse_lnurl; -use crate::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef}; +use crate::lightning_invoice::Bolt11Invoice; use crate::lnurl::{ models::{LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse}, utils::parse_invoice, @@ -10,202 +10,243 @@ use crate::lnurl::{ use anyhow::{anyhow, ensure, Result}; use log::debug; use reqwest::Url; -use sha256; -pub async fn resolve_lnurl_to_invoice( - http_client: &T, - lnurl_identifier: &str, - amount_msats: u64, -) -> Result { - let url = match is_lnurl(lnurl_identifier) { - true => parse_lnurl(lnurl_identifier)?, - false => parse_lightning_address(lnurl_identifier)?, - }; +impl PayRequestResponse { + /// Extract the "text/plain" description from the metadata JSON. + pub fn description(&self) -> Option { + super::utils::extract_description_from_metadata(&self.metadata) + } - debug!("Domain: {}", Url::parse(&url).unwrap().host().unwrap()); + /// Validate this pay request response for a given amount. + /// + /// Checks the tag, amount range, and — for lightning addresses — + /// that the metadata contains a matching identifier. + pub fn validate(&self, identifier: &str, amount_msats: u64) -> Result<()> { + if self.tag != "payRequest" { + return Err(anyhow!("Expected tag to say 'payRequest'")); + } - let lnurl_pay_request_response: PayRequestResponse = - http_client.get_pay_request_response(&url).await?; + if amount_msats < self.min_sendable { + return Err(anyhow!( + "Amount must be {} or greater", + self.min_sendable + )); + } + if amount_msats > self.max_sendable { + return Err(anyhow!( + "Amount must be {} or less", + self.max_sendable + )); + } - validate_pay_request_response(lnurl_identifier, &lnurl_pay_request_response, amount_msats)?; + debug!( + "Accepted range (in millisatoshis): {} - {}", + self.min_sendable, self.max_sendable + ); - let callback_url = build_callback_url(&lnurl_pay_request_response, amount_msats)?; - let callback_response: PayRequestCallbackResponse = http_client - .get_pay_request_callback_response(&callback_url) - .await?; + // For lightning addresses, verify the identifier appears in metadata + if !is_lnurl(identifier) { + let entries: Vec> = + serde_json::from_str(&self.metadata) + .map_err(|e| anyhow!("Failed to deserialize metadata: {}", e))?; + + let found = entries.iter().any(|entry| { + entry.len() >= 2 + && (entry[0] == "text/email" || entry[0] == "text/identifier") + && entry[1] == identifier + }); + + if !found { + return Err(anyhow!( + "The lightning address specified in the original request \ + does not match what was found in the metadata array" + )); + } + } - let invoice = parse_invoice(&callback_response.pr)?; - validate_invoice_from_callback_response( - &invoice, - amount_msats, - &lnurl_pay_request_response.metadata, - )?; - Ok(invoice.to_string()) -} + Ok(()) + } -fn is_lnurl(lnurl_identifier: &str) -> bool { - const LNURL_PREFIX: &str = "LNURL"; - lnurl_identifier - .trim() - .to_uppercase() - .starts_with(LNURL_PREFIX) + /// Fetch an invoice from this pay request's callback endpoint. + /// + /// Builds the callback URL with the given amount and optional comment, + /// fetches the invoice, validates it against the metadata, and returns + /// the invoice string along with any success action. + pub async fn get_invoice( + &self, + http_client: &T, + amount_msats: u64, + comment: Option<&str>, + ) -> Result<(String, Option)> { + fetch_invoice(http_client, &self.callback, amount_msats, comment).await + } } -pub fn validate_pay_request_response( - lnurl_identifier: &str, - lnurl_pay_request_response: &PayRequestResponse, +/// Fetch an invoice from a pay-request callback URL. +/// +/// This is the "phase 2" of the two-phase LNURL-pay flow: the caller +/// already has the callback URL from the initial `payRequest` response, +/// and now requests an invoice for a specific amount. The returned +/// invoice is validated to be parseable and match the requested amount. +pub async fn fetch_invoice( + http_client: &T, + callback: &str, amount_msats: u64, -) -> Result<()> { - if lnurl_pay_request_response.tag != "payRequest" { - return Err(anyhow!("Expected tag to say 'payRequest'")); + comment: Option<&str>, +) -> Result<(String, Option)> { + let callback_url = build_callback_url(callback, amount_msats, comment)?; + let raw: serde_json::Value = http_client.get_json(&callback_url).await?; + + if raw.get("status").and_then(|v| v.as_str()) == Some("ERROR") { + let reason = raw + .get("reason") + .and_then(|v| v.as_str()) + .unwrap_or("unknown error"); + return Err(anyhow!("{}{}", LNURL_SERVICE_ERROR_PREFIX, reason)); } - ensure_amount_is_within_range(&lnurl_pay_request_response, amount_msats)?; - let description = extract_description(&lnurl_pay_request_response)?; - - debug!("Description: {}", description); - debug!( - "Accepted range (in millisatoshis): {} - {}", - lnurl_pay_request_response.min_sendable, lnurl_pay_request_response.max_sendable - ); - if !is_lnurl(lnurl_identifier) { - let deserialized_metadata: Vec> = - serde_json::from_str(&lnurl_pay_request_response.metadata.to_owned()) - .map_err(|e| anyhow!("Failed to deserialize metadata: {}", e))?; + let callback_response: PayRequestCallbackResponse = serde_json::from_value(raw)?; - let mut identifier = String::new(); - - let metadata_entry_types = ["text/email", "text/identifier"]; + let invoice = parse_invoice(&callback_response.pr)?; + validate_invoice(&invoice, amount_msats)?; + Ok((invoice.to_string(), callback_response.success_action)) +} - for metadata in deserialized_metadata { - let x = &*metadata[0].clone(); - if metadata_entry_types.contains(&x) { - identifier = String::from(metadata[1].clone()); - break; - } - } +/// Prefix used on `fetch_invoice` errors that originate from the LNURL +/// service returning a `{"status":"ERROR"}` body. Callers can match on +/// this prefix to distinguish service-side rejections from transport +/// or parsing failures. +pub const LNURL_SERVICE_ERROR_PREFIX: &str = "LNURL service error: "; - if identifier.is_empty() { - return Err(anyhow!("Could not find an entry of type ")); - } - - if identifier != lnurl_identifier { - return Err(anyhow!("The lightning address specified in the original request does not match what was found in the metadata array")); +/// Build a callback URL with amount and optional comment query parameters. +fn build_callback_url( + callback: &str, + amount: u64, + comment: Option<&str>, +) -> Result { + let mut url = Url::parse(callback)?; + url.query_pairs_mut() + .append_pair("amount", &amount.to_string()); + if let Some(c) = comment { + if !c.is_empty() { + url.query_pairs_mut().append_pair("comment", c); } } - - Ok(()) + Ok(url.to_string()) } -// Validates the invoice on the pay request's callback response -pub fn validate_invoice_from_callback_response( - invoice: &Bolt11Invoice, - amount_msats: u64, - metadata: &str, -) -> Result<()> { - ensure!(invoice.amount_milli_satoshis().unwrap_or_default() == amount_msats , - "Amount found in invoice was not equal to the amount found in the original request\nRequest amount: {}\nInvoice amount:{:?}", amount_msats, invoice.amount_milli_satoshis().unwrap() - ); - - let description_hash: String = match invoice.description() { - Bolt11InvoiceDescriptionRef::Direct(d) => sha256::digest(d.to_string()), - Bolt11InvoiceDescriptionRef::Hash(h) => h.0.to_string(), - }; - +/// Validate a BOLT11 invoice against the user-requested amount. +fn validate_invoice(invoice: &Bolt11Invoice, amount_msats: u64) -> Result<()> { ensure!( - description_hash == sha256::digest(metadata), - "description_hash {} does not match the hash of the metadata {}", - description_hash, - sha256::digest(metadata) + invoice.amount_milli_satoshis().unwrap_or_default() == amount_msats, + "Amount found in invoice was not equal to the amount found in the original request\n\ + Request amount: {}\nInvoice amount: {:?}", + amount_msats, + invoice.amount_milli_satoshis() ); - Ok(()) } -// Function to extract the description from the lnurl pay request response -fn extract_description(lnurl_pay_request_response: &PayRequestResponse) -> Result { - let mut description = String::new(); - - let serialized_metadata = lnurl_pay_request_response.metadata.clone(); +/// Decrypt an AES-256-CBC encrypted success action payload (LUD-10). +/// +/// - `preimage`: 32-byte payment preimage (used as the AES key) +/// - `ciphertext_b64`: base64-encoded ciphertext +/// - `iv_b64`: base64-encoded IV (decodes to 16 bytes) +pub fn decrypt_aes_success_action( + preimage: &[u8], + ciphertext_b64: &str, + iv_b64: &str, +) -> Result { + use aes::Aes256; + use base64::{engine::general_purpose::STANDARD, Engine}; + use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; + + let ciphertext = STANDARD + .decode(ciphertext_b64) + .map_err(|e| anyhow!("Invalid base64 ciphertext: {}", e))?; + let iv = STANDARD + .decode(iv_b64) + .map_err(|e| anyhow!("Invalid base64 IV: {}", e))?; + + if preimage.len() != 32 { + return Err(anyhow!( + "Payment preimage must be 32 bytes, got {}", + preimage.len() + )); + } + if iv.len() != 16 { + return Err(anyhow!("IV must be 16 bytes, got {}", iv.len())); + } - let deserialized_metadata: Vec> = - serde_json::from_str(&serialized_metadata.to_owned()) - .map_err(|e| anyhow!("Failed to deserialize metadata: {}", e))?; + type Aes256CbcDec = cbc::Decryptor; + let decryptor = Aes256CbcDec::new_from_slices(preimage, &iv) + .map_err(|e| anyhow!("AES init failed: {}", e))?; - for metadata in deserialized_metadata { - if metadata[0] == "text/plain" { - description = metadata[1].clone(); - } - } + let plaintext_bytes = decryptor + .decrypt_padded_vec_mut::(&ciphertext) + .map_err(|e| anyhow!("AES decryption failed: {}", e))?; - Ok(description) + String::from_utf8(plaintext_bytes) + .map_err(|e| anyhow!("Decrypted data is not valid UTF-8: {}", e)) } -// Function to build the callback URL based on lnurl pay request response and amount -fn build_callback_url( - lnurl_pay_request_response: &models::PayRequestResponse, - amount: u64, -) -> Result { - let mut url = Url::parse(&lnurl_pay_request_response.callback)?; - url.query_pairs_mut() - .append_pair("amount", &amount.to_string()); - Ok(url.to_string()) +fn is_lnurl(lnurl_identifier: &str) -> bool { + const LNURL_PREFIX: &str = "LNURL"; + lnurl_identifier + .trim() + .to_uppercase() + .starts_with(LNURL_PREFIX) } -// Validates the pay request response for expected values -fn ensure_amount_is_within_range( - lnurl_pay_request_response: &PayRequestResponse, - amount: u64, -) -> Result<()> { - if amount < lnurl_pay_request_response.min_sendable { - return Err(anyhow!( - "Amount must be {} or greater", - lnurl_pay_request_response.min_sendable - )); - } +/// Resolve an LNURL or lightning address to an invoice in one shot. +/// +/// Convenience function that combines resolution + validation + invoice +/// fetching. For a two-phase flow, use `PayRequestResponse::get_invoice()` +/// directly after resolving. +pub async fn resolve_lnurl_to_invoice( + http_client: &T, + lnurl_identifier: &str, + amount_msats: u64, + comment: Option<&str>, +) -> Result<(String, Option)> { + let url = match is_lnurl(lnurl_identifier) { + true => parse_lnurl(lnurl_identifier)?, + false => parse_lightning_address(lnurl_identifier)?, + }; - if amount > lnurl_pay_request_response.max_sendable { - return Err(anyhow!( - "Amount must be {} or less", - lnurl_pay_request_response.max_sendable - )); - } + debug!("Domain: {}", Url::parse(&url).unwrap().host().unwrap()); - Ok(()) + let pay_request: PayRequestResponse = + http_client.get_pay_request_response(&url).await?; + + pay_request.validate(lnurl_identifier, amount_msats)?; + pay_request.get_invoice(http_client, amount_msats, comment).await } -//LUD-16: Paying to static internet identifiers. +/// Parse a lightning address into its well-known LNURL-pay URL (LUD-16). pub fn parse_lightning_address(lightning_address: &str) -> Result { - let lightning_address_components: Vec<&str> = lightning_address.split("@").collect(); - - if lightning_address_components.len() != 2 { - return Err(anyhow!("The provided lightning address is improperly formatted")); - } - - let username = match lightning_address_components.get(0) { - None => return Err(anyhow!("Could not parse username in lightning address")), - Some(u) => { - if u.is_empty() { - return Err(anyhow!("Username can not be empty")) - } + let parts: Vec<&str> = lightning_address.split('@').collect(); - u - } - }; + if parts.len() != 2 { + return Err(anyhow!( + "The provided lightning address is improperly formatted" + )); + } - let domain = match lightning_address_components.get(1) { - None => return Err(anyhow!("Could not parse domain in lightning address")), - Some(d) => { - if d.is_empty() { - return Err(anyhow!("Domain can not be empty")) - } + let username = parts[0]; + let domain = parts[1]; - d - } - }; + if username.is_empty() { + return Err(anyhow!("Username can not be empty")); + } + if domain.is_empty() { + return Err(anyhow!("Domain can not be empty")); + } - let pay_request_url = ["https://", domain, "/.well-known/lnurlp/", username].concat(); - return Ok(pay_request_url); + Ok(format!( + "https://{}/.well-known/lnurlp/{}", + domain, username + )) } #[cfg(test)] @@ -244,18 +285,18 @@ mod tests { convert_to_async_return_value(Ok(x)) }); - mock_http_client.expect_get_pay_request_callback_response().returning(|_url| { + mock_http_client.expect_get_json().returning(|_url| { let invoice = "lnbc1u1pjv9qrvsp5e5wwexctzp9yklcrzx448c68q2a7kma55cm67ruajjwfkrswnqvqpp55x6mmz8ch6nahrcuxjsjvs23xkgt8eu748nukq463zhjcjk4s65shp5dd6hc533r655wtyz63jpf6ja08srn6rz6cjhwsjuyckrqwanhjtsxqzjccqpjrzjqw6lfdpjecp4d5t0gxk5khkrzfejjxyxtxg5exqsd95py6rhwwh72rpgrgqq3hcqqgqqqqlgqqqqqqgq9q9qxpqysgq95njz4sz6h7r2qh7txnevcrvg0jdsfpe72cecmjfka8mw5nvm7tydd0j34ps2u9q9h6v5u8h3vxs8jqq5fwehdda6a8qmpn93fm290cquhuc6r"; - let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string(); - let x = serde_json::from_str(&callback_response_json).unwrap(); + let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice); + let x: serde_json::Value = serde_json::from_str(&callback_response_json).unwrap(); convert_to_async_return_value(Ok(x)) }); let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; let amount = 100000; - let invoice = resolve_lnurl_to_invoice(&mock_http_client, &lnurl, amount).await; - assert!(invoice.is_ok()); + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount, None).await; + assert!(result.is_ok()); } #[tokio::test] @@ -279,101 +320,58 @@ mod tests { convert_to_async_return_value(Ok(x)) }); - mock_http_client.expect_get_pay_request_callback_response().returning(|_url| { + mock_http_client.expect_get_json().returning(|_url| { let invoice = "lnbcrt1u1pj0ypx6sp5hzczugdw9eyw3fcsjkssux7awjlt68vpj7uhmen7sup0hdlrqxaqpp5gp5fm2sn5rua2jlzftkf5h22rxppwgszs7ncm73pmwhvjcttqp3qdy2tddjyar90p6z7urvv95kug3vyq39xarpwf6zqargv5syxmmfde28yctfdc396tpqtv38getcwshkjer9de6xjenfv4ezytpqyfekzar0wd5xjsrrd9cxsetjwp6ku6ewvdhk6gjat5xqyjw5qcqp29qxpqysgqujuf5zavazln2q9gks7nqwdgjypg2qlvv7aqwfmwg7xmjt8hy4hx2ctr5fcspjvmz9x5wvmur8vh6nkynsvateafm73zwg5hkf7xszsqajqwcf"; - let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string(); - let x = serde_json::from_str(&callback_response_json).unwrap(); + let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice); + let x: serde_json::Value = serde_json::from_str(&callback_response_json).unwrap(); convert_to_async_return_value(Ok(x)) }); let amount = 100000; - let invoice = resolve_lnurl_to_invoice(&mock_http_client, &lnurl, amount).await; - assert!(invoice.is_ok()); + let result = resolve_lnurl_to_invoice(&mock_http_client, &lnurl, amount, None).await; + assert!(result.is_ok()); } - #[tokio::test] async fn test_lnurl_pay_with_lightning_address_fails_with_empty_username() { let mock_http_client = MockLnUrlHttpClient::new(); - let lightning_address_username = ""; - let lightning_address_domain = "cipherpunk.com"; - let lnurl = format!( - "{}@{}", - lightning_address_username, lightning_address_domain - ); - - let amount = 100000; - - let error = resolve_lnurl_to_invoice(&mock_http_client, &lnurl, amount).await; - assert!(error.is_err()); - assert!(error.unwrap_err().to_string().contains("Username can not be empty")); + let lnurl = "@cipherpunk.com"; + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 100000, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Username can not be empty")); } #[tokio::test] async fn test_lnurl_pay_with_lightning_address_fails_with_empty_domain() { let mock_http_client = MockLnUrlHttpClient::new(); - let lightning_address_username = "satoshi"; - let lightning_address_domain = ""; - let lnurl = format!( - "{}@{}", - lightning_address_username, lightning_address_domain - ); - - let amount = 100000; - - let error = resolve_lnurl_to_invoice(&mock_http_client, &lnurl, amount).await; - assert!(error.is_err()); - assert!(error.unwrap_err().to_string().contains("Domain can not be empty")); + let lnurl = "satoshi@"; + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 100000, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Domain can not be empty")); } #[tokio::test] async fn test_lnurl_pay_returns_error_on_invalid_lnurl() { let mock_http_client = MockLnUrlHttpClient::new(); - let lnurl = "LNURL1111111111111111111111111111111111111111111111111111111111111111111"; - let amount = 100000; - let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; - - match result { - Err(err) => { - assert!(err - .to_string() - .contains("Failed to decode lnurl: invalid length")); - } - _ => panic!("Expected an error, but got Ok"), - } + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 100000, None).await; + assert!(result.unwrap_err().to_string().contains("Failed to decode lnurl: invalid length")); } #[tokio::test] async fn test_lnurl_pay_returns_error_on_amount_less_than_min_sendable() { let mut mock_http_client = MockLnUrlHttpClient::new(); - // Set up expectations for the first two calls mock_http_client.expect_get_pay_request_response().returning(|_url| { let x: PayRequestResponse = serde_json::from_str("{ \"callback\": \"https://cipherpunk.com/lnurlp/api/v1/lnurl/cb/1\", \"maxSendable\": 100000, \"minSendable\": 100000, \"tag\": \"payRequest\", \"metadata\": \"[[\\\"text/plain\\\", \\\"Start the CoinTrain\\\"]]\" }").unwrap(); convert_to_async_return_value(Ok(x)) }); - mock_http_client.expect_get_pay_request_callback_response().returning(|_url| { - let invoice = "lnbc1u1pjv9qrvsp5e5wwexctzp9yklcrzx448c68q2a7kma55cm67ruajjwfkrswnqvqpp55x6mmz8ch6nahrcuxjsjvs23xkgt8eu748nukq463zhjcjk4s65shp5dd6hc533r655wtyz63jpf6ja08srn6rz6cjhwsjuyckrqwanhjtsxqzjccqpjrzjqw6lfdpjecp4d5t0gxk5khkrzfejjxyxtxg5exqsd95py6rhwwh72rpgrgqq3hcqqgqqqqlgqqqqqqgq9q9qxpqysgq95njz4sz6h7r2qh7txnevcrvg0jdsfpe72cecmjfka8mw5nvm7tydd0j34ps2u9q9h6v5u8h3vxs8jqq5fwehdda6a8qmpn93fm290cquhuc6r"; - let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string(); - let callback_response = serde_json::from_str(&callback_response_json).unwrap(); - convert_to_async_return_value(Ok(callback_response)) - }); - let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; - let amount = 1; - - let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; - - match result { - Err(err) => { - assert!(err.to_string().contains("Amount must be")); - } - _ => panic!("Expected an error, but got Ok"), - } + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 1, None).await; + assert!(result.unwrap_err().to_string().contains("Amount must be")); } #[tokio::test] @@ -385,23 +383,61 @@ mod tests { convert_to_async_return_value(Ok(x)) }); - mock_http_client.expect_get_pay_request_callback_response().returning(|_url| { - let invoice = "lnbc1u1pjv9qrvsp5e5wwexctzp9yklcrzx448c68q2a7kma55cm67ruajjwfkrswnqvqpp55x6mmz8ch6nahrcuxjsjvs23xkgt8eu748nukq463zhjcjk4s65shp5dd6hc533r655wtyz63jpf6ja08srn6rz6cjhwsjuyckrqwanhjtsxqzjccqpjrzjqw6lfdpjecp4d5t0gxk5khkrzfejjxyxtxg5exqsd95py6rhwwh72rpgrgqq3hcqqgqqqqlgqqqqqqgq9q9qxpqysgq95njz4sz6h7r2qh7txnevcrvg0jdsfpe72cecmjfka8mw5nvm7tydd0j34ps2u9q9h6v5u8h3vxs8jqq5fwehdda6a8qmpn93fm290cquhuc6r"; - let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string(); - let value = serde_json::from_str(&callback_response_json).unwrap(); - convert_to_async_return_value(Ok(value)) - }); - let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; - let amount = 1; + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 200000, None).await; + assert!(result.unwrap_err().to_string().contains("Amount must be")); + } - let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; + #[test] + fn test_aes_decrypt_known_vector() { + use aes::Aes256; + use base64::{engine::general_purpose::STANDARD, Engine}; + use cbc::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit}; + + let key = [0x42u8; 32]; + let iv = [0x24u8; 16]; + let plaintext = b"hello world"; + + // Encrypt + type Aes256CbcEnc = cbc::Encryptor; + let ciphertext = Aes256CbcEnc::new_from_slices(&key, &iv) + .unwrap() + .encrypt_padded_vec_mut::(plaintext); + + let ciphertext_b64 = STANDARD.encode(&ciphertext); + let iv_b64 = STANDARD.encode(&iv); + + // Decrypt + let result = decrypt_aes_success_action(&key, &ciphertext_b64, &iv_b64).unwrap(); + assert_eq!(result, "hello world"); + } - match result { - Err(err) => { - assert!(err.to_string().contains("Amount must be")); - } - _ => panic!("Expected an error, amount specified is greater than maxSendable"), - } + #[test] + fn test_aes_decrypt_wrong_preimage_length() { + let result = decrypt_aes_success_action(&[0u8; 16], "YWJj", "MTIzNDU2Nzg5MDEyMzQ1Ng=="); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("32 bytes")); + } + + #[test] + fn test_pay_request_description() { + let resp: PayRequestResponse = serde_json::from_str( + r#"{"callback":"https://x.com/cb","maxSendable":1000,"minSendable":1,"tag":"payRequest","metadata":"[[\"text/plain\",\"Buy coffee\"]]"}"# + ).unwrap(); + assert_eq!(resp.description(), Some("Buy coffee".to_string())); + } + + #[test] + fn test_pay_request_validate_amount_range() { + let resp: PayRequestResponse = serde_json::from_str( + r#"{"callback":"https://x.com/cb","maxSendable":10000,"minSendable":1000,"tag":"payRequest","metadata":"[[\"text/plain\",\"test\"]]"}"# + ).unwrap(); + + // In range + assert!(resp.validate("LNURL1TEST", 5000).is_ok()); + // Below min + assert!(resp.validate("LNURL1TEST", 500).is_err()); + // Above max + assert!(resp.validate("LNURL1TEST", 20000).is_err()); } } diff --git a/libs/gl-client/src/lnurl/utils.rs b/libs/gl-client/src/lnurl/utils.rs index b8b73818c..744b6949e 100644 --- a/libs/gl-client/src/lnurl/utils.rs +++ b/libs/gl-client/src/lnurl/utils.rs @@ -1,11 +1,11 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; -use bech32::FromBase32; +use bech32::{FromBase32, ToBase32}; use crate::lightning_invoice::Bolt11Invoice; -// Function to decode and parse the lnurl into a URL +/// Decode an LNURL bech32 string into the underlying URL (LUD-01). pub fn parse_lnurl(lnurl: &str) -> Result { let (_hrp, data, _variant) = bech32::decode(lnurl).map_err(|e| anyhow!("Failed to decode lnurl: {}", e))?; @@ -17,7 +17,88 @@ pub fn parse_lnurl(lnurl: &str) -> Result { Ok(url) } -// Get an Invoice from a Lightning Network URL pay request +/// Encode a URL as an LNURL bech32 string (LUD-01). +/// +/// Returns uppercase by convention (for QR code compatibility). +pub fn lnurl_encode(url: &str) -> Result { + let data = url.as_bytes().to_base32(); + bech32::encode("lnurl", data, bech32::Variant::Bech32) + .map(|s| s.to_uppercase()) + .map_err(|e| anyhow!("Failed to encode lnurl: {}", e)) +} + +/// Extract the "text/plain" description from LNURL metadata JSON. +/// +/// Metadata is a JSON array of `["mime", "content"]` pairs. +/// Returns the content of the first "text/plain" entry, or None. +pub fn extract_description_from_metadata(metadata: &str) -> Option { + let entries: Vec> = serde_json::from_str(metadata).ok()?; + for entry in entries { + if entry.len() >= 2 && entry[0] == "text/plain" { + return Some(entry[1].clone()); + } + } + None +} + +/// Parse a BOLT11 invoice string. pub fn parse_invoice(invoice_str: &str) -> Result { - Bolt11Invoice::from_str(&invoice_str).map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e))) + Bolt11Invoice::from_str(invoice_str) + .map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lnurl_encode_decode_roundtrip() { + let url = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b7e86a0850"; + let encoded = lnurl_encode(url).unwrap(); + assert!(encoded.starts_with("LNURL1")); + let decoded = parse_lnurl(&encoded).unwrap(); + assert_eq!(decoded, url); + } + + #[test] + fn test_lnurl_decode_is_case_insensitive() { + let url = "https://example.com/lnurl"; + let encoded = lnurl_encode(url).unwrap(); + // Uppercase (default) should work + let decoded = parse_lnurl(&encoded).unwrap(); + assert_eq!(decoded, url); + // Lowercase should also work + let decoded = parse_lnurl(&encoded.to_lowercase()).unwrap(); + assert_eq!(decoded, url); + } + + #[test] + fn test_extract_description_from_metadata() { + let metadata = r#"[["text/plain", "Pay to example"]]"#; + assert_eq!( + extract_description_from_metadata(metadata), + Some("Pay to example".to_string()) + ); + } + + #[test] + fn test_extract_description_from_metadata_with_multiple_entries() { + let metadata = + r#"[["text/identifier", "user@example.com"], ["text/plain", "Pay user"]]"#; + assert_eq!( + extract_description_from_metadata(metadata), + Some("Pay user".to_string()) + ); + } + + #[test] + fn test_extract_description_from_metadata_missing() { + let metadata = r#"[["text/identifier", "user@example.com"]]"#; + assert_eq!(extract_description_from_metadata(metadata), None); + } + + #[test] + fn test_extract_description_from_metadata_invalid_json() { + assert_eq!(extract_description_from_metadata("not json"), None); + } } diff --git a/libs/gl-client/src/lnurl/withdraw/mod.rs b/libs/gl-client/src/lnurl/withdraw/mod.rs index 5f1e85747..5b2295f4a 100644 --- a/libs/gl-client/src/lnurl/withdraw/mod.rs +++ b/libs/gl-client/src/lnurl/withdraw/mod.rs @@ -1,18 +1,27 @@ use super::models::WithdrawRequestResponse; -use anyhow::{anyhow, Result}; +use anyhow::Result; use log::debug; use reqwest::Url; use serde_json::{to_value, Map, Value}; -pub fn build_withdraw_request_callback_url( - lnurl_pay_request_response: &WithdrawRequestResponse, - invoice: String, -) -> Result { - let mut url = Url::parse(&lnurl_pay_request_response.callback)?; - url.query_pairs_mut() - .append_pair("k1", &lnurl_pay_request_response.k1) - .append_pair("pr", &invoice); +impl WithdrawRequestResponse { + /// Build the callback URL for submitting an invoice to the service. + /// + /// Appends `k1` and `pr` (the BOLT11 invoice) as query parameters. + pub fn build_callback_url(&self, invoice: &str) -> Result { + build_withdraw_callback_url(&self.callback, &self.k1, invoice) + } +} +/// Build a withdraw callback URL from its individual components. +/// +/// Appends `k1` and `pr` (the BOLT11 invoice) as query parameters +/// to the callback base URL. +pub fn build_withdraw_callback_url(callback: &str, k1: &str, invoice: &str) -> Result { + let mut url = Url::parse(callback)?; + url.query_pairs_mut() + .append_pair("k1", k1) + .append_pair("pr", invoice); Ok(url.to_string()) } @@ -24,17 +33,18 @@ fn convert_value_field_from_str_to_u64( Some(field_value) => match field_value.as_str() { Some(field_value_str) => { let converted_field_value = field_value_str.parse::()?; - - //overwrites old type value value.insert( String::from(field_name), to_value(converted_field_value).unwrap(), ); - return Ok(()); + Ok(()) } - None => return Err(anyhow!("Failed to convert {} into a str", field_name)), + None => Err(anyhow::anyhow!( + "Failed to convert {} into a str", + field_name + )), }, - None => return Err(anyhow!("Failed to find {} in map", field_name)), + None => Err(anyhow::anyhow!("Failed to find {} in map", field_name)), } } @@ -56,7 +66,7 @@ pub fn parse_withdraw_request_response_from_url(url: &str) -> Option { return w; - }, + } Err(e) => { debug!("{:?}", e); return None; @@ -73,25 +83,25 @@ mod test { #[test] fn test_build_withdraw_request_callback_url() -> Result<()> { + let resp = WithdrawRequestResponse { + tag: String::from("withdraw"), + callback: String::from("https://cipherpunk.com/"), + k1: String::from("unique"), + default_description: String::from(""), + min_withdrawable: 2, + max_withdrawable: 300, + }; - let k1 = String::from("unique"); - let invoice = String::from("invoice"); - - let built_withdraw_request_callback_url = build_withdraw_request_callback_url(&WithdrawRequestResponse { - tag: String::from("withdraw"), - callback: String::from("https://cipherpunk.com/"), - k1: k1.clone(), - default_description: String::from(""), - min_withdrawable: 2, - max_withdrawable: 300, - }, invoice.clone()); - - let url = Url::parse(&built_withdraw_request_callback_url.unwrap())?; + let url_str = resp.build_callback_url("invoice")?; + let url = Url::parse(&url_str)?; let query_pairs = url.query_pairs().collect::(); let query_params: &Map = query_pairs.as_object().unwrap(); - - assert_eq!(query_params.get("k1").unwrap().as_str().unwrap(), k1); - assert_eq!(query_params.get("pr").unwrap().as_str().unwrap(), invoice); + + assert_eq!(query_params.get("k1").unwrap().as_str().unwrap(), "unique"); + assert_eq!( + query_params.get("pr").unwrap().as_str().unwrap(), + "invoice" + ); Ok(()) } diff --git a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LnurlParseTest.kt b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LnurlParseTest.kt new file mode 100644 index 000000000..fb59a2bbb --- /dev/null +++ b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LnurlParseTest.kt @@ -0,0 +1,35 @@ +// Instrumented tests for parse_input() error cases on LNURL / +// Lightning Address inputs. `parse_input` is offline — these all +// reject before any HTTP would be attempted. Successful HTTP +// resolution is covered by `resolveInput` in gl-testing integration +// tests against a live LNURL fixture. + +package com.blockstream.glsdk + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LnurlParseTest { + + @Test(expected = Exception::class) + fun parse_invalid_lnurl_bech32_returns_error() { + parseInput("LNURL1INVALIDDATA") + } + + @Test(expected = Exception::class) + fun parse_lightning_address_no_dot_in_domain_returns_error() { + parseInput("user@localhost") + } + + @Test(expected = Exception::class) + fun parse_lightning_address_empty_local_part_returns_error() { + parseInput("@example.com") + } + + @Test(expected = Exception::class) + fun parse_lightning_address_empty_domain_returns_error() { + parseInput("user@") + } +} diff --git a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ParseInputTest.kt b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ParseInputTest.kt index eb27f57d0..c0d6ca8ba 100644 --- a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ParseInputTest.kt +++ b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ParseInputTest.kt @@ -1,5 +1,6 @@ -// Instrumented tests for parse_input(). -// Tests BOLT11 invoice parsing, node ID parsing, and error cases. +// Instrumented tests for the synchronous parse_input(). +// `parse_input` is offline — no HTTP, no I/O. LNURL HTTP resolution +// is `resolveInput` and is covered by gl-testing integration tests. package com.blockstream.glsdk @@ -22,6 +23,10 @@ class ParseInputTest { "d2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy" + "22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz" + // Bech32-encoded "https://service.com/lnurl" + private val lnurlBech32 = + "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2" + // ============================================================ // Node ID parsing // ============================================================ @@ -29,7 +34,7 @@ class ParseInputTest { @Test fun parse_valid_node_id() { val result = parseInput(validNodeId) - assertNotNull(result) + assertTrue("Expected NodeId, got $result", result is ParsedInput.NodeId) } @Test(expected = Exception::class) @@ -49,19 +54,38 @@ class ParseInputTest { @Test fun parse_valid_bolt11() { val result = parseInput(bolt11Invoice) - assertNotNull(result) + assertTrue("Expected Bolt11, got $result", result is ParsedInput.Bolt11) } @Test fun parse_bolt11_with_lightning_prefix() { val result = parseInput("lightning:$bolt11Invoice") - assertNotNull(result) + assertTrue("Expected Bolt11, got $result", result is ParsedInput.Bolt11) } @Test fun parse_bolt11_with_uppercase_prefix() { val result = parseInput("LIGHTNING:$bolt11Invoice") - assertNotNull(result) + assertTrue("Expected Bolt11, got $result", result is ParsedInput.Bolt11) + } + + // ============================================================ + // LNURL bech32 / Lightning Address — offline classification + // ============================================================ + + @Test + fun parse_lnurl_bech32_decodes_url() { + val result = parseInput(lnurlBech32) + assertTrue("Expected LnUrl, got $result", result is ParsedInput.LnUrl) + val url = (result as ParsedInput.LnUrl).url + assertTrue("Expected decoded https URL, got $url", url.startsWith("https://")) + } + + @Test + fun parse_lightning_address() { + val result = parseInput("user@example.com") + assertTrue("Expected LnUrlAddress, got $result", result is ParsedInput.LnUrlAddress) + assertEquals("user@example.com", (result as ParsedInput.LnUrlAddress).address) } // ============================================================ diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index e01bf6701..3e0749cff 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -10,11 +10,14 @@ use glsdk::{ Credentials as GlCredentials, DeveloperCert as GlDeveloperCert, Handle as GlHandle, + ParsedInput as GlParsedInput, + ResolvedInput as GlResolvedInput, Network as GlNetwork, Node as GlNode, NodeEvent as GlNodeEvent, NodeEventStream as GlNodeEventStream, OutputStatus as GlOutputStatus, + ParsedInvoice as GlParsedInvoice, Scheduler as GlScheduler, Signer as GlSigner, }; @@ -246,6 +249,167 @@ pub struct FundChannel { pub channel_id: Option, } +// ============================================================================ +// Input Parsing Types +// ============================================================================ + +#[napi(object)] +pub struct ParsedInvoice { + pub bolt11: String, + pub payee_pubkey: Option, + pub payment_hash: Buffer, + pub description: Option, + /// Amount in millisatoshis (i64 for JS), `None` for any-amount invoices. + pub amount_msat: Option, + /// Seconds from creation until the invoice expires. + pub expiry: i64, + /// Unix timestamp (seconds) when the invoice was created. + pub timestamp: i64, +} + +/// Result of `parseInput` — offline classification, no HTTP. +/// Discriminated by `type`. LNURL bech32 strings come back as their +/// decoded URL; Lightning Addresses as the unparsed `user@host` form. +#[napi(object)] +pub struct ParsedInput { + /// "bolt11" | "node_id" | "lnurl" | "lnurl_address" + pub r#type: String, + /// Present when type == "bolt11" + pub bolt11: Option, + /// Present when type == "node_id" + pub node_id: Option, + /// Present when type == "lnurl" — the decoded URL of the LNURL. + pub lnurl: Option, + /// Present when type == "lnurl_address" — `user@host` form. + pub lnurl_address: Option, +} + +/// Result of `resolveInput` — fully-resolved, may have performed HTTP. +/// Discriminated by `type`. Exactly one of the variant fields +/// (`bolt11`, `node_id`, `lnurl_pay`, `lnurl_withdraw`) is populated. +#[napi(object)] +pub struct ResolvedInput { + /// "bolt11" | "node_id" | "lnurl_pay" | "lnurl_withdraw" + pub r#type: String, + /// Present when type == "bolt11" + pub bolt11: Option, + /// Present when type == "node_id" + pub node_id: Option, + /// Present when type == "lnurl_pay" + pub lnurl_pay: Option, + /// Present when type == "lnurl_withdraw" + pub lnurl_withdraw: Option, +} + +// ============================================================================ +// LNURL Types +// ============================================================================ + +#[napi(object)] +pub struct LnUrlPayRequestData { + pub callback: String, + /// Minimum amount in millisatoshis (i64 for JS) + pub min_sendable: i64, + /// Maximum amount in millisatoshis (i64 for JS) + pub max_sendable: i64, + pub metadata: String, + pub comment_allowed: i64, + pub description: String, + pub lnurl: String, +} + +#[napi(object)] +pub struct LnUrlWithdrawRequestData { + pub callback: String, + pub k1: String, + pub default_description: String, + /// Minimum withdrawable in millisatoshis (i64 for JS) + pub min_withdrawable: i64, + /// Maximum withdrawable in millisatoshis (i64 for JS) + pub max_withdrawable: i64, + pub lnurl: String, +} + +#[napi(object)] +pub struct LnUrlPayRequest { + pub data: LnUrlPayRequestData, + /// Amount in millisatoshis (i64 for JS) + pub amount_msat: i64, + pub comment: Option, + /// When true (the default), a URL success action is rejected if its + /// domain differs from the callback's domain. + pub validate_success_action_url: Option, +} + +#[napi(object)] +pub struct LnUrlWithdrawRequest { + pub data: LnUrlWithdrawRequestData, + /// Amount in millisatoshis (i64 for JS) + pub amount_msat: i64, + pub description: Option, +} + +#[napi(object)] +pub struct LnUrlPaySuccessData { + pub payment_preimage: String, + pub success_action: Option, +} + +#[napi(object)] +pub struct LnUrlErrorData { + pub reason: String, +} + +#[napi(object)] +pub struct LnUrlPayErrorData { + pub payment_hash: String, + pub reason: String, +} + +/// Result of an LNURL-pay operation. Discriminated by `type` field. +#[napi(object)] +pub struct LnUrlPayResult { + /// "success", "error", or "pay_error" + pub r#type: String, + /// Present when type == "success" + pub success: Option, + /// Present when type == "error" (LNURL service rejected the request) + pub error: Option, + /// Present when type == "pay_error" (invoice fetched but paying it failed) + pub pay_error: Option, +} + +#[napi(object)] +pub struct LnUrlWithdrawSuccessData { + pub invoice: String, +} + +/// Result of an LNURL-withdraw operation. Discriminated by `type` field. +#[napi(object)] +pub struct LnUrlWithdrawResult { + /// "ok" or "error" + pub r#type: String, + /// Present when type == "ok" + pub ok: Option, + /// Present when type == "error" + pub error: Option, +} + +/// Processed success action. Discriminated by `type` field. +#[napi(object)] +pub struct SuccessActionProcessed { + /// "message", "url", or "aes" + pub r#type: String, + /// Present for "message" type + pub message: Option, + /// Present for "url" type + pub description: Option, + /// Present for "url" type + pub url: Option, + /// Present for "aes" type (decrypted plaintext) + pub plaintext: Option, +} + // ============================================================================ // Struct Definitions (all structs must be defined before impl blocks) // ============================================================================ @@ -852,6 +1016,46 @@ impl Node { .collect(), }) } + + // ── LNURL methods ─────────────────────────────────────────── + + /// Execute an LNURL-pay flow. + /// + /// Build the request from `LnUrlPayRequestData` (obtained out of + /// band) and a chosen amount. + #[napi] + pub async fn lnurl_pay(&self, request: LnUrlPayRequest) -> Result { + let inner = self.inner.clone(); + let gl_request = gl_lnurl_pay_request_from_napi(request); + let result = tokio::task::spawn_blocking(move || { + inner + .lnurl_pay(gl_request) + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; + + Ok(napi_lnurl_pay_result_from_gl(result)) + } + + /// Execute an LNURL-withdraw flow. + /// + /// Build the request from `LnUrlWithdrawRequestData` (obtained out + /// of band) and a chosen amount. + #[napi] + pub async fn lnurl_withdraw(&self, request: LnUrlWithdrawRequest) -> Result { + let inner = self.inner.clone(); + let gl_request = gl_lnurl_withdraw_request_from_napi(request); + let result = tokio::task::spawn_blocking(move || { + inner + .lnurl_withdraw(gl_request) + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; + + Ok(napi_lnurl_withdraw_result_from_gl(result)) + } } // ============================================================================ @@ -901,3 +1105,262 @@ fn output_status_to_string(status: &GlOutputStatus) -> String { GlOutputStatus::Immature => "immature".to_string(), } } + +// ============================================================================ +// Input Parsing Conversion Helpers +// ============================================================================ + +fn napi_parsed_invoice_from_gl(invoice: GlParsedInvoice) -> ParsedInvoice { + ParsedInvoice { + bolt11: invoice.bolt11, + payee_pubkey: invoice.payee_pubkey.map(Buffer::from), + payment_hash: Buffer::from(invoice.payment_hash), + description: invoice.description, + amount_msat: invoice.amount_msat.map(|v| v as i64), + expiry: invoice.expiry as i64, + timestamp: invoice.timestamp as i64, + } +} + +fn napi_pay_request_data_from_gl(data: glsdk::LnUrlPayRequestData) -> LnUrlPayRequestData { + LnUrlPayRequestData { + callback: data.callback, + min_sendable: data.min_sendable as i64, + max_sendable: data.max_sendable as i64, + metadata: data.metadata, + comment_allowed: data.comment_allowed as i64, + description: data.description, + lnurl: data.lnurl, + } +} + +fn napi_withdraw_request_data_from_gl( + data: glsdk::LnUrlWithdrawRequestData, +) -> LnUrlWithdrawRequestData { + LnUrlWithdrawRequestData { + callback: data.callback, + k1: data.k1, + default_description: data.default_description, + min_withdrawable: data.min_withdrawable as i64, + max_withdrawable: data.max_withdrawable as i64, + lnurl: data.lnurl, + } +} + +fn napi_parsed_input_from_gl(input: GlParsedInput) -> ParsedInput { + match input { + GlParsedInput::Bolt11 { invoice } => ParsedInput { + r#type: "bolt11".to_string(), + bolt11: Some(napi_parsed_invoice_from_gl(invoice)), + node_id: None, + lnurl: None, + lnurl_address: None, + }, + GlParsedInput::NodeId { node_id } => ParsedInput { + r#type: "node_id".to_string(), + bolt11: None, + node_id: Some(node_id), + lnurl: None, + lnurl_address: None, + }, + GlParsedInput::LnUrl { url } => ParsedInput { + r#type: "lnurl".to_string(), + bolt11: None, + node_id: None, + lnurl: Some(url), + lnurl_address: None, + }, + GlParsedInput::LnUrlAddress { address } => ParsedInput { + r#type: "lnurl_address".to_string(), + bolt11: None, + node_id: None, + lnurl: None, + lnurl_address: Some(address), + }, + } +} + +fn napi_resolved_input_from_gl(input: GlResolvedInput) -> ResolvedInput { + match input { + GlResolvedInput::Bolt11 { invoice } => ResolvedInput { + r#type: "bolt11".to_string(), + bolt11: Some(napi_parsed_invoice_from_gl(invoice)), + node_id: None, + lnurl_pay: None, + lnurl_withdraw: None, + }, + GlResolvedInput::NodeId { node_id } => ResolvedInput { + r#type: "node_id".to_string(), + bolt11: None, + node_id: Some(node_id), + lnurl_pay: None, + lnurl_withdraw: None, + }, + GlResolvedInput::LnUrlPay { data } => ResolvedInput { + r#type: "lnurl_pay".to_string(), + bolt11: None, + node_id: None, + lnurl_pay: Some(napi_pay_request_data_from_gl(data)), + lnurl_withdraw: None, + }, + GlResolvedInput::LnUrlWithdraw { data } => ResolvedInput { + r#type: "lnurl_withdraw".to_string(), + bolt11: None, + node_id: None, + lnurl_pay: None, + lnurl_withdraw: Some(napi_withdraw_request_data_from_gl(data)), + }, + } +} + +/// Synchronously classify the input. **No HTTP, no I/O.** +/// +/// Recognises BOLT11 invoices, node IDs, LNURL bech32 strings, and +/// Lightning Addresses. Strips `lightning:` / `LIGHTNING:` prefixes +/// automatically. LNURL inputs are decoded to their underlying URL +/// but **not fetched** — call `resolveInput` for that. +#[napi] +pub fn parse_input(input: String) -> Result { + let parsed = + glsdk::parse_input(input).map_err(|e| Error::from_reason(e.to_string()))?; + Ok(napi_parsed_input_from_gl(parsed)) +} + +/// Asynchronously classify and resolve the input. +/// +/// Internally calls `parseInput`. For LNURL bech32 strings and +/// Lightning Addresses performs the HTTP GET to the endpoint and +/// returns typed pay or withdraw request data. For BOLT11 invoices +/// and node IDs returns immediately without I/O. +#[napi] +pub async fn resolve_input(input: String) -> Result { + let resolved = glsdk::resolve_input(input) + .await + .map_err(|e| Error::from_reason(e.to_string()))?; + Ok(napi_resolved_input_from_gl(resolved)) +} + +// ============================================================================ +// LNURL Conversion Helpers +// ============================================================================ + +fn gl_pay_request_data_from_napi(data: LnUrlPayRequestData) -> glsdk::LnUrlPayRequestData { + glsdk::LnUrlPayRequestData { + callback: data.callback, + min_sendable: data.min_sendable as u64, + max_sendable: data.max_sendable as u64, + metadata: data.metadata, + comment_allowed: data.comment_allowed as u64, + description: data.description, + lnurl: data.lnurl, + } +} + +fn gl_withdraw_request_data_from_napi( + data: LnUrlWithdrawRequestData, +) -> glsdk::LnUrlWithdrawRequestData { + glsdk::LnUrlWithdrawRequestData { + callback: data.callback, + k1: data.k1, + default_description: data.default_description, + min_withdrawable: data.min_withdrawable as u64, + max_withdrawable: data.max_withdrawable as u64, + lnurl: data.lnurl, + } +} + +fn gl_lnurl_pay_request_from_napi(req: LnUrlPayRequest) -> glsdk::LnUrlPayRequest { + glsdk::LnUrlPayRequest { + data: gl_pay_request_data_from_napi(req.data), + amount_msat: req.amount_msat as u64, + comment: req.comment, + validate_success_action_url: req.validate_success_action_url, + } +} + +fn gl_lnurl_withdraw_request_from_napi(req: LnUrlWithdrawRequest) -> glsdk::LnUrlWithdrawRequest { + glsdk::LnUrlWithdrawRequest { + data: gl_withdraw_request_data_from_napi(req.data), + amount_msat: req.amount_msat as u64, + description: req.description, + } +} + +fn napi_success_action_from_gl(action: glsdk::SuccessActionProcessed) -> SuccessActionProcessed { + match action { + glsdk::SuccessActionProcessed::Message { message } => SuccessActionProcessed { + r#type: "message".to_string(), + message: Some(message), + description: None, + url: None, + plaintext: None, + }, + glsdk::SuccessActionProcessed::Url { description, url } => SuccessActionProcessed { + r#type: "url".to_string(), + message: None, + description: Some(description), + url: Some(url), + plaintext: None, + }, + glsdk::SuccessActionProcessed::Aes { + description, + plaintext, + } => SuccessActionProcessed { + r#type: "aes".to_string(), + message: None, + description: Some(description), + url: None, + plaintext: Some(plaintext), + }, + } +} + +fn napi_lnurl_pay_result_from_gl(result: glsdk::LnUrlPayResult) -> LnUrlPayResult { + match result { + glsdk::LnUrlPayResult::EndpointSuccess { data } => LnUrlPayResult { + r#type: "success".to_string(), + success: Some(LnUrlPaySuccessData { + payment_preimage: data.payment_preimage, + success_action: data.success_action.map(napi_success_action_from_gl), + }), + error: None, + pay_error: None, + }, + glsdk::LnUrlPayResult::EndpointError { data } => LnUrlPayResult { + r#type: "error".to_string(), + success: None, + error: Some(LnUrlErrorData { + reason: data.reason, + }), + pay_error: None, + }, + glsdk::LnUrlPayResult::PayError { data } => LnUrlPayResult { + r#type: "pay_error".to_string(), + success: None, + error: None, + pay_error: Some(LnUrlPayErrorData { + payment_hash: data.payment_hash, + reason: data.reason, + }), + }, + } +} + +fn napi_lnurl_withdraw_result_from_gl(result: glsdk::LnUrlWithdrawResult) -> LnUrlWithdrawResult { + match result { + glsdk::LnUrlWithdrawResult::Ok { data } => LnUrlWithdrawResult { + r#type: "ok".to_string(), + ok: Some(LnUrlWithdrawSuccessData { + invoice: data.invoice, + }), + error: None, + }, + glsdk::LnUrlWithdrawResult::ErrorStatus { data } => LnUrlWithdrawResult { + r#type: "error".to_string(), + ok: None, + error: Some(LnUrlErrorData { + reason: data.reason, + }), + }, + } +} diff --git a/libs/gl-sdk-napi/tests/parse-input.spec.ts b/libs/gl-sdk-napi/tests/parse-input.spec.ts new file mode 100644 index 000000000..146566575 --- /dev/null +++ b/libs/gl-sdk-napi/tests/parse-input.spec.ts @@ -0,0 +1,99 @@ +import { parseInput } from '../index.js'; + +const VALID_NODE_ID = + '02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'; + +const BOLT11_INVOICE = + 'lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhm' + + 'nsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhh' + + 'd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy' + + '22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz'; + +const VALID_LNURL_BECH32 = + 'LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2'; + +describe('parseInput (synchronous, offline)', () => { + describe('BOLT11 invoices', () => { + it('classifies a valid BOLT11 invoice', () => { + const result = parseInput(BOLT11_INVOICE); + expect(result.type).toBe('bolt11'); + expect(result.bolt11).toBeDefined(); + expect(result.bolt11!.bolt11).toBe(BOLT11_INVOICE); + }); + + it('strips a lowercase lightning: prefix', () => { + const result = parseInput(`lightning:${BOLT11_INVOICE}`); + expect(result.type).toBe('bolt11'); + }); + + it('strips an uppercase LIGHTNING: prefix', () => { + const result = parseInput(`LIGHTNING:${BOLT11_INVOICE}`); + expect(result.type).toBe('bolt11'); + }); + }); + + describe('node IDs', () => { + it('classifies a valid compressed pubkey', () => { + const result = parseInput(VALID_NODE_ID); + expect(result.type).toBe('node_id'); + expect(result.nodeId).toBe(VALID_NODE_ID); + }); + + it('rejects a 66-char string that is not valid hex', () => { + expect(() => + parseInput('not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx'), + ).toThrow(); + }); + + it('rejects an uncompressed (0x04) pubkey', () => { + expect(() => + parseInput('04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), + ).toThrow(); + }); + }); + + describe('LNURL bech32 / Lightning Address', () => { + it('decodes an LNURL bech32 to its underlying URL', () => { + const result = parseInput(VALID_LNURL_BECH32); + expect(result.type).toBe('lnurl'); + expect(result.lnurl).toBeDefined(); + expect(result.lnurl!.startsWith('https://')).toBe(true); + }); + + it('returns a Lightning Address as the user@host form', () => { + const result = parseInput('user@example.com'); + expect(result.type).toBe('lnurl_address'); + expect(result.lnurlAddress).toBe('user@example.com'); + }); + }); + + describe('error cases', () => { + it('rejects empty input', () => { + expect(() => parseInput('')).toThrow(); + }); + + it('rejects whitespace-only input', () => { + expect(() => parseInput(' ')).toThrow(); + }); + + it('rejects unrecognized garbage', () => { + expect(() => parseInput('hello world')).toThrow(); + }); + + it('rejects an invalid LNURL bech32 string', () => { + expect(() => parseInput('LNURL1INVALIDDATA')).toThrow(); + }); + + it('rejects a malformed Lightning Address (no dot in domain)', () => { + expect(() => parseInput('user@localhost')).toThrow(); + }); + + it('rejects an empty local-part Lightning Address', () => { + expect(() => parseInput('@example.com')).toThrow(); + }); + + it('rejects an empty domain Lightning Address', () => { + expect(() => parseInput('user@')).toThrow(); + }); + }); +}); diff --git a/libs/gl-sdk-swift b/libs/gl-sdk-swift index 39910590d..d834249c6 160000 --- a/libs/gl-sdk-swift +++ b/libs/gl-sdk-swift @@ -1 +1 @@ -Subproject commit 39910590da8b344845ce56f8203de533b5db77f2 +Subproject commit d834249c6cd3642d1f07ac27be6a746d58038a32 diff --git a/libs/gl-sdk/.gitignore b/libs/gl-sdk/.gitignore new file mode 100644 index 000000000..1bf662cdb --- /dev/null +++ b/libs/gl-sdk/.gitignore @@ -0,0 +1,5 @@ +# UniFFI-generated build artifacts +glsdk/glsdk.py +glsdk/libglsdk.* +glsdk/__pycache__/ +bindings/ diff --git a/libs/gl-sdk/CHANGELOG.md b/libs/gl-sdk/CHANGELOG.md index 26b1fab85..e4d4ee890 100644 --- a/libs/gl-sdk/CHANGELOG.md +++ b/libs/gl-sdk/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +### Added + +- `parse_input()` — synchronous, offline classification of user input. Recognises BOLT11 invoices, node IDs, LNURL bech32 strings (decoded to their underlying URL), and Lightning Addresses (returned as the unparsed `user@host` form). LNURL inputs are classified but not fetched; the cost contract is "no HTTP, no I/O." Use this for clipboard validation, invoice sanity checks, and any other path that must not touch the network. +- `resolve_input()` — asynchronous, network-touching classification. Internally calls `parse_input()`, then for the LNURL / Lightning Address branches performs the HTTP GET to the service endpoint and returns typed pay or withdraw request data. BOLT11 invoices and node IDs pass through without I/O. Use this for the QR-scan flow that should proceed straight to a pay/withdraw screen. +- `ParsedInput` enum (offline result): `Bolt11`, `NodeId`, `LnUrl { url }`, `LnUrlAddress { address }`. +- `ResolvedInput` enum (resolved result): `Bolt11`, `NodeId`, `LnUrlPay { data }`, `LnUrlWithdraw { data }`. + +### Removed + +- `Node::resolve_lnurl()` and the `ResolvedLnUrl` enum. Use `parse_input()` (offline) or `resolve_input()` (HTTP) to obtain `LnUrlPayRequestData` / `LnUrlWithdrawRequestData`, then call `Node::lnurl_pay()` / `Node::lnurl_withdraw()`. + ## [0.2.0] - 2026-04-02 ### Added diff --git a/libs/gl-sdk/Cargo.toml b/libs/gl-sdk/Cargo.toml index dbe33492f..03b908c2a 100644 --- a/libs/gl-sdk/Cargo.toml +++ b/libs/gl-sdk/Cargo.toml @@ -24,7 +24,8 @@ thiserror = "2.0.17" tokio = { version = "1", features = ["sync"] } tonic.workspace = true tracing = { version = "0.1.43", features = ["async-await", "log"] } -uniffi = { version = "0.29.4" } +uniffi = { version = "0.29.4", features = ["tokio"] } +url = "2" [build-dependencies] uniffi = { version = "0.29.4", features = [ "build" ] } diff --git a/libs/gl-sdk/glsdk/glsdk.py b/libs/gl-sdk/glsdk/glsdk.py index 2099af126..a85c957d0 100644 --- a/libs/gl-sdk/glsdk/glsdk.py +++ b/libs/gl-sdk/glsdk/glsdk.py @@ -27,6 +27,7 @@ import itertools import traceback import typing +import asyncio import platform # Used for default argument values @@ -460,7 +461,17 @@ def _uniffi_check_contract_api_version(lib): raise InternalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") def _uniffi_check_api_checksums(lib): - if lib.uniffi_glsdk_checksum_func_parse_input() != 12312: + if lib.uniffi_glsdk_checksum_func_connect() != 43555: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_func_parse_input() != 49187: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_func_recover() != 39257: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_func_register() != 39628: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_func_register_or_recover() != 65070: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_func_resolve_input() != 24844: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_func_set_log_level() != 52328: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") @@ -496,6 +507,10 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_list_peers() != 29567: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_node_lnurl_pay() != 61306: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_node_lnurl_withdraw() != 61467: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_node_state() != 41833: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_onchain_receive() != 46432: @@ -510,16 +525,6 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_stream_node_events() != 5933: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_nodebuilder_connect() != 47474: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_nodebuilder_recover() != 46087: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_nodebuilder_register() != 49580: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_nodebuilder_register_or_recover() != 5543: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_nodebuilder_with_event_listener() != 56760: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_nodeeventstream_next() != 12635: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_scheduler_recover() != 55514: @@ -540,7 +545,7 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_constructor_developercert_new() != 57793: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_constructor_nodebuilder_new() != 34740: + if lib.uniffi_glsdk_checksum_constructor_node_new() != 7003: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_constructor_scheduler_new() != 15239: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") @@ -550,8 +555,6 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_loglistener_on_log() != 34844: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_nodeeventlistener_on_event() != 17790: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") # A ctypes library to expose the extern-C FFI definitions. # This is an implementation detail which will be called internally by the public API. @@ -661,19 +664,11 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UNIFFI_CALLBACK_INTERFACE_LOG_LISTENER_METHOD0 = ctypes.CFUNCTYPE(None,ctypes.c_uint64,_UniffiRustBuffer,ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), ) -_UNIFFI_CALLBACK_INTERFACE_NODE_EVENT_LISTENER_METHOD0 = ctypes.CFUNCTYPE(None,ctypes.c_uint64,_UniffiRustBuffer,ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): _fields_ = [ ("on_log", _UNIFFI_CALLBACK_INTERFACE_LOG_LISTENER_METHOD0), ("uniffi_free", _UNIFFI_CALLBACK_INTERFACE_FREE), ] -class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): - _fields_ = [ - ("on_event", _UNIFFI_CALLBACK_INTERFACE_NODE_EVENT_LISTENER_METHOD0), - ("uniffi_free", _UNIFFI_CALLBACK_INTERFACE_FREE), - ] _UniffiLib.uniffi_glsdk_fn_clone_config.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -766,6 +761,11 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_free_node.restype = None +_UniffiLib.uniffi_glsdk_fn_constructor_node_new.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_constructor_node_new.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_method_node_credentials.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -830,6 +830,18 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_node_list_peers.restype = _UniffiRustBuffer +_UniffiLib.uniffi_glsdk_fn_method_node_lnurl_pay.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_node_lnurl_pay.restype = _UniffiRustBuffer +_UniffiLib.uniffi_glsdk_fn_method_node_lnurl_withdraw.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_node_lnurl_withdraw.restype = _UniffiRustBuffer _UniffiLib.uniffi_glsdk_fn_method_node_node_state.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -872,54 +884,6 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_node_stream_node_events.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_clone_nodebuilder.argtypes = ( - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_clone_nodebuilder.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_free_nodebuilder.argtypes = ( - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_free_nodebuilder.restype = None -_UniffiLib.uniffi_glsdk_fn_constructor_nodebuilder_new.argtypes = ( - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_constructor_nodebuilder_new.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_connect.argtypes = ( - ctypes.c_void_p, - _UniffiRustBuffer, - _UniffiRustBuffer, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_connect.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_recover.argtypes = ( - ctypes.c_void_p, - _UniffiRustBuffer, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_recover.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register.argtypes = ( - ctypes.c_void_p, - _UniffiRustBuffer, - _UniffiRustBuffer, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register_or_recover.argtypes = ( - ctypes.c_void_p, - _UniffiRustBuffer, - _UniffiRustBuffer, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register_or_recover.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_with_event_listener.argtypes = ( - ctypes.c_void_p, - ctypes.c_uint64, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_with_event_listener.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_clone_nodeeventstream.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -1009,15 +973,42 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): ctypes.POINTER(_UniffiVTableCallbackInterfaceLogListener), ) _UniffiLib.uniffi_glsdk_fn_init_callback_vtable_loglistener.restype = None -_UniffiLib.uniffi_glsdk_fn_init_callback_vtable_nodeeventlistener.argtypes = ( - ctypes.POINTER(_UniffiVTableCallbackInterfaceNodeEventListener), +_UniffiLib.uniffi_glsdk_fn_func_connect.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), ) -_UniffiLib.uniffi_glsdk_fn_init_callback_vtable_nodeeventlistener.restype = None +_UniffiLib.uniffi_glsdk_fn_func_connect.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_func_parse_input.argtypes = ( _UniffiRustBuffer, ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_func_parse_input.restype = _UniffiRustBuffer +_UniffiLib.uniffi_glsdk_fn_func_recover.argtypes = ( + _UniffiRustBuffer, + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_func_recover.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_func_register.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_func_register.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_func_register_or_recover.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_func_register_or_recover.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_func_resolve_input.argtypes = ( + _UniffiRustBuffer, +) +_UniffiLib.uniffi_glsdk_fn_func_resolve_input.restype = ctypes.c_uint64 _UniffiLib.uniffi_glsdk_fn_func_set_log_level.argtypes = ( _UniffiRustBuffer, ctypes.POINTER(_UniffiRustCallStatus), @@ -1297,9 +1288,24 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.ffi_glsdk_rust_future_complete_void.restype = None +_UniffiLib.uniffi_glsdk_checksum_func_connect.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_func_connect.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_func_parse_input.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_func_parse_input.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_func_recover.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_func_recover.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_func_register.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_func_register.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_func_register_or_recover.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_func_register_or_recover.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_func_resolve_input.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_func_resolve_input.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_func_set_log_level.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_func_set_log_level.restype = ctypes.c_uint16 @@ -1351,6 +1357,12 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_node_list_peers.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_list_peers.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_node_lnurl_pay.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_node_lnurl_pay.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_node_lnurl_withdraw.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_node_lnurl_withdraw.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_method_node_node_state.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_node_state.restype = ctypes.c_uint16 @@ -1372,21 +1384,6 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_node_stream_node_events.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_stream_node_events.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_connect.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_connect.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_recover.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_recover.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register_or_recover.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register_or_recover.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_with_event_listener.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_with_event_listener.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_method_nodeeventstream_next.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_nodeeventstream_next.restype = ctypes.c_uint16 @@ -1417,9 +1414,9 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_constructor_developercert_new.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_constructor_developercert_new.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_constructor_nodebuilder_new.argtypes = ( +_UniffiLib.uniffi_glsdk_checksum_constructor_node_new.argtypes = ( ) -_UniffiLib.uniffi_glsdk_checksum_constructor_nodebuilder_new.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_constructor_node_new.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_constructor_scheduler_new.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_constructor_scheduler_new.restype = ctypes.c_uint16 @@ -1432,9 +1429,6 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_loglistener_on_log.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_loglistener_on_log.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_nodeeventlistener_on_event.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_nodeeventlistener_on_event.restype = ctypes.c_uint16 _UniffiLib.ffi_glsdk_uniffi_contract_version.argtypes = ( ) _UniffiLib.ffi_glsdk_uniffi_contract_version.restype = ctypes.c_uint32 @@ -1592,8 +1586,6 @@ def write(value, buf): - - class FundChannel: peer_id: "str" """ @@ -2326,1232 +2318,2414 @@ def write(value, buf): _UniffiConverterSequenceTypePeer.write(value.peers, buf) -class LogEntry: +class LnUrlErrorData: """ - A single log message from the SDK. + Error returned by an LNURL service endpoint. """ - level: "LogLevel" - message: "str" - target: "str" + reason: "str" + def __init__(self, *, reason: "str"): + self.reason = reason + + def __str__(self): + return "LnUrlErrorData(reason={})".format(self.reason) + + def __eq__(self, other): + if self.reason != other.reason: + return False + return True + +class _UniffiConverterTypeLnUrlErrorData(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return LnUrlErrorData( + reason=_UniffiConverterString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.reason) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.reason, buf) + + +class LnUrlPayErrorData: """ - The module that produced this log (e.g. "gl_client::scheduler"). + Details of a failed LNURL-pay attempt on the pay phase. """ - file: "typing.Optional[str]" + payment_hash: "str" """ - Source file path, if the log macro recorded one. + Hex-encoded payment hash of the invoice the service returned. """ - line: "typing.Optional[int]" + reason: "str" """ - Source line number, if the log macro recorded one. + Human-readable reason the pay attempt failed. """ - def __init__(self, *, level: "LogLevel", message: "str", target: "str", file: "typing.Optional[str]", line: "typing.Optional[int]"): - self.level = level - self.message = message - self.target = target - self.file = file - self.line = line + def __init__(self, *, payment_hash: "str", reason: "str"): + self.payment_hash = payment_hash + self.reason = reason def __str__(self): - return "LogEntry(level={}, message={}, target={}, file={}, line={})".format(self.level, self.message, self.target, self.file, self.line) + return "LnUrlPayErrorData(payment_hash={}, reason={})".format(self.payment_hash, self.reason) def __eq__(self, other): - if self.level != other.level: - return False - if self.message != other.message: - return False - if self.target != other.target: - return False - if self.file != other.file: + if self.payment_hash != other.payment_hash: return False - if self.line != other.line: + if self.reason != other.reason: return False return True -class _UniffiConverterTypeLogEntry(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLnUrlPayErrorData(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return LogEntry( - level=_UniffiConverterTypeLogLevel.read(buf), - message=_UniffiConverterString.read(buf), - target=_UniffiConverterString.read(buf), - file=_UniffiConverterOptionalString.read(buf), - line=_UniffiConverterOptionalUInt32.read(buf), + return LnUrlPayErrorData( + payment_hash=_UniffiConverterString.read(buf), + reason=_UniffiConverterString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterTypeLogLevel.check_lower(value.level) - _UniffiConverterString.check_lower(value.message) - _UniffiConverterString.check_lower(value.target) - _UniffiConverterOptionalString.check_lower(value.file) - _UniffiConverterOptionalUInt32.check_lower(value.line) + _UniffiConverterString.check_lower(value.payment_hash) + _UniffiConverterString.check_lower(value.reason) @staticmethod def write(value, buf): - _UniffiConverterTypeLogLevel.write(value.level, buf) - _UniffiConverterString.write(value.message, buf) - _UniffiConverterString.write(value.target, buf) - _UniffiConverterOptionalString.write(value.file, buf) - _UniffiConverterOptionalUInt32.write(value.line, buf) + _UniffiConverterString.write(value.payment_hash, buf) + _UniffiConverterString.write(value.reason, buf) -class NodeState: +class LnUrlPayRequest: """ - A point-in-time snapshot of the node's balances, capacity, and - connectivity. Returned by `node_state()`. + Request to execute an LNURL-pay flow. - All amounts are in millisatoshis (1 sat = 1000 msat). + Combines the resolved service data with the user's chosen amount. """ - id: "str" + data: "LnUrlPayRequestData" """ - The node's public key as a lowercase hex string (66 chars). + The resolved pay request data from `parse_input()`. """ - block_height: "int" + amount_msat: "int" """ - Latest block height the node has synced to. + Amount to pay in millisatoshis. """ - network: "str" + comment: "typing.Optional[str]" """ - The Bitcoin network this node is running on (e.g. "bitcoin", "regtest"). + Optional comment to send with the payment. """ - version: "str" - """ - CLN version string (e.g. "v24.11"). + validate_success_action_url: "typing.Optional[bool]" """ + When true (the default), a URL success action is rejected if its + domain differs from the callback's domain. - alias: "typing.Optional[str]" - """ - Human-readable node alias, if set. + This is a wallet-side safety convention, not a LUD-09 requirement: + LUD-09 does not mandate same-domain URLs, but a divergent domain + can be used to phish users, so the SDK rejects it by default. + Set to `Some(false)` only if you have a specific reason to trust + cross-domain success-action URLs from this service. """ - color: "str" + def __init__(self, *, data: "LnUrlPayRequestData", amount_msat: "int", comment: "typing.Optional[str]", validate_success_action_url: "typing.Optional[bool]"): + self.data = data + self.amount_msat = amount_msat + self.comment = comment + self.validate_success_action_url = validate_success_action_url + + def __str__(self): + return "LnUrlPayRequest(data={}, amount_msat={}, comment={}, validate_success_action_url={})".format(self.data, self.amount_msat, self.comment, self.validate_success_action_url) + + def __eq__(self, other): + if self.data != other.data: + return False + if self.amount_msat != other.amount_msat: + return False + if self.comment != other.comment: + return False + if self.validate_success_action_url != other.validate_success_action_url: + return False + return True + +class _UniffiConverterTypeLnUrlPayRequest(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return LnUrlPayRequest( + data=_UniffiConverterTypeLnUrlPayRequestData.read(buf), + amount_msat=_UniffiConverterUInt64.read(buf), + comment=_UniffiConverterOptionalString.read(buf), + validate_success_action_url=_UniffiConverterOptionalBool.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterTypeLnUrlPayRequestData.check_lower(value.data) + _UniffiConverterUInt64.check_lower(value.amount_msat) + _UniffiConverterOptionalString.check_lower(value.comment) + _UniffiConverterOptionalBool.check_lower(value.validate_success_action_url) + + @staticmethod + def write(value, buf): + _UniffiConverterTypeLnUrlPayRequestData.write(value.data, buf) + _UniffiConverterUInt64.write(value.amount_msat, buf) + _UniffiConverterOptionalString.write(value.comment, buf) + _UniffiConverterOptionalBool.write(value.validate_success_action_url, buf) + + +class LnUrlPayRequestData: """ - 3-byte RGB color of the node, as a lowercase hex string (6 chars). + Data from an LNURL-pay endpoint (LUD-06). + + Contains the service's accepted amount range and metadata. + Returned inside `InputType::LnUrlPay` after `parse_input` resolves + an LNURL or Lightning Address. """ - num_active_channels: "int" + callback: "str" """ - Number of channels that are open and operational. These are the - channels that contribute to `channels_balance_msat`, - `max_payable_msat`, `total_channel_capacity_msat`, and - `total_inbound_liquidity_msat`. + The callback URL to request an invoice from. """ - num_pending_channels: "int" + min_sendable: "int" """ - Number of channels that are being opened but not yet confirmed. - Pending channels do not contribute to any balance or capacity - field on this snapshot; their funds show up only after they - transition to active. + Minimum amount the service accepts, in millisatoshis. """ - num_inactive_channels: "int" + max_sendable: "int" """ - Number of channels that are open but the peer is offline. - Inactive channels hold balance but cannot be used for payments - until the peer reconnects; they do not contribute to - `max_payable_msat` or `total_inbound_liquidity_msat` (those are - computed from the live `spendable_msat` / `receivable_msat` - reported by CLN, which goes to zero when the peer is offline). + Maximum amount the service accepts, in millisatoshis. """ - channels_balance_msat: "int" + metadata: "str" """ - Total our-side balance across all open channels, including amounts - that protocol reserves make unspendable. - - This is the field a wallet's home screen should show as the - user's "Lightning balance" — it reflects what they own off-chain, - matching what they'd expect to see at a glance. - - Do **not** use this to gate a send button: some of it is locked - in channel reserves. Use `max_payable_msat` for that. + Raw metadata JSON string (array of `["mime", "content"]` pairs). """ - max_payable_msat: "int" + comment_allowed: "int" """ - Aggregate spendable amount across all open channels. Equal to - `channels_balance_msat - max_chan_reserve_msat`. - - This is the field a send screen should gate against — it is what - the user can actually move right now over Lightning in total. - - Caveat: a single Lightning payment is additionally bounded by - the largest channel's own `spendable_msat`. Reaching this full - aggregate amount in one payment requires multi-path-payment - support from the recipient and a working route. + Maximum comment length the service accepts. 0 means no comments. """ - total_channel_capacity_msat: "int" + description: "str" """ - Sum of all open channel capacities (your side + remote side). + Human-readable description extracted from metadata. """ - max_chan_reserve_msat: "int" + lnurl: "str" """ - Amount locked in protocol channel reserves, computed as - `channels_balance_msat - max_payable_msat`. These sats are yours - on paper but cannot be spent until the channel closes. + The original LNURL or lightning address that was resolved. """ - onchain_balance_msat: "int" - """ - Confirmed on-chain balance available for spending or opening channels. - """ - - unconfirmed_onchain_balance_msat: "int" - """ - On-chain balance from transactions that have not yet been confirmed. - """ - - immature_onchain_balance_msat: "int" - """ - On-chain balance confirmed but not yet spendable (e.g. coinbase - outputs inside the 100-block maturation window). - """ - - pending_onchain_balance_msat: "int" - """ - On-chain balance locked in channels that are being closed. - These funds will become available once the close is confirmed. - """ - - max_receivable_single_payment_msat: "int" - """ - Largest single Lightning payment the node can receive without - splitting across channels. Bounded by the inbound capacity of - the largest open channel. - """ - - total_inbound_liquidity_msat: "int" - """ - Total amount you can receive across all open channels combined. - """ - - connected_channel_peers: "typing.List[str]" - """ - Lowercase hex public keys of peers we have at least one channel - with and are currently connected to. Peers we're connected to but - have no channel with are not represented here; for routing-node - use cases, query `list_peers()` directly. - """ - - utxos: "typing.List[FundOutput]" - """ - Unspent on-chain outputs owned by the node's wallet. Excludes - spent outputs; includes confirmed, unconfirmed, immature, and - reserved UTXOs (callers can filter by `status` and `reserved`). - """ - - total_onchain_msat: "int" - """ - All non-pending on-chain balance buckets summed: - `onchain_balance_msat + unconfirmed_onchain_balance_msat + immature_onchain_balance_msat`. - Excludes funds locked in closing channels (`pending_onchain_balance_msat`) - since those are not yet on-chain UTXOs. - """ - - total_balance_msat: "int" - """ - Everything the user owns, summed: channel balance (including - protocol reserves) + all on-chain buckets + funds locked in - closing channels. The "total holdings" number a wallet home - screen typically shows. - """ - - spendable_balance_msat: "int" - """ - What the user can spend *right now*: - `max_payable_msat + onchain_balance_msat`. Excludes reserves, - unconfirmed, immature, and pending amounts. The number a - send-money screen should gate against. - """ - - def __init__(self, *, id: "str", block_height: "int", network: "str", version: "str", alias: "typing.Optional[str]", color: "str", num_active_channels: "int", num_pending_channels: "int", num_inactive_channels: "int", channels_balance_msat: "int", max_payable_msat: "int", total_channel_capacity_msat: "int", max_chan_reserve_msat: "int", onchain_balance_msat: "int", unconfirmed_onchain_balance_msat: "int", immature_onchain_balance_msat: "int", pending_onchain_balance_msat: "int", max_receivable_single_payment_msat: "int", total_inbound_liquidity_msat: "int", connected_channel_peers: "typing.List[str]", utxos: "typing.List[FundOutput]", total_onchain_msat: "int", total_balance_msat: "int", spendable_balance_msat: "int"): - self.id = id - self.block_height = block_height - self.network = network - self.version = version - self.alias = alias - self.color = color - self.num_active_channels = num_active_channels - self.num_pending_channels = num_pending_channels - self.num_inactive_channels = num_inactive_channels - self.channels_balance_msat = channels_balance_msat - self.max_payable_msat = max_payable_msat - self.total_channel_capacity_msat = total_channel_capacity_msat - self.max_chan_reserve_msat = max_chan_reserve_msat - self.onchain_balance_msat = onchain_balance_msat - self.unconfirmed_onchain_balance_msat = unconfirmed_onchain_balance_msat - self.immature_onchain_balance_msat = immature_onchain_balance_msat - self.pending_onchain_balance_msat = pending_onchain_balance_msat - self.max_receivable_single_payment_msat = max_receivable_single_payment_msat - self.total_inbound_liquidity_msat = total_inbound_liquidity_msat - self.connected_channel_peers = connected_channel_peers - self.utxos = utxos - self.total_onchain_msat = total_onchain_msat - self.total_balance_msat = total_balance_msat - self.spendable_balance_msat = spendable_balance_msat + def __init__(self, *, callback: "str", min_sendable: "int", max_sendable: "int", metadata: "str", comment_allowed: "int", description: "str", lnurl: "str"): + self.callback = callback + self.min_sendable = min_sendable + self.max_sendable = max_sendable + self.metadata = metadata + self.comment_allowed = comment_allowed + self.description = description + self.lnurl = lnurl def __str__(self): - return "NodeState(id={}, block_height={}, network={}, version={}, alias={}, color={}, num_active_channels={}, num_pending_channels={}, num_inactive_channels={}, channels_balance_msat={}, max_payable_msat={}, total_channel_capacity_msat={}, max_chan_reserve_msat={}, onchain_balance_msat={}, unconfirmed_onchain_balance_msat={}, immature_onchain_balance_msat={}, pending_onchain_balance_msat={}, max_receivable_single_payment_msat={}, total_inbound_liquidity_msat={}, connected_channel_peers={}, utxos={}, total_onchain_msat={}, total_balance_msat={}, spendable_balance_msat={})".format(self.id, self.block_height, self.network, self.version, self.alias, self.color, self.num_active_channels, self.num_pending_channels, self.num_inactive_channels, self.channels_balance_msat, self.max_payable_msat, self.total_channel_capacity_msat, self.max_chan_reserve_msat, self.onchain_balance_msat, self.unconfirmed_onchain_balance_msat, self.immature_onchain_balance_msat, self.pending_onchain_balance_msat, self.max_receivable_single_payment_msat, self.total_inbound_liquidity_msat, self.connected_channel_peers, self.utxos, self.total_onchain_msat, self.total_balance_msat, self.spendable_balance_msat) + return "LnUrlPayRequestData(callback={}, min_sendable={}, max_sendable={}, metadata={}, comment_allowed={}, description={}, lnurl={})".format(self.callback, self.min_sendable, self.max_sendable, self.metadata, self.comment_allowed, self.description, self.lnurl) def __eq__(self, other): - if self.id != other.id: - return False - if self.block_height != other.block_height: - return False - if self.network != other.network: - return False - if self.version != other.version: - return False - if self.alias != other.alias: - return False - if self.color != other.color: - return False - if self.num_active_channels != other.num_active_channels: - return False - if self.num_pending_channels != other.num_pending_channels: - return False - if self.num_inactive_channels != other.num_inactive_channels: - return False - if self.channels_balance_msat != other.channels_balance_msat: - return False - if self.max_payable_msat != other.max_payable_msat: - return False - if self.total_channel_capacity_msat != other.total_channel_capacity_msat: - return False - if self.max_chan_reserve_msat != other.max_chan_reserve_msat: - return False - if self.onchain_balance_msat != other.onchain_balance_msat: - return False - if self.unconfirmed_onchain_balance_msat != other.unconfirmed_onchain_balance_msat: - return False - if self.immature_onchain_balance_msat != other.immature_onchain_balance_msat: - return False - if self.pending_onchain_balance_msat != other.pending_onchain_balance_msat: - return False - if self.max_receivable_single_payment_msat != other.max_receivable_single_payment_msat: + if self.callback != other.callback: return False - if self.total_inbound_liquidity_msat != other.total_inbound_liquidity_msat: + if self.min_sendable != other.min_sendable: return False - if self.connected_channel_peers != other.connected_channel_peers: + if self.max_sendable != other.max_sendable: return False - if self.utxos != other.utxos: + if self.metadata != other.metadata: return False - if self.total_onchain_msat != other.total_onchain_msat: + if self.comment_allowed != other.comment_allowed: return False - if self.total_balance_msat != other.total_balance_msat: + if self.description != other.description: return False - if self.spendable_balance_msat != other.spendable_balance_msat: + if self.lnurl != other.lnurl: return False return True -class _UniffiConverterTypeNodeState(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLnUrlPayRequestData(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return NodeState( - id=_UniffiConverterString.read(buf), - block_height=_UniffiConverterUInt32.read(buf), - network=_UniffiConverterString.read(buf), - version=_UniffiConverterString.read(buf), - alias=_UniffiConverterOptionalString.read(buf), - color=_UniffiConverterString.read(buf), - num_active_channels=_UniffiConverterUInt32.read(buf), - num_pending_channels=_UniffiConverterUInt32.read(buf), - num_inactive_channels=_UniffiConverterUInt32.read(buf), - channels_balance_msat=_UniffiConverterUInt64.read(buf), - max_payable_msat=_UniffiConverterUInt64.read(buf), - total_channel_capacity_msat=_UniffiConverterUInt64.read(buf), - max_chan_reserve_msat=_UniffiConverterUInt64.read(buf), - onchain_balance_msat=_UniffiConverterUInt64.read(buf), - unconfirmed_onchain_balance_msat=_UniffiConverterUInt64.read(buf), - immature_onchain_balance_msat=_UniffiConverterUInt64.read(buf), - pending_onchain_balance_msat=_UniffiConverterUInt64.read(buf), - max_receivable_single_payment_msat=_UniffiConverterUInt64.read(buf), - total_inbound_liquidity_msat=_UniffiConverterUInt64.read(buf), - connected_channel_peers=_UniffiConverterSequenceString.read(buf), - utxos=_UniffiConverterSequenceTypeFundOutput.read(buf), - total_onchain_msat=_UniffiConverterUInt64.read(buf), - total_balance_msat=_UniffiConverterUInt64.read(buf), - spendable_balance_msat=_UniffiConverterUInt64.read(buf), + return LnUrlPayRequestData( + callback=_UniffiConverterString.read(buf), + min_sendable=_UniffiConverterUInt64.read(buf), + max_sendable=_UniffiConverterUInt64.read(buf), + metadata=_UniffiConverterString.read(buf), + comment_allowed=_UniffiConverterUInt64.read(buf), + description=_UniffiConverterString.read(buf), + lnurl=_UniffiConverterString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.id) - _UniffiConverterUInt32.check_lower(value.block_height) - _UniffiConverterString.check_lower(value.network) - _UniffiConverterString.check_lower(value.version) - _UniffiConverterOptionalString.check_lower(value.alias) - _UniffiConverterString.check_lower(value.color) - _UniffiConverterUInt32.check_lower(value.num_active_channels) - _UniffiConverterUInt32.check_lower(value.num_pending_channels) - _UniffiConverterUInt32.check_lower(value.num_inactive_channels) - _UniffiConverterUInt64.check_lower(value.channels_balance_msat) - _UniffiConverterUInt64.check_lower(value.max_payable_msat) - _UniffiConverterUInt64.check_lower(value.total_channel_capacity_msat) - _UniffiConverterUInt64.check_lower(value.max_chan_reserve_msat) - _UniffiConverterUInt64.check_lower(value.onchain_balance_msat) - _UniffiConverterUInt64.check_lower(value.unconfirmed_onchain_balance_msat) - _UniffiConverterUInt64.check_lower(value.immature_onchain_balance_msat) - _UniffiConverterUInt64.check_lower(value.pending_onchain_balance_msat) - _UniffiConverterUInt64.check_lower(value.max_receivable_single_payment_msat) - _UniffiConverterUInt64.check_lower(value.total_inbound_liquidity_msat) - _UniffiConverterSequenceString.check_lower(value.connected_channel_peers) - _UniffiConverterSequenceTypeFundOutput.check_lower(value.utxos) - _UniffiConverterUInt64.check_lower(value.total_onchain_msat) - _UniffiConverterUInt64.check_lower(value.total_balance_msat) - _UniffiConverterUInt64.check_lower(value.spendable_balance_msat) + _UniffiConverterString.check_lower(value.callback) + _UniffiConverterUInt64.check_lower(value.min_sendable) + _UniffiConverterUInt64.check_lower(value.max_sendable) + _UniffiConverterString.check_lower(value.metadata) + _UniffiConverterUInt64.check_lower(value.comment_allowed) + _UniffiConverterString.check_lower(value.description) + _UniffiConverterString.check_lower(value.lnurl) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.id, buf) - _UniffiConverterUInt32.write(value.block_height, buf) - _UniffiConverterString.write(value.network, buf) - _UniffiConverterString.write(value.version, buf) - _UniffiConverterOptionalString.write(value.alias, buf) - _UniffiConverterString.write(value.color, buf) - _UniffiConverterUInt32.write(value.num_active_channels, buf) - _UniffiConverterUInt32.write(value.num_pending_channels, buf) - _UniffiConverterUInt32.write(value.num_inactive_channels, buf) - _UniffiConverterUInt64.write(value.channels_balance_msat, buf) - _UniffiConverterUInt64.write(value.max_payable_msat, buf) - _UniffiConverterUInt64.write(value.total_channel_capacity_msat, buf) - _UniffiConverterUInt64.write(value.max_chan_reserve_msat, buf) - _UniffiConverterUInt64.write(value.onchain_balance_msat, buf) - _UniffiConverterUInt64.write(value.unconfirmed_onchain_balance_msat, buf) - _UniffiConverterUInt64.write(value.immature_onchain_balance_msat, buf) - _UniffiConverterUInt64.write(value.pending_onchain_balance_msat, buf) - _UniffiConverterUInt64.write(value.max_receivable_single_payment_msat, buf) - _UniffiConverterUInt64.write(value.total_inbound_liquidity_msat, buf) - _UniffiConverterSequenceString.write(value.connected_channel_peers, buf) - _UniffiConverterSequenceTypeFundOutput.write(value.utxos, buf) - _UniffiConverterUInt64.write(value.total_onchain_msat, buf) - _UniffiConverterUInt64.write(value.total_balance_msat, buf) - _UniffiConverterUInt64.write(value.spendable_balance_msat, buf) + _UniffiConverterString.write(value.callback, buf) + _UniffiConverterUInt64.write(value.min_sendable, buf) + _UniffiConverterUInt64.write(value.max_sendable, buf) + _UniffiConverterString.write(value.metadata, buf) + _UniffiConverterUInt64.write(value.comment_allowed, buf) + _UniffiConverterString.write(value.description, buf) + _UniffiConverterString.write(value.lnurl, buf) -class OnchainReceiveResponse: +class LnUrlPaySuccessData: """ - A pair of on-chain addresses for receiving funds. + Successful LNURL-pay result data. """ - bech32: "str" + payment_preimage: "str" """ - SegWit v0 (bech32) address — starts with `bc1q` on mainnet. + The payment preimage (proof of payment), hex-encoded. """ - p2tr: "str" + success_action: "typing.Optional[SuccessActionProcessed]" """ - Taproot (bech32m) address — starts with `bc1p` on mainnet. + Optional success action from the service (LUD-09). """ - def __init__(self, *, bech32: "str", p2tr: "str"): - self.bech32 = bech32 - self.p2tr = p2tr + def __init__(self, *, payment_preimage: "str", success_action: "typing.Optional[SuccessActionProcessed]"): + self.payment_preimage = payment_preimage + self.success_action = success_action def __str__(self): - return "OnchainReceiveResponse(bech32={}, p2tr={})".format(self.bech32, self.p2tr) + return "LnUrlPaySuccessData(payment_preimage={}, success_action={})".format(self.payment_preimage, self.success_action) def __eq__(self, other): - if self.bech32 != other.bech32: + if self.payment_preimage != other.payment_preimage: return False - if self.p2tr != other.p2tr: + if self.success_action != other.success_action: return False return True -class _UniffiConverterTypeOnchainReceiveResponse(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLnUrlPaySuccessData(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return OnchainReceiveResponse( - bech32=_UniffiConverterString.read(buf), - p2tr=_UniffiConverterString.read(buf), + return LnUrlPaySuccessData( + payment_preimage=_UniffiConverterString.read(buf), + success_action=_UniffiConverterOptionalTypeSuccessActionProcessed.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.bech32) - _UniffiConverterString.check_lower(value.p2tr) + _UniffiConverterString.check_lower(value.payment_preimage) + _UniffiConverterOptionalTypeSuccessActionProcessed.check_lower(value.success_action) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.bech32, buf) - _UniffiConverterString.write(value.p2tr, buf) + _UniffiConverterString.write(value.payment_preimage, buf) + _UniffiConverterOptionalTypeSuccessActionProcessed.write(value.success_action, buf) -class OnchainSendResponse: +class LnUrlWithdrawRequest: """ - Result of an on-chain send. The transaction has already been broadcast. + Request to execute an LNURL-withdraw flow. + + Combines the resolved service data with the user's chosen amount. """ - tx: "bytes" + data: "LnUrlWithdrawRequestData" """ - The raw signed transaction bytes. + The resolved withdraw request data from `parse_input()`. """ - txid: "str" + amount_msat: "int" """ - The transaction id as lowercase hex (64 chars). + Amount to withdraw in millisatoshis. """ - psbt: "str" + description: "typing.Optional[str]" """ - The transaction as a Partially Signed Bitcoin Transaction string. + Optional description for the invoice (overrides default). """ - def __init__(self, *, tx: "bytes", txid: "str", psbt: "str"): - self.tx = tx - self.txid = txid - self.psbt = psbt - - def __str__(self): - return "OnchainSendResponse(tx={}, txid={}, psbt={})".format(self.tx, self.txid, self.psbt) + def __init__(self, *, data: "LnUrlWithdrawRequestData", amount_msat: "int", description: "typing.Optional[str]"): + self.data = data + self.amount_msat = amount_msat + self.description = description + + def __str__(self): + return "LnUrlWithdrawRequest(data={}, amount_msat={}, description={})".format(self.data, self.amount_msat, self.description) def __eq__(self, other): - if self.tx != other.tx: + if self.data != other.data: return False - if self.txid != other.txid: + if self.amount_msat != other.amount_msat: return False - if self.psbt != other.psbt: + if self.description != other.description: return False return True -class _UniffiConverterTypeOnchainSendResponse(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLnUrlWithdrawRequest(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return OnchainSendResponse( - tx=_UniffiConverterBytes.read(buf), - txid=_UniffiConverterString.read(buf), - psbt=_UniffiConverterString.read(buf), + return LnUrlWithdrawRequest( + data=_UniffiConverterTypeLnUrlWithdrawRequestData.read(buf), + amount_msat=_UniffiConverterUInt64.read(buf), + description=_UniffiConverterOptionalString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.tx) - _UniffiConverterString.check_lower(value.txid) - _UniffiConverterString.check_lower(value.psbt) + _UniffiConverterTypeLnUrlWithdrawRequestData.check_lower(value.data) + _UniffiConverterUInt64.check_lower(value.amount_msat) + _UniffiConverterOptionalString.check_lower(value.description) @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.tx, buf) - _UniffiConverterString.write(value.txid, buf) - _UniffiConverterString.write(value.psbt, buf) + _UniffiConverterTypeLnUrlWithdrawRequestData.write(value.data, buf) + _UniffiConverterUInt64.write(value.amount_msat, buf) + _UniffiConverterOptionalString.write(value.description, buf) -class ParsedInvoice: - """ - Parsed BOLT11 invoice with extracted fields. +class LnUrlWithdrawRequestData: """ + Data from an LNURL-withdraw endpoint (LUD-03). - bolt11: "str" - """ - The original invoice string. + Contains the service's accepted withdrawal range and session key. + Returned inside `InputType::LnUrlWithdraw` after `parse_input` + resolves an LNURL. """ - payee_pubkey: "typing.Optional[str]" + callback: "str" """ - Recipient public key as lowercase hex (66 chars), recovered from the invoice signature. + The callback URL to submit the invoice to. """ - payment_hash: "str" + k1: "str" """ - Payment hash as lowercase hex (64 chars) identifying this payment. + Ephemeral secret linking this wallet session to the service. """ - description: "typing.Optional[str]" + default_description: "str" """ - Invoice description. None if the invoice uses a description hash. + Default description for the invoice. """ - amount_msat: "typing.Optional[int]" + min_withdrawable: "int" """ - Requested amount in millisatoshis. None for "any amount" invoices. + Minimum withdrawable amount in millisatoshis. """ - expiry: "int" + max_withdrawable: "int" """ - Seconds from creation until the invoice expires. + Maximum withdrawable amount in millisatoshis. """ - timestamp: "int" + lnurl: "str" """ - Unix timestamp (seconds) when the invoice was created. + The original LNURL that was resolved. """ - def __init__(self, *, bolt11: "str", payee_pubkey: "typing.Optional[str]", payment_hash: "str", description: "typing.Optional[str]", amount_msat: "typing.Optional[int]", expiry: "int", timestamp: "int"): - self.bolt11 = bolt11 - self.payee_pubkey = payee_pubkey - self.payment_hash = payment_hash - self.description = description - self.amount_msat = amount_msat - self.expiry = expiry - self.timestamp = timestamp + def __init__(self, *, callback: "str", k1: "str", default_description: "str", min_withdrawable: "int", max_withdrawable: "int", lnurl: "str"): + self.callback = callback + self.k1 = k1 + self.default_description = default_description + self.min_withdrawable = min_withdrawable + self.max_withdrawable = max_withdrawable + self.lnurl = lnurl def __str__(self): - return "ParsedInvoice(bolt11={}, payee_pubkey={}, payment_hash={}, description={}, amount_msat={}, expiry={}, timestamp={})".format(self.bolt11, self.payee_pubkey, self.payment_hash, self.description, self.amount_msat, self.expiry, self.timestamp) + return "LnUrlWithdrawRequestData(callback={}, k1={}, default_description={}, min_withdrawable={}, max_withdrawable={}, lnurl={})".format(self.callback, self.k1, self.default_description, self.min_withdrawable, self.max_withdrawable, self.lnurl) def __eq__(self, other): - if self.bolt11 != other.bolt11: + if self.callback != other.callback: return False - if self.payee_pubkey != other.payee_pubkey: - return False - if self.payment_hash != other.payment_hash: + if self.k1 != other.k1: return False - if self.description != other.description: + if self.default_description != other.default_description: return False - if self.amount_msat != other.amount_msat: + if self.min_withdrawable != other.min_withdrawable: return False - if self.expiry != other.expiry: + if self.max_withdrawable != other.max_withdrawable: return False - if self.timestamp != other.timestamp: + if self.lnurl != other.lnurl: return False return True -class _UniffiConverterTypeParsedInvoice(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLnUrlWithdrawRequestData(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return ParsedInvoice( - bolt11=_UniffiConverterString.read(buf), - payee_pubkey=_UniffiConverterOptionalString.read(buf), - payment_hash=_UniffiConverterString.read(buf), - description=_UniffiConverterOptionalString.read(buf), - amount_msat=_UniffiConverterOptionalUInt64.read(buf), - expiry=_UniffiConverterUInt64.read(buf), - timestamp=_UniffiConverterUInt64.read(buf), + return LnUrlWithdrawRequestData( + callback=_UniffiConverterString.read(buf), + k1=_UniffiConverterString.read(buf), + default_description=_UniffiConverterString.read(buf), + min_withdrawable=_UniffiConverterUInt64.read(buf), + max_withdrawable=_UniffiConverterUInt64.read(buf), + lnurl=_UniffiConverterString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.bolt11) - _UniffiConverterOptionalString.check_lower(value.payee_pubkey) - _UniffiConverterString.check_lower(value.payment_hash) - _UniffiConverterOptionalString.check_lower(value.description) - _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) - _UniffiConverterUInt64.check_lower(value.expiry) - _UniffiConverterUInt64.check_lower(value.timestamp) + _UniffiConverterString.check_lower(value.callback) + _UniffiConverterString.check_lower(value.k1) + _UniffiConverterString.check_lower(value.default_description) + _UniffiConverterUInt64.check_lower(value.min_withdrawable) + _UniffiConverterUInt64.check_lower(value.max_withdrawable) + _UniffiConverterString.check_lower(value.lnurl) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.bolt11, buf) - _UniffiConverterOptionalString.write(value.payee_pubkey, buf) - _UniffiConverterString.write(value.payment_hash, buf) - _UniffiConverterOptionalString.write(value.description, buf) - _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) - _UniffiConverterUInt64.write(value.expiry, buf) - _UniffiConverterUInt64.write(value.timestamp, buf) + _UniffiConverterString.write(value.callback, buf) + _UniffiConverterString.write(value.k1, buf) + _UniffiConverterString.write(value.default_description, buf) + _UniffiConverterUInt64.write(value.min_withdrawable, buf) + _UniffiConverterUInt64.write(value.max_withdrawable, buf) + _UniffiConverterString.write(value.lnurl, buf) -class Pay: - payment_hash: "str" - """ - Payment hash as lowercase hex (64 chars). - """ - - status: "PayStatus" - destination_pubkey: "typing.Optional[str]" +class LnUrlWithdrawSuccessData: """ - Recipient node pubkey as lowercase hex (66 chars), if known. + Successful LNURL-withdraw result data. """ - amount_msat: "typing.Optional[int]" - amount_sent_msat: "typing.Optional[int]" - label: "typing.Optional[str]" - bolt11: "typing.Optional[str]" - description: "typing.Optional[str]" - bolt12: "typing.Optional[str]" - preimage: "typing.Optional[str]" + invoice: "str" """ - Payment preimage as lowercase hex (64 chars), if the payment completed. + The BOLT11 invoice that was submitted for withdrawal. """ - created_at: "int" - completed_at: "typing.Optional[int]" - number_of_parts: "typing.Optional[int]" - def __init__(self, *, payment_hash: "str", status: "PayStatus", destination_pubkey: "typing.Optional[str]", amount_msat: "typing.Optional[int]", amount_sent_msat: "typing.Optional[int]", label: "typing.Optional[str]", bolt11: "typing.Optional[str]", description: "typing.Optional[str]", bolt12: "typing.Optional[str]", preimage: "typing.Optional[str]", created_at: "int", completed_at: "typing.Optional[int]", number_of_parts: "typing.Optional[int]"): - self.payment_hash = payment_hash - self.status = status - self.destination_pubkey = destination_pubkey - self.amount_msat = amount_msat - self.amount_sent_msat = amount_sent_msat - self.label = label - self.bolt11 = bolt11 - self.description = description - self.bolt12 = bolt12 - self.preimage = preimage - self.created_at = created_at - self.completed_at = completed_at - self.number_of_parts = number_of_parts + def __init__(self, *, invoice: "str"): + self.invoice = invoice def __str__(self): - return "Pay(payment_hash={}, status={}, destination_pubkey={}, amount_msat={}, amount_sent_msat={}, label={}, bolt11={}, description={}, bolt12={}, preimage={}, created_at={}, completed_at={}, number_of_parts={})".format(self.payment_hash, self.status, self.destination_pubkey, self.amount_msat, self.amount_sent_msat, self.label, self.bolt11, self.description, self.bolt12, self.preimage, self.created_at, self.completed_at, self.number_of_parts) + return "LnUrlWithdrawSuccessData(invoice={})".format(self.invoice) def __eq__(self, other): - if self.payment_hash != other.payment_hash: - return False - if self.status != other.status: - return False - if self.destination_pubkey != other.destination_pubkey: - return False - if self.amount_msat != other.amount_msat: - return False - if self.amount_sent_msat != other.amount_sent_msat: - return False - if self.label != other.label: - return False - if self.bolt11 != other.bolt11: - return False - if self.description != other.description: - return False - if self.bolt12 != other.bolt12: - return False - if self.preimage != other.preimage: - return False - if self.created_at != other.created_at: - return False - if self.completed_at != other.completed_at: - return False - if self.number_of_parts != other.number_of_parts: + if self.invoice != other.invoice: return False return True -class _UniffiConverterTypePay(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLnUrlWithdrawSuccessData(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return Pay( - payment_hash=_UniffiConverterString.read(buf), - status=_UniffiConverterTypePayStatus.read(buf), - destination_pubkey=_UniffiConverterOptionalString.read(buf), - amount_msat=_UniffiConverterOptionalUInt64.read(buf), - amount_sent_msat=_UniffiConverterOptionalUInt64.read(buf), - label=_UniffiConverterOptionalString.read(buf), - bolt11=_UniffiConverterOptionalString.read(buf), - description=_UniffiConverterOptionalString.read(buf), - bolt12=_UniffiConverterOptionalString.read(buf), - preimage=_UniffiConverterOptionalString.read(buf), - created_at=_UniffiConverterUInt64.read(buf), - completed_at=_UniffiConverterOptionalUInt64.read(buf), - number_of_parts=_UniffiConverterOptionalUInt64.read(buf), + return LnUrlWithdrawSuccessData( + invoice=_UniffiConverterString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.payment_hash) - _UniffiConverterTypePayStatus.check_lower(value.status) - _UniffiConverterOptionalString.check_lower(value.destination_pubkey) - _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) - _UniffiConverterOptionalUInt64.check_lower(value.amount_sent_msat) - _UniffiConverterOptionalString.check_lower(value.label) - _UniffiConverterOptionalString.check_lower(value.bolt11) - _UniffiConverterOptionalString.check_lower(value.description) - _UniffiConverterOptionalString.check_lower(value.bolt12) - _UniffiConverterOptionalString.check_lower(value.preimage) - _UniffiConverterUInt64.check_lower(value.created_at) - _UniffiConverterOptionalUInt64.check_lower(value.completed_at) - _UniffiConverterOptionalUInt64.check_lower(value.number_of_parts) + _UniffiConverterString.check_lower(value.invoice) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.payment_hash, buf) - _UniffiConverterTypePayStatus.write(value.status, buf) - _UniffiConverterOptionalString.write(value.destination_pubkey, buf) - _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) - _UniffiConverterOptionalUInt64.write(value.amount_sent_msat, buf) - _UniffiConverterOptionalString.write(value.label, buf) - _UniffiConverterOptionalString.write(value.bolt11, buf) - _UniffiConverterOptionalString.write(value.description, buf) - _UniffiConverterOptionalString.write(value.bolt12, buf) - _UniffiConverterOptionalString.write(value.preimage, buf) - _UniffiConverterUInt64.write(value.created_at, buf) - _UniffiConverterOptionalUInt64.write(value.completed_at, buf) - _UniffiConverterOptionalUInt64.write(value.number_of_parts, buf) + _UniffiConverterString.write(value.invoice, buf) -class Payment: - id: "str" - payment_type: "PaymentType" - payment_time: "int" - amount_msat: "int" - fee_msat: "int" - status: "PaymentStatus" - description: "typing.Optional[str]" - bolt11: "typing.Optional[str]" - preimage: "typing.Optional[str]" +class LogEntry: """ - Payment preimage as lowercase hex (64 chars), when known. + A single log message from the SDK. """ - destination: "typing.Optional[str]" + level: "LogLevel" + message: "str" + target: "str" + """ + The module that produced this log (e.g. "gl_client::scheduler"). """ - Pubkey of the counterparty in the payment, as lowercase hex - (66 chars). - For `PaymentType::Sent`: the recipient node we paid (when CLN - reports it). + file: "typing.Optional[str]" + """ + Source file path, if the log macro recorded one. + """ - For `PaymentType::Received`: always `None`. Lightning's privacy - model does not reveal the sender's pubkey to the recipient — the - HTLC arrives via one of our channel peers, but that peer is - usually just a router, not the original payer. The only pubkey - derivable from a paid invoice is the *payee* (i.e. our own - node), which is uninteresting to display per-row. + line: "typing.Optional[int]" + """ + Source line number, if the log macro recorded one. """ - def __init__(self, *, id: "str", payment_type: "PaymentType", payment_time: "int", amount_msat: "int", fee_msat: "int", status: "PaymentStatus", description: "typing.Optional[str]", bolt11: "typing.Optional[str]", preimage: "typing.Optional[str]", destination: "typing.Optional[str]"): - self.id = id - self.payment_type = payment_type - self.payment_time = payment_time - self.amount_msat = amount_msat - self.fee_msat = fee_msat - self.status = status - self.description = description - self.bolt11 = bolt11 - self.preimage = preimage - self.destination = destination + def __init__(self, *, level: "LogLevel", message: "str", target: "str", file: "typing.Optional[str]", line: "typing.Optional[int]"): + self.level = level + self.message = message + self.target = target + self.file = file + self.line = line def __str__(self): - return "Payment(id={}, payment_type={}, payment_time={}, amount_msat={}, fee_msat={}, status={}, description={}, bolt11={}, preimage={}, destination={})".format(self.id, self.payment_type, self.payment_time, self.amount_msat, self.fee_msat, self.status, self.description, self.bolt11, self.preimage, self.destination) + return "LogEntry(level={}, message={}, target={}, file={}, line={})".format(self.level, self.message, self.target, self.file, self.line) def __eq__(self, other): - if self.id != other.id: - return False - if self.payment_type != other.payment_type: - return False - if self.payment_time != other.payment_time: + if self.level != other.level: return False - if self.amount_msat != other.amount_msat: + if self.message != other.message: return False - if self.fee_msat != other.fee_msat: + if self.target != other.target: return False - if self.status != other.status: + if self.file != other.file: return False - if self.description != other.description: - return False - if self.bolt11 != other.bolt11: - return False - if self.preimage != other.preimage: - return False - if self.destination != other.destination: + if self.line != other.line: return False return True -class _UniffiConverterTypePayment(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLogEntry(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return Payment( - id=_UniffiConverterString.read(buf), - payment_type=_UniffiConverterTypePaymentType.read(buf), - payment_time=_UniffiConverterUInt64.read(buf), - amount_msat=_UniffiConverterUInt64.read(buf), - fee_msat=_UniffiConverterUInt64.read(buf), - status=_UniffiConverterTypePaymentStatus.read(buf), - description=_UniffiConverterOptionalString.read(buf), - bolt11=_UniffiConverterOptionalString.read(buf), - preimage=_UniffiConverterOptionalString.read(buf), - destination=_UniffiConverterOptionalString.read(buf), + return LogEntry( + level=_UniffiConverterTypeLogLevel.read(buf), + message=_UniffiConverterString.read(buf), + target=_UniffiConverterString.read(buf), + file=_UniffiConverterOptionalString.read(buf), + line=_UniffiConverterOptionalUInt32.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.id) - _UniffiConverterTypePaymentType.check_lower(value.payment_type) - _UniffiConverterUInt64.check_lower(value.payment_time) - _UniffiConverterUInt64.check_lower(value.amount_msat) - _UniffiConverterUInt64.check_lower(value.fee_msat) - _UniffiConverterTypePaymentStatus.check_lower(value.status) - _UniffiConverterOptionalString.check_lower(value.description) - _UniffiConverterOptionalString.check_lower(value.bolt11) - _UniffiConverterOptionalString.check_lower(value.preimage) - _UniffiConverterOptionalString.check_lower(value.destination) + _UniffiConverterTypeLogLevel.check_lower(value.level) + _UniffiConverterString.check_lower(value.message) + _UniffiConverterString.check_lower(value.target) + _UniffiConverterOptionalString.check_lower(value.file) + _UniffiConverterOptionalUInt32.check_lower(value.line) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.id, buf) - _UniffiConverterTypePaymentType.write(value.payment_type, buf) - _UniffiConverterUInt64.write(value.payment_time, buf) - _UniffiConverterUInt64.write(value.amount_msat, buf) - _UniffiConverterUInt64.write(value.fee_msat, buf) - _UniffiConverterTypePaymentStatus.write(value.status, buf) - _UniffiConverterOptionalString.write(value.description, buf) - _UniffiConverterOptionalString.write(value.bolt11, buf) - _UniffiConverterOptionalString.write(value.preimage, buf) - _UniffiConverterOptionalString.write(value.destination, buf) + _UniffiConverterTypeLogLevel.write(value.level, buf) + _UniffiConverterString.write(value.message, buf) + _UniffiConverterString.write(value.target, buf) + _UniffiConverterOptionalString.write(value.file, buf) + _UniffiConverterOptionalUInt32.write(value.line, buf) -class Peer: +class NodeState: + """ + A point-in-time snapshot of the node's balances, capacity, and + connectivity. Returned by `node_state()`. + + All amounts are in millisatoshis (1 sat = 1000 msat). + """ + id: "str" """ - Peer node public key as lowercase hex (66 chars). + The node's public key as a lowercase hex string (66 chars). """ - connected: "bool" - num_channels: "typing.Optional[int]" - netaddr: "typing.List[str]" - remote_addr: "typing.Optional[str]" - features: "typing.Optional[bytes]" - def __init__(self, *, id: "str", connected: "bool", num_channels: "typing.Optional[int]", netaddr: "typing.List[str]", remote_addr: "typing.Optional[str]", features: "typing.Optional[bytes]"): - self.id = id - self.connected = connected - self.num_channels = num_channels - self.netaddr = netaddr - self.remote_addr = remote_addr - self.features = features + block_height: "int" + """ + Latest block height the node has synced to. + """ - def __str__(self): - return "Peer(id={}, connected={}, num_channels={}, netaddr={}, remote_addr={}, features={})".format(self.id, self.connected, self.num_channels, self.netaddr, self.remote_addr, self.features) + network: "str" + """ + The Bitcoin network this node is running on (e.g. "bitcoin", "regtest"). + """ - def __eq__(self, other): - if self.id != other.id: - return False - if self.connected != other.connected: - return False - if self.num_channels != other.num_channels: - return False - if self.netaddr != other.netaddr: - return False - if self.remote_addr != other.remote_addr: - return False - if self.features != other.features: - return False - return True + version: "str" + """ + CLN version string (e.g. "v24.11"). + """ -class _UniffiConverterTypePeer(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - return Peer( - id=_UniffiConverterString.read(buf), - connected=_UniffiConverterBool.read(buf), - num_channels=_UniffiConverterOptionalUInt32.read(buf), - netaddr=_UniffiConverterSequenceString.read(buf), - remote_addr=_UniffiConverterOptionalString.read(buf), - features=_UniffiConverterOptionalBytes.read(buf), - ) + alias: "typing.Optional[str]" + """ + Human-readable node alias, if set. + """ - @staticmethod - def check_lower(value): - _UniffiConverterString.check_lower(value.id) - _UniffiConverterBool.check_lower(value.connected) - _UniffiConverterOptionalUInt32.check_lower(value.num_channels) - _UniffiConverterSequenceString.check_lower(value.netaddr) - _UniffiConverterOptionalString.check_lower(value.remote_addr) - _UniffiConverterOptionalBytes.check_lower(value.features) + color: "str" + """ + 3-byte RGB color of the node, as a lowercase hex string (6 chars). + """ - @staticmethod - def write(value, buf): - _UniffiConverterString.write(value.id, buf) - _UniffiConverterBool.write(value.connected, buf) - _UniffiConverterOptionalUInt32.write(value.num_channels, buf) - _UniffiConverterSequenceString.write(value.netaddr, buf) - _UniffiConverterOptionalString.write(value.remote_addr, buf) - _UniffiConverterOptionalBytes.write(value.features, buf) + num_active_channels: "int" + """ + Number of channels that are open and operational. These are the + channels that contribute to `channels_balance_msat`, + `max_payable_msat`, `total_channel_capacity_msat`, and + `total_inbound_liquidity_msat`. + """ + num_pending_channels: "int" + """ + Number of channels that are being opened but not yet confirmed. + Pending channels do not contribute to any balance or capacity + field on this snapshot; their funds show up only after they + transition to active. + """ -class PeerChannel: - peer_id: "str" + num_inactive_channels: "int" """ - Peer node public key as lowercase hex (66 chars). + Number of channels that are open but the peer is offline. + Inactive channels hold balance but cannot be used for payments + until the peer reconnects; they do not contribute to + `max_payable_msat` or `total_inbound_liquidity_msat` (those are + computed from the live `spendable_msat` / `receivable_msat` + reported by CLN, which goes to zero when the peer is offline). """ - peer_connected: "bool" - state: "ChannelState" - short_channel_id: "typing.Optional[str]" - channel_id: "typing.Optional[str]" + channels_balance_msat: "int" """ - Channel id as lowercase hex (64 chars). + Total our-side balance across all open channels, including amounts + that protocol reserves make unspendable. + + This is the field a wallet's home screen should show as the + user's "Lightning balance" — it reflects what they own off-chain, + matching what they'd expect to see at a glance. + + Do **not** use this to gate a send button: some of it is locked + in channel reserves. Use `max_payable_msat` for that. """ - funding_txid: "typing.Optional[str]" + max_payable_msat: "int" """ - Funding transaction id as lowercase hex (64 chars). + Aggregate spendable amount across all open channels. Equal to + `channels_balance_msat - max_chan_reserve_msat`. + + This is the field a send screen should gate against — it is what + the user can actually move right now over Lightning in total. + + Caveat: a single Lightning payment is additionally bounded by + the largest channel's own `spendable_msat`. Reaching this full + aggregate amount in one payment requires multi-path-payment + support from the recipient and a working route. """ - funding_outnum: "typing.Optional[int]" - to_us_msat: "typing.Optional[int]" - total_msat: "typing.Optional[int]" - spendable_msat: "typing.Optional[int]" - receivable_msat: "typing.Optional[int]" - closer: "typing.Optional[ChannelSide]" + total_channel_capacity_msat: "int" """ - Which side initiated the close, if the channel is closing or closed. + Sum of all open channel capacities (your side + remote side). """ - status: "typing.List[str]" + max_chan_reserve_msat: "int" """ - Human-readable status strings from CLN, ordered oldest to newest. - For a channel in `Onchain` state, the last entry indicates whether - our payout is still timelocked (`DELAYED_OUTPUT_TO_US`) or already - available in the on-chain balance. + Amount locked in protocol channel reserves, computed as + `channels_balance_msat - max_payable_msat`. These sats are yours + on paper but cannot be spent until the channel closes. """ - def __init__(self, *, peer_id: "str", peer_connected: "bool", state: "ChannelState", short_channel_id: "typing.Optional[str]", channel_id: "typing.Optional[str]", funding_txid: "typing.Optional[str]", funding_outnum: "typing.Optional[int]", to_us_msat: "typing.Optional[int]", total_msat: "typing.Optional[int]", spendable_msat: "typing.Optional[int]", receivable_msat: "typing.Optional[int]", closer: "typing.Optional[ChannelSide]", status: "typing.List[str]"): - self.peer_id = peer_id - self.peer_connected = peer_connected - self.state = state - self.short_channel_id = short_channel_id - self.channel_id = channel_id - self.funding_txid = funding_txid - self.funding_outnum = funding_outnum - self.to_us_msat = to_us_msat - self.total_msat = total_msat - self.spendable_msat = spendable_msat - self.receivable_msat = receivable_msat - self.closer = closer - self.status = status + onchain_balance_msat: "int" + """ + Confirmed on-chain balance available for spending or opening channels. + """ - def __str__(self): - return "PeerChannel(peer_id={}, peer_connected={}, state={}, short_channel_id={}, channel_id={}, funding_txid={}, funding_outnum={}, to_us_msat={}, total_msat={}, spendable_msat={}, receivable_msat={}, closer={}, status={})".format(self.peer_id, self.peer_connected, self.state, self.short_channel_id, self.channel_id, self.funding_txid, self.funding_outnum, self.to_us_msat, self.total_msat, self.spendable_msat, self.receivable_msat, self.closer, self.status) + unconfirmed_onchain_balance_msat: "int" + """ + On-chain balance from transactions that have not yet been confirmed. + """ - def __eq__(self, other): - if self.peer_id != other.peer_id: - return False - if self.peer_connected != other.peer_connected: - return False - if self.state != other.state: - return False - if self.short_channel_id != other.short_channel_id: - return False - if self.channel_id != other.channel_id: - return False - if self.funding_txid != other.funding_txid: - return False - if self.funding_outnum != other.funding_outnum: - return False - if self.to_us_msat != other.to_us_msat: - return False - if self.total_msat != other.total_msat: - return False - if self.spendable_msat != other.spendable_msat: - return False - if self.receivable_msat != other.receivable_msat: + immature_onchain_balance_msat: "int" + """ + On-chain balance confirmed but not yet spendable (e.g. coinbase + outputs inside the 100-block maturation window). + """ + + pending_onchain_balance_msat: "int" + """ + On-chain balance locked in channels that are being closed. + These funds will become available once the close is confirmed. + """ + + max_receivable_single_payment_msat: "int" + """ + Largest single Lightning payment the node can receive without + splitting across channels. Bounded by the inbound capacity of + the largest open channel. + """ + + total_inbound_liquidity_msat: "int" + """ + Total amount you can receive across all open channels combined. + """ + + connected_channel_peers: "typing.List[str]" + """ + Lowercase hex public keys of peers we have at least one channel + with and are currently connected to. Peers we're connected to but + have no channel with are not represented here; for routing-node + use cases, query `list_peers()` directly. + """ + + utxos: "typing.List[FundOutput]" + """ + Unspent on-chain outputs owned by the node's wallet. Excludes + spent outputs; includes confirmed, unconfirmed, immature, and + reserved UTXOs (callers can filter by `status` and `reserved`). + """ + + total_onchain_msat: "int" + """ + All non-pending on-chain balance buckets summed: + `onchain_balance_msat + unconfirmed_onchain_balance_msat + immature_onchain_balance_msat`. + Excludes funds locked in closing channels (`pending_onchain_balance_msat`) + since those are not yet on-chain UTXOs. + """ + + total_balance_msat: "int" + """ + Everything the user owns, summed: channel balance (including + protocol reserves) + all on-chain buckets + funds locked in + closing channels. The "total holdings" number a wallet home + screen typically shows. + """ + + spendable_balance_msat: "int" + """ + What the user can spend *right now*: + `max_payable_msat + onchain_balance_msat`. Excludes reserves, + unconfirmed, immature, and pending amounts. The number a + send-money screen should gate against. + """ + + def __init__(self, *, id: "str", block_height: "int", network: "str", version: "str", alias: "typing.Optional[str]", color: "str", num_active_channels: "int", num_pending_channels: "int", num_inactive_channels: "int", channels_balance_msat: "int", max_payable_msat: "int", total_channel_capacity_msat: "int", max_chan_reserve_msat: "int", onchain_balance_msat: "int", unconfirmed_onchain_balance_msat: "int", immature_onchain_balance_msat: "int", pending_onchain_balance_msat: "int", max_receivable_single_payment_msat: "int", total_inbound_liquidity_msat: "int", connected_channel_peers: "typing.List[str]", utxos: "typing.List[FundOutput]", total_onchain_msat: "int", total_balance_msat: "int", spendable_balance_msat: "int"): + self.id = id + self.block_height = block_height + self.network = network + self.version = version + self.alias = alias + self.color = color + self.num_active_channels = num_active_channels + self.num_pending_channels = num_pending_channels + self.num_inactive_channels = num_inactive_channels + self.channels_balance_msat = channels_balance_msat + self.max_payable_msat = max_payable_msat + self.total_channel_capacity_msat = total_channel_capacity_msat + self.max_chan_reserve_msat = max_chan_reserve_msat + self.onchain_balance_msat = onchain_balance_msat + self.unconfirmed_onchain_balance_msat = unconfirmed_onchain_balance_msat + self.immature_onchain_balance_msat = immature_onchain_balance_msat + self.pending_onchain_balance_msat = pending_onchain_balance_msat + self.max_receivable_single_payment_msat = max_receivable_single_payment_msat + self.total_inbound_liquidity_msat = total_inbound_liquidity_msat + self.connected_channel_peers = connected_channel_peers + self.utxos = utxos + self.total_onchain_msat = total_onchain_msat + self.total_balance_msat = total_balance_msat + self.spendable_balance_msat = spendable_balance_msat + + def __str__(self): + return "NodeState(id={}, block_height={}, network={}, version={}, alias={}, color={}, num_active_channels={}, num_pending_channels={}, num_inactive_channels={}, channels_balance_msat={}, max_payable_msat={}, total_channel_capacity_msat={}, max_chan_reserve_msat={}, onchain_balance_msat={}, unconfirmed_onchain_balance_msat={}, immature_onchain_balance_msat={}, pending_onchain_balance_msat={}, max_receivable_single_payment_msat={}, total_inbound_liquidity_msat={}, connected_channel_peers={}, utxos={}, total_onchain_msat={}, total_balance_msat={}, spendable_balance_msat={})".format(self.id, self.block_height, self.network, self.version, self.alias, self.color, self.num_active_channels, self.num_pending_channels, self.num_inactive_channels, self.channels_balance_msat, self.max_payable_msat, self.total_channel_capacity_msat, self.max_chan_reserve_msat, self.onchain_balance_msat, self.unconfirmed_onchain_balance_msat, self.immature_onchain_balance_msat, self.pending_onchain_balance_msat, self.max_receivable_single_payment_msat, self.total_inbound_liquidity_msat, self.connected_channel_peers, self.utxos, self.total_onchain_msat, self.total_balance_msat, self.spendable_balance_msat) + + def __eq__(self, other): + if self.id != other.id: return False - if self.closer != other.closer: + if self.block_height != other.block_height: return False - if self.status != other.status: + if self.network != other.network: + return False + if self.version != other.version: + return False + if self.alias != other.alias: + return False + if self.color != other.color: + return False + if self.num_active_channels != other.num_active_channels: + return False + if self.num_pending_channels != other.num_pending_channels: + return False + if self.num_inactive_channels != other.num_inactive_channels: + return False + if self.channels_balance_msat != other.channels_balance_msat: + return False + if self.max_payable_msat != other.max_payable_msat: + return False + if self.total_channel_capacity_msat != other.total_channel_capacity_msat: + return False + if self.max_chan_reserve_msat != other.max_chan_reserve_msat: + return False + if self.onchain_balance_msat != other.onchain_balance_msat: + return False + if self.unconfirmed_onchain_balance_msat != other.unconfirmed_onchain_balance_msat: + return False + if self.immature_onchain_balance_msat != other.immature_onchain_balance_msat: + return False + if self.pending_onchain_balance_msat != other.pending_onchain_balance_msat: + return False + if self.max_receivable_single_payment_msat != other.max_receivable_single_payment_msat: + return False + if self.total_inbound_liquidity_msat != other.total_inbound_liquidity_msat: + return False + if self.connected_channel_peers != other.connected_channel_peers: + return False + if self.utxos != other.utxos: + return False + if self.total_onchain_msat != other.total_onchain_msat: + return False + if self.total_balance_msat != other.total_balance_msat: + return False + if self.spendable_balance_msat != other.spendable_balance_msat: + return False + return True + +class _UniffiConverterTypeNodeState(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return NodeState( + id=_UniffiConverterString.read(buf), + block_height=_UniffiConverterUInt32.read(buf), + network=_UniffiConverterString.read(buf), + version=_UniffiConverterString.read(buf), + alias=_UniffiConverterOptionalString.read(buf), + color=_UniffiConverterString.read(buf), + num_active_channels=_UniffiConverterUInt32.read(buf), + num_pending_channels=_UniffiConverterUInt32.read(buf), + num_inactive_channels=_UniffiConverterUInt32.read(buf), + channels_balance_msat=_UniffiConverterUInt64.read(buf), + max_payable_msat=_UniffiConverterUInt64.read(buf), + total_channel_capacity_msat=_UniffiConverterUInt64.read(buf), + max_chan_reserve_msat=_UniffiConverterUInt64.read(buf), + onchain_balance_msat=_UniffiConverterUInt64.read(buf), + unconfirmed_onchain_balance_msat=_UniffiConverterUInt64.read(buf), + immature_onchain_balance_msat=_UniffiConverterUInt64.read(buf), + pending_onchain_balance_msat=_UniffiConverterUInt64.read(buf), + max_receivable_single_payment_msat=_UniffiConverterUInt64.read(buf), + total_inbound_liquidity_msat=_UniffiConverterUInt64.read(buf), + connected_channel_peers=_UniffiConverterSequenceString.read(buf), + utxos=_UniffiConverterSequenceTypeFundOutput.read(buf), + total_onchain_msat=_UniffiConverterUInt64.read(buf), + total_balance_msat=_UniffiConverterUInt64.read(buf), + spendable_balance_msat=_UniffiConverterUInt64.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.id) + _UniffiConverterUInt32.check_lower(value.block_height) + _UniffiConverterString.check_lower(value.network) + _UniffiConverterString.check_lower(value.version) + _UniffiConverterOptionalString.check_lower(value.alias) + _UniffiConverterString.check_lower(value.color) + _UniffiConverterUInt32.check_lower(value.num_active_channels) + _UniffiConverterUInt32.check_lower(value.num_pending_channels) + _UniffiConverterUInt32.check_lower(value.num_inactive_channels) + _UniffiConverterUInt64.check_lower(value.channels_balance_msat) + _UniffiConverterUInt64.check_lower(value.max_payable_msat) + _UniffiConverterUInt64.check_lower(value.total_channel_capacity_msat) + _UniffiConverterUInt64.check_lower(value.max_chan_reserve_msat) + _UniffiConverterUInt64.check_lower(value.onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.unconfirmed_onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.immature_onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.pending_onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.max_receivable_single_payment_msat) + _UniffiConverterUInt64.check_lower(value.total_inbound_liquidity_msat) + _UniffiConverterSequenceString.check_lower(value.connected_channel_peers) + _UniffiConverterSequenceTypeFundOutput.check_lower(value.utxos) + _UniffiConverterUInt64.check_lower(value.total_onchain_msat) + _UniffiConverterUInt64.check_lower(value.total_balance_msat) + _UniffiConverterUInt64.check_lower(value.spendable_balance_msat) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.id, buf) + _UniffiConverterUInt32.write(value.block_height, buf) + _UniffiConverterString.write(value.network, buf) + _UniffiConverterString.write(value.version, buf) + _UniffiConverterOptionalString.write(value.alias, buf) + _UniffiConverterString.write(value.color, buf) + _UniffiConverterUInt32.write(value.num_active_channels, buf) + _UniffiConverterUInt32.write(value.num_pending_channels, buf) + _UniffiConverterUInt32.write(value.num_inactive_channels, buf) + _UniffiConverterUInt64.write(value.channels_balance_msat, buf) + _UniffiConverterUInt64.write(value.max_payable_msat, buf) + _UniffiConverterUInt64.write(value.total_channel_capacity_msat, buf) + _UniffiConverterUInt64.write(value.max_chan_reserve_msat, buf) + _UniffiConverterUInt64.write(value.onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.unconfirmed_onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.immature_onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.pending_onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.max_receivable_single_payment_msat, buf) + _UniffiConverterUInt64.write(value.total_inbound_liquidity_msat, buf) + _UniffiConverterSequenceString.write(value.connected_channel_peers, buf) + _UniffiConverterSequenceTypeFundOutput.write(value.utxos, buf) + _UniffiConverterUInt64.write(value.total_onchain_msat, buf) + _UniffiConverterUInt64.write(value.total_balance_msat, buf) + _UniffiConverterUInt64.write(value.spendable_balance_msat, buf) + + +class OnchainReceiveResponse: + """ + A pair of on-chain addresses for receiving funds. + """ + + bech32: "str" + """ + SegWit v0 (bech32) address — starts with `bc1q` on mainnet. + """ + + p2tr: "str" + """ + Taproot (bech32m) address — starts with `bc1p` on mainnet. + """ + + def __init__(self, *, bech32: "str", p2tr: "str"): + self.bech32 = bech32 + self.p2tr = p2tr + + def __str__(self): + return "OnchainReceiveResponse(bech32={}, p2tr={})".format(self.bech32, self.p2tr) + + def __eq__(self, other): + if self.bech32 != other.bech32: + return False + if self.p2tr != other.p2tr: return False return True -class _UniffiConverterTypePeerChannel(_UniffiConverterRustBuffer): +class _UniffiConverterTypeOnchainReceiveResponse(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return OnchainReceiveResponse( + bech32=_UniffiConverterString.read(buf), + p2tr=_UniffiConverterString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.bech32) + _UniffiConverterString.check_lower(value.p2tr) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.bech32, buf) + _UniffiConverterString.write(value.p2tr, buf) + + +class OnchainSendResponse: + """ + Result of an on-chain send. The transaction has already been broadcast. + """ + + tx: "bytes" + """ + The raw signed transaction bytes. + """ + + txid: "str" + """ + The transaction id as lowercase hex (64 chars). + """ + + psbt: "str" + """ + The transaction as a Partially Signed Bitcoin Transaction string. + """ + + def __init__(self, *, tx: "bytes", txid: "str", psbt: "str"): + self.tx = tx + self.txid = txid + self.psbt = psbt + + def __str__(self): + return "OnchainSendResponse(tx={}, txid={}, psbt={})".format(self.tx, self.txid, self.psbt) + + def __eq__(self, other): + if self.tx != other.tx: + return False + if self.txid != other.txid: + return False + if self.psbt != other.psbt: + return False + return True + +class _UniffiConverterTypeOnchainSendResponse(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return OnchainSendResponse( + tx=_UniffiConverterBytes.read(buf), + txid=_UniffiConverterString.read(buf), + psbt=_UniffiConverterString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterBytes.check_lower(value.tx) + _UniffiConverterString.check_lower(value.txid) + _UniffiConverterString.check_lower(value.psbt) + + @staticmethod + def write(value, buf): + _UniffiConverterBytes.write(value.tx, buf) + _UniffiConverterString.write(value.txid, buf) + _UniffiConverterString.write(value.psbt, buf) + + +class ParsedInvoice: + """ + Parsed BOLT11 invoice with extracted fields. + """ + + bolt11: "str" + """ + The original invoice string. + """ + + payee_pubkey: "typing.Optional[str]" + """ + Recipient public key as lowercase hex (66 chars), recovered from the invoice signature. + """ + + payment_hash: "str" + """ + Payment hash as lowercase hex (64 chars) identifying this payment. + """ + + description: "typing.Optional[str]" + """ + Invoice description. None if the invoice uses a description hash. + """ + + amount_msat: "typing.Optional[int]" + """ + Requested amount in millisatoshis. None for "any amount" invoices. + """ + + expiry: "int" + """ + Seconds from creation until the invoice expires. + """ + + timestamp: "int" + """ + Unix timestamp (seconds) when the invoice was created. + """ + + def __init__(self, *, bolt11: "str", payee_pubkey: "typing.Optional[str]", payment_hash: "str", description: "typing.Optional[str]", amount_msat: "typing.Optional[int]", expiry: "int", timestamp: "int"): + self.bolt11 = bolt11 + self.payee_pubkey = payee_pubkey + self.payment_hash = payment_hash + self.description = description + self.amount_msat = amount_msat + self.expiry = expiry + self.timestamp = timestamp + + def __str__(self): + return "ParsedInvoice(bolt11={}, payee_pubkey={}, payment_hash={}, description={}, amount_msat={}, expiry={}, timestamp={})".format(self.bolt11, self.payee_pubkey, self.payment_hash, self.description, self.amount_msat, self.expiry, self.timestamp) + + def __eq__(self, other): + if self.bolt11 != other.bolt11: + return False + if self.payee_pubkey != other.payee_pubkey: + return False + if self.payment_hash != other.payment_hash: + return False + if self.description != other.description: + return False + if self.amount_msat != other.amount_msat: + return False + if self.expiry != other.expiry: + return False + if self.timestamp != other.timestamp: + return False + return True + +class _UniffiConverterTypeParsedInvoice(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return ParsedInvoice( + bolt11=_UniffiConverterString.read(buf), + payee_pubkey=_UniffiConverterOptionalString.read(buf), + payment_hash=_UniffiConverterString.read(buf), + description=_UniffiConverterOptionalString.read(buf), + amount_msat=_UniffiConverterOptionalUInt64.read(buf), + expiry=_UniffiConverterUInt64.read(buf), + timestamp=_UniffiConverterUInt64.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.bolt11) + _UniffiConverterOptionalString.check_lower(value.payee_pubkey) + _UniffiConverterString.check_lower(value.payment_hash) + _UniffiConverterOptionalString.check_lower(value.description) + _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) + _UniffiConverterUInt64.check_lower(value.expiry) + _UniffiConverterUInt64.check_lower(value.timestamp) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.bolt11, buf) + _UniffiConverterOptionalString.write(value.payee_pubkey, buf) + _UniffiConverterString.write(value.payment_hash, buf) + _UniffiConverterOptionalString.write(value.description, buf) + _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) + _UniffiConverterUInt64.write(value.expiry, buf) + _UniffiConverterUInt64.write(value.timestamp, buf) + + +class Pay: + payment_hash: "str" + """ + Payment hash as lowercase hex (64 chars). + """ + + status: "PayStatus" + destination_pubkey: "typing.Optional[str]" + """ + Recipient node pubkey as lowercase hex (66 chars), if known. + """ + + amount_msat: "typing.Optional[int]" + amount_sent_msat: "typing.Optional[int]" + label: "typing.Optional[str]" + bolt11: "typing.Optional[str]" + description: "typing.Optional[str]" + bolt12: "typing.Optional[str]" + preimage: "typing.Optional[str]" + """ + Payment preimage as lowercase hex (64 chars), if the payment completed. + """ + + created_at: "int" + completed_at: "typing.Optional[int]" + number_of_parts: "typing.Optional[int]" + def __init__(self, *, payment_hash: "str", status: "PayStatus", destination_pubkey: "typing.Optional[str]", amount_msat: "typing.Optional[int]", amount_sent_msat: "typing.Optional[int]", label: "typing.Optional[str]", bolt11: "typing.Optional[str]", description: "typing.Optional[str]", bolt12: "typing.Optional[str]", preimage: "typing.Optional[str]", created_at: "int", completed_at: "typing.Optional[int]", number_of_parts: "typing.Optional[int]"): + self.payment_hash = payment_hash + self.status = status + self.destination_pubkey = destination_pubkey + self.amount_msat = amount_msat + self.amount_sent_msat = amount_sent_msat + self.label = label + self.bolt11 = bolt11 + self.description = description + self.bolt12 = bolt12 + self.preimage = preimage + self.created_at = created_at + self.completed_at = completed_at + self.number_of_parts = number_of_parts + + def __str__(self): + return "Pay(payment_hash={}, status={}, destination_pubkey={}, amount_msat={}, amount_sent_msat={}, label={}, bolt11={}, description={}, bolt12={}, preimage={}, created_at={}, completed_at={}, number_of_parts={})".format(self.payment_hash, self.status, self.destination_pubkey, self.amount_msat, self.amount_sent_msat, self.label, self.bolt11, self.description, self.bolt12, self.preimage, self.created_at, self.completed_at, self.number_of_parts) + + def __eq__(self, other): + if self.payment_hash != other.payment_hash: + return False + if self.status != other.status: + return False + if self.destination_pubkey != other.destination_pubkey: + return False + if self.amount_msat != other.amount_msat: + return False + if self.amount_sent_msat != other.amount_sent_msat: + return False + if self.label != other.label: + return False + if self.bolt11 != other.bolt11: + return False + if self.description != other.description: + return False + if self.bolt12 != other.bolt12: + return False + if self.preimage != other.preimage: + return False + if self.created_at != other.created_at: + return False + if self.completed_at != other.completed_at: + return False + if self.number_of_parts != other.number_of_parts: + return False + return True + +class _UniffiConverterTypePay(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return Pay( + payment_hash=_UniffiConverterString.read(buf), + status=_UniffiConverterTypePayStatus.read(buf), + destination_pubkey=_UniffiConverterOptionalString.read(buf), + amount_msat=_UniffiConverterOptionalUInt64.read(buf), + amount_sent_msat=_UniffiConverterOptionalUInt64.read(buf), + label=_UniffiConverterOptionalString.read(buf), + bolt11=_UniffiConverterOptionalString.read(buf), + description=_UniffiConverterOptionalString.read(buf), + bolt12=_UniffiConverterOptionalString.read(buf), + preimage=_UniffiConverterOptionalString.read(buf), + created_at=_UniffiConverterUInt64.read(buf), + completed_at=_UniffiConverterOptionalUInt64.read(buf), + number_of_parts=_UniffiConverterOptionalUInt64.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.payment_hash) + _UniffiConverterTypePayStatus.check_lower(value.status) + _UniffiConverterOptionalString.check_lower(value.destination_pubkey) + _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) + _UniffiConverterOptionalUInt64.check_lower(value.amount_sent_msat) + _UniffiConverterOptionalString.check_lower(value.label) + _UniffiConverterOptionalString.check_lower(value.bolt11) + _UniffiConverterOptionalString.check_lower(value.description) + _UniffiConverterOptionalString.check_lower(value.bolt12) + _UniffiConverterOptionalString.check_lower(value.preimage) + _UniffiConverterUInt64.check_lower(value.created_at) + _UniffiConverterOptionalUInt64.check_lower(value.completed_at) + _UniffiConverterOptionalUInt64.check_lower(value.number_of_parts) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.payment_hash, buf) + _UniffiConverterTypePayStatus.write(value.status, buf) + _UniffiConverterOptionalString.write(value.destination_pubkey, buf) + _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) + _UniffiConverterOptionalUInt64.write(value.amount_sent_msat, buf) + _UniffiConverterOptionalString.write(value.label, buf) + _UniffiConverterOptionalString.write(value.bolt11, buf) + _UniffiConverterOptionalString.write(value.description, buf) + _UniffiConverterOptionalString.write(value.bolt12, buf) + _UniffiConverterOptionalString.write(value.preimage, buf) + _UniffiConverterUInt64.write(value.created_at, buf) + _UniffiConverterOptionalUInt64.write(value.completed_at, buf) + _UniffiConverterOptionalUInt64.write(value.number_of_parts, buf) + + +class Payment: + id: "str" + payment_type: "PaymentType" + payment_time: "int" + amount_msat: "int" + fee_msat: "int" + status: "PaymentStatus" + description: "typing.Optional[str]" + bolt11: "typing.Optional[str]" + preimage: "typing.Optional[str]" + """ + Payment preimage as lowercase hex (64 chars), when known. + """ + + destination: "typing.Optional[str]" + """ + Pubkey of the counterparty in the payment, as lowercase hex + (66 chars). + + For `PaymentType::Sent`: the recipient node we paid (when CLN + reports it). + + For `PaymentType::Received`: always `None`. Lightning's privacy + model does not reveal the sender's pubkey to the recipient — the + HTLC arrives via one of our channel peers, but that peer is + usually just a router, not the original payer. The only pubkey + derivable from a paid invoice is the *payee* (i.e. our own + node), which is uninteresting to display per-row. + """ + + def __init__(self, *, id: "str", payment_type: "PaymentType", payment_time: "int", amount_msat: "int", fee_msat: "int", status: "PaymentStatus", description: "typing.Optional[str]", bolt11: "typing.Optional[str]", preimage: "typing.Optional[str]", destination: "typing.Optional[str]"): + self.id = id + self.payment_type = payment_type + self.payment_time = payment_time + self.amount_msat = amount_msat + self.fee_msat = fee_msat + self.status = status + self.description = description + self.bolt11 = bolt11 + self.preimage = preimage + self.destination = destination + + def __str__(self): + return "Payment(id={}, payment_type={}, payment_time={}, amount_msat={}, fee_msat={}, status={}, description={}, bolt11={}, preimage={}, destination={})".format(self.id, self.payment_type, self.payment_time, self.amount_msat, self.fee_msat, self.status, self.description, self.bolt11, self.preimage, self.destination) + + def __eq__(self, other): + if self.id != other.id: + return False + if self.payment_type != other.payment_type: + return False + if self.payment_time != other.payment_time: + return False + if self.amount_msat != other.amount_msat: + return False + if self.fee_msat != other.fee_msat: + return False + if self.status != other.status: + return False + if self.description != other.description: + return False + if self.bolt11 != other.bolt11: + return False + if self.preimage != other.preimage: + return False + if self.destination != other.destination: + return False + return True + +class _UniffiConverterTypePayment(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return Payment( + id=_UniffiConverterString.read(buf), + payment_type=_UniffiConverterTypePaymentType.read(buf), + payment_time=_UniffiConverterUInt64.read(buf), + amount_msat=_UniffiConverterUInt64.read(buf), + fee_msat=_UniffiConverterUInt64.read(buf), + status=_UniffiConverterTypePaymentStatus.read(buf), + description=_UniffiConverterOptionalString.read(buf), + bolt11=_UniffiConverterOptionalString.read(buf), + preimage=_UniffiConverterOptionalString.read(buf), + destination=_UniffiConverterOptionalString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.id) + _UniffiConverterTypePaymentType.check_lower(value.payment_type) + _UniffiConverterUInt64.check_lower(value.payment_time) + _UniffiConverterUInt64.check_lower(value.amount_msat) + _UniffiConverterUInt64.check_lower(value.fee_msat) + _UniffiConverterTypePaymentStatus.check_lower(value.status) + _UniffiConverterOptionalString.check_lower(value.description) + _UniffiConverterOptionalString.check_lower(value.bolt11) + _UniffiConverterOptionalString.check_lower(value.preimage) + _UniffiConverterOptionalString.check_lower(value.destination) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.id, buf) + _UniffiConverterTypePaymentType.write(value.payment_type, buf) + _UniffiConverterUInt64.write(value.payment_time, buf) + _UniffiConverterUInt64.write(value.amount_msat, buf) + _UniffiConverterUInt64.write(value.fee_msat, buf) + _UniffiConverterTypePaymentStatus.write(value.status, buf) + _UniffiConverterOptionalString.write(value.description, buf) + _UniffiConverterOptionalString.write(value.bolt11, buf) + _UniffiConverterOptionalString.write(value.preimage, buf) + _UniffiConverterOptionalString.write(value.destination, buf) + + +class Peer: + id: "str" + """ + Peer node public key as lowercase hex (66 chars). + """ + + connected: "bool" + num_channels: "typing.Optional[int]" + netaddr: "typing.List[str]" + remote_addr: "typing.Optional[str]" + features: "typing.Optional[bytes]" + def __init__(self, *, id: "str", connected: "bool", num_channels: "typing.Optional[int]", netaddr: "typing.List[str]", remote_addr: "typing.Optional[str]", features: "typing.Optional[bytes]"): + self.id = id + self.connected = connected + self.num_channels = num_channels + self.netaddr = netaddr + self.remote_addr = remote_addr + self.features = features + + def __str__(self): + return "Peer(id={}, connected={}, num_channels={}, netaddr={}, remote_addr={}, features={})".format(self.id, self.connected, self.num_channels, self.netaddr, self.remote_addr, self.features) + + def __eq__(self, other): + if self.id != other.id: + return False + if self.connected != other.connected: + return False + if self.num_channels != other.num_channels: + return False + if self.netaddr != other.netaddr: + return False + if self.remote_addr != other.remote_addr: + return False + if self.features != other.features: + return False + return True + +class _UniffiConverterTypePeer(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return Peer( + id=_UniffiConverterString.read(buf), + connected=_UniffiConverterBool.read(buf), + num_channels=_UniffiConverterOptionalUInt32.read(buf), + netaddr=_UniffiConverterSequenceString.read(buf), + remote_addr=_UniffiConverterOptionalString.read(buf), + features=_UniffiConverterOptionalBytes.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.id) + _UniffiConverterBool.check_lower(value.connected) + _UniffiConverterOptionalUInt32.check_lower(value.num_channels) + _UniffiConverterSequenceString.check_lower(value.netaddr) + _UniffiConverterOptionalString.check_lower(value.remote_addr) + _UniffiConverterOptionalBytes.check_lower(value.features) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.id, buf) + _UniffiConverterBool.write(value.connected, buf) + _UniffiConverterOptionalUInt32.write(value.num_channels, buf) + _UniffiConverterSequenceString.write(value.netaddr, buf) + _UniffiConverterOptionalString.write(value.remote_addr, buf) + _UniffiConverterOptionalBytes.write(value.features, buf) + + +class PeerChannel: + peer_id: "str" + """ + Peer node public key as lowercase hex (66 chars). + """ + + peer_connected: "bool" + state: "ChannelState" + short_channel_id: "typing.Optional[str]" + channel_id: "typing.Optional[str]" + """ + Channel id as lowercase hex (64 chars). + """ + + funding_txid: "typing.Optional[str]" + """ + Funding transaction id as lowercase hex (64 chars). + """ + + funding_outnum: "typing.Optional[int]" + to_us_msat: "typing.Optional[int]" + total_msat: "typing.Optional[int]" + spendable_msat: "typing.Optional[int]" + receivable_msat: "typing.Optional[int]" + closer: "typing.Optional[ChannelSide]" + """ + Which side initiated the close, if the channel is closing or closed. + """ + + status: "typing.List[str]" + """ + Human-readable status strings from CLN, ordered oldest to newest. + For a channel in `Onchain` state, the last entry indicates whether + our payout is still timelocked (`DELAYED_OUTPUT_TO_US`) or already + available in the on-chain balance. + """ + + def __init__(self, *, peer_id: "str", peer_connected: "bool", state: "ChannelState", short_channel_id: "typing.Optional[str]", channel_id: "typing.Optional[str]", funding_txid: "typing.Optional[str]", funding_outnum: "typing.Optional[int]", to_us_msat: "typing.Optional[int]", total_msat: "typing.Optional[int]", spendable_msat: "typing.Optional[int]", receivable_msat: "typing.Optional[int]", closer: "typing.Optional[ChannelSide]", status: "typing.List[str]"): + self.peer_id = peer_id + self.peer_connected = peer_connected + self.state = state + self.short_channel_id = short_channel_id + self.channel_id = channel_id + self.funding_txid = funding_txid + self.funding_outnum = funding_outnum + self.to_us_msat = to_us_msat + self.total_msat = total_msat + self.spendable_msat = spendable_msat + self.receivable_msat = receivable_msat + self.closer = closer + self.status = status + + def __str__(self): + return "PeerChannel(peer_id={}, peer_connected={}, state={}, short_channel_id={}, channel_id={}, funding_txid={}, funding_outnum={}, to_us_msat={}, total_msat={}, spendable_msat={}, receivable_msat={}, closer={}, status={})".format(self.peer_id, self.peer_connected, self.state, self.short_channel_id, self.channel_id, self.funding_txid, self.funding_outnum, self.to_us_msat, self.total_msat, self.spendable_msat, self.receivable_msat, self.closer, self.status) + + def __eq__(self, other): + if self.peer_id != other.peer_id: + return False + if self.peer_connected != other.peer_connected: + return False + if self.state != other.state: + return False + if self.short_channel_id != other.short_channel_id: + return False + if self.channel_id != other.channel_id: + return False + if self.funding_txid != other.funding_txid: + return False + if self.funding_outnum != other.funding_outnum: + return False + if self.to_us_msat != other.to_us_msat: + return False + if self.total_msat != other.total_msat: + return False + if self.spendable_msat != other.spendable_msat: + return False + if self.receivable_msat != other.receivable_msat: + return False + if self.closer != other.closer: + return False + if self.status != other.status: + return False + return True + +class _UniffiConverterTypePeerChannel(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return PeerChannel( + peer_id=_UniffiConverterString.read(buf), + peer_connected=_UniffiConverterBool.read(buf), + state=_UniffiConverterTypeChannelState.read(buf), + short_channel_id=_UniffiConverterOptionalString.read(buf), + channel_id=_UniffiConverterOptionalString.read(buf), + funding_txid=_UniffiConverterOptionalString.read(buf), + funding_outnum=_UniffiConverterOptionalUInt32.read(buf), + to_us_msat=_UniffiConverterOptionalUInt64.read(buf), + total_msat=_UniffiConverterOptionalUInt64.read(buf), + spendable_msat=_UniffiConverterOptionalUInt64.read(buf), + receivable_msat=_UniffiConverterOptionalUInt64.read(buf), + closer=_UniffiConverterOptionalTypeChannelSide.read(buf), + status=_UniffiConverterSequenceString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.peer_id) + _UniffiConverterBool.check_lower(value.peer_connected) + _UniffiConverterTypeChannelState.check_lower(value.state) + _UniffiConverterOptionalString.check_lower(value.short_channel_id) + _UniffiConverterOptionalString.check_lower(value.channel_id) + _UniffiConverterOptionalString.check_lower(value.funding_txid) + _UniffiConverterOptionalUInt32.check_lower(value.funding_outnum) + _UniffiConverterOptionalUInt64.check_lower(value.to_us_msat) + _UniffiConverterOptionalUInt64.check_lower(value.total_msat) + _UniffiConverterOptionalUInt64.check_lower(value.spendable_msat) + _UniffiConverterOptionalUInt64.check_lower(value.receivable_msat) + _UniffiConverterOptionalTypeChannelSide.check_lower(value.closer) + _UniffiConverterSequenceString.check_lower(value.status) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.peer_id, buf) + _UniffiConverterBool.write(value.peer_connected, buf) + _UniffiConverterTypeChannelState.write(value.state, buf) + _UniffiConverterOptionalString.write(value.short_channel_id, buf) + _UniffiConverterOptionalString.write(value.channel_id, buf) + _UniffiConverterOptionalString.write(value.funding_txid, buf) + _UniffiConverterOptionalUInt32.write(value.funding_outnum, buf) + _UniffiConverterOptionalUInt64.write(value.to_us_msat, buf) + _UniffiConverterOptionalUInt64.write(value.total_msat, buf) + _UniffiConverterOptionalUInt64.write(value.spendable_msat, buf) + _UniffiConverterOptionalUInt64.write(value.receivable_msat, buf) + _UniffiConverterOptionalTypeChannelSide.write(value.closer, buf) + _UniffiConverterSequenceString.write(value.status, buf) + + +class ReceiveResponse: + bolt11: "str" + opening_fee_msat: "int" + """ + The fee charged by the LSP for opening a JIT channel, in + millisatoshi. This is 0 if no JIT channel was needed. + """ + + def __init__(self, *, bolt11: "str", opening_fee_msat: "int"): + self.bolt11 = bolt11 + self.opening_fee_msat = opening_fee_msat + + def __str__(self): + return "ReceiveResponse(bolt11={}, opening_fee_msat={})".format(self.bolt11, self.opening_fee_msat) + + def __eq__(self, other): + if self.bolt11 != other.bolt11: + return False + if self.opening_fee_msat != other.opening_fee_msat: + return False + return True + +class _UniffiConverterTypeReceiveResponse(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return ReceiveResponse( + bolt11=_UniffiConverterString.read(buf), + opening_fee_msat=_UniffiConverterUInt64.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.bolt11) + _UniffiConverterUInt64.check_lower(value.opening_fee_msat) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.bolt11, buf) + _UniffiConverterUInt64.write(value.opening_fee_msat, buf) + + +class SendResponse: + status: "PayStatus" + preimage: "str" + """ + Payment preimage (proof of payment) as lowercase hex (64 chars). + """ + + payment_hash: "str" + """ + Payment hash as lowercase hex (64 chars). + """ + + destination_pubkey: "typing.Optional[str]" + """ + Recipient node pubkey as lowercase hex (66 chars), if known. + """ + + amount_msat: "int" + amount_sent_msat: "int" + parts: "int" + def __init__(self, *, status: "PayStatus", preimage: "str", payment_hash: "str", destination_pubkey: "typing.Optional[str]", amount_msat: "int", amount_sent_msat: "int", parts: "int"): + self.status = status + self.preimage = preimage + self.payment_hash = payment_hash + self.destination_pubkey = destination_pubkey + self.amount_msat = amount_msat + self.amount_sent_msat = amount_sent_msat + self.parts = parts + + def __str__(self): + return "SendResponse(status={}, preimage={}, payment_hash={}, destination_pubkey={}, amount_msat={}, amount_sent_msat={}, parts={})".format(self.status, self.preimage, self.payment_hash, self.destination_pubkey, self.amount_msat, self.amount_sent_msat, self.parts) + + def __eq__(self, other): + if self.status != other.status: + return False + if self.preimage != other.preimage: + return False + if self.payment_hash != other.payment_hash: + return False + if self.destination_pubkey != other.destination_pubkey: + return False + if self.amount_msat != other.amount_msat: + return False + if self.amount_sent_msat != other.amount_sent_msat: + return False + if self.parts != other.parts: + return False + return True + +class _UniffiConverterTypeSendResponse(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return SendResponse( + status=_UniffiConverterTypePayStatus.read(buf), + preimage=_UniffiConverterString.read(buf), + payment_hash=_UniffiConverterString.read(buf), + destination_pubkey=_UniffiConverterOptionalString.read(buf), + amount_msat=_UniffiConverterUInt64.read(buf), + amount_sent_msat=_UniffiConverterUInt64.read(buf), + parts=_UniffiConverterUInt32.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterTypePayStatus.check_lower(value.status) + _UniffiConverterString.check_lower(value.preimage) + _UniffiConverterString.check_lower(value.payment_hash) + _UniffiConverterOptionalString.check_lower(value.destination_pubkey) + _UniffiConverterUInt64.check_lower(value.amount_msat) + _UniffiConverterUInt64.check_lower(value.amount_sent_msat) + _UniffiConverterUInt32.check_lower(value.parts) + + @staticmethod + def write(value, buf): + _UniffiConverterTypePayStatus.write(value.status, buf) + _UniffiConverterString.write(value.preimage, buf) + _UniffiConverterString.write(value.payment_hash, buf) + _UniffiConverterOptionalString.write(value.destination_pubkey, buf) + _UniffiConverterUInt64.write(value.amount_msat, buf) + _UniffiConverterUInt64.write(value.amount_sent_msat, buf) + _UniffiConverterUInt32.write(value.parts, buf) + + + + + +class ChannelSide(enum.Enum): + """ + Which side of a channel performed a given action (e.g. initiated close). + """ + + LOCAL = 0 + + REMOTE = 1 + + + +class _UniffiConverterTypeChannelSide(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return ChannelSide.LOCAL + if variant == 2: + return ChannelSide.REMOTE + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value == ChannelSide.LOCAL: + return + if value == ChannelSide.REMOTE: + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value == ChannelSide.LOCAL: + buf.write_i32(1) + if value == ChannelSide.REMOTE: + buf.write_i32(2) + + + + + + + +class ChannelState(enum.Enum): + OPENINGD = 0 + + CHANNELD_AWAITING_LOCKIN = 1 + + CHANNELD_NORMAL = 2 + + CHANNELD_SHUTTING_DOWN = 3 + + CLOSINGD_SIGEXCHANGE = 4 + + CLOSINGD_COMPLETE = 5 + + AWAITING_UNILATERAL = 6 + + FUNDING_SPEND_SEEN = 7 + + ONCHAIN = 8 + + DUALOPEND_OPEN_INIT = 9 + + DUALOPEND_AWAITING_LOCKIN = 10 + + DUALOPEND_OPEN_COMMITTED = 11 + + DUALOPEND_OPEN_COMMIT_READY = 12 + + UNKNOWN = 13 + """ + A state reported by the node that this SDK doesn't recognize. + Returned when CLN introduces a new channel state after this SDK + was built. Treated as neither open nor closing by balance math. + """ + + + + +class _UniffiConverterTypeChannelState(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return ChannelState.OPENINGD + if variant == 2: + return ChannelState.CHANNELD_AWAITING_LOCKIN + if variant == 3: + return ChannelState.CHANNELD_NORMAL + if variant == 4: + return ChannelState.CHANNELD_SHUTTING_DOWN + if variant == 5: + return ChannelState.CLOSINGD_SIGEXCHANGE + if variant == 6: + return ChannelState.CLOSINGD_COMPLETE + if variant == 7: + return ChannelState.AWAITING_UNILATERAL + if variant == 8: + return ChannelState.FUNDING_SPEND_SEEN + if variant == 9: + return ChannelState.ONCHAIN + if variant == 10: + return ChannelState.DUALOPEND_OPEN_INIT + if variant == 11: + return ChannelState.DUALOPEND_AWAITING_LOCKIN + if variant == 12: + return ChannelState.DUALOPEND_OPEN_COMMITTED + if variant == 13: + return ChannelState.DUALOPEND_OPEN_COMMIT_READY + if variant == 14: + return ChannelState.UNKNOWN + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value == ChannelState.OPENINGD: + return + if value == ChannelState.CHANNELD_AWAITING_LOCKIN: + return + if value == ChannelState.CHANNELD_NORMAL: + return + if value == ChannelState.CHANNELD_SHUTTING_DOWN: + return + if value == ChannelState.CLOSINGD_SIGEXCHANGE: + return + if value == ChannelState.CLOSINGD_COMPLETE: + return + if value == ChannelState.AWAITING_UNILATERAL: + return + if value == ChannelState.FUNDING_SPEND_SEEN: + return + if value == ChannelState.ONCHAIN: + return + if value == ChannelState.DUALOPEND_OPEN_INIT: + return + if value == ChannelState.DUALOPEND_AWAITING_LOCKIN: + return + if value == ChannelState.DUALOPEND_OPEN_COMMITTED: + return + if value == ChannelState.DUALOPEND_OPEN_COMMIT_READY: + return + if value == ChannelState.UNKNOWN: + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value == ChannelState.OPENINGD: + buf.write_i32(1) + if value == ChannelState.CHANNELD_AWAITING_LOCKIN: + buf.write_i32(2) + if value == ChannelState.CHANNELD_NORMAL: + buf.write_i32(3) + if value == ChannelState.CHANNELD_SHUTTING_DOWN: + buf.write_i32(4) + if value == ChannelState.CLOSINGD_SIGEXCHANGE: + buf.write_i32(5) + if value == ChannelState.CLOSINGD_COMPLETE: + buf.write_i32(6) + if value == ChannelState.AWAITING_UNILATERAL: + buf.write_i32(7) + if value == ChannelState.FUNDING_SPEND_SEEN: + buf.write_i32(8) + if value == ChannelState.ONCHAIN: + buf.write_i32(9) + if value == ChannelState.DUALOPEND_OPEN_INIT: + buf.write_i32(10) + if value == ChannelState.DUALOPEND_AWAITING_LOCKIN: + buf.write_i32(11) + if value == ChannelState.DUALOPEND_OPEN_COMMITTED: + buf.write_i32(12) + if value == ChannelState.DUALOPEND_OPEN_COMMIT_READY: + buf.write_i32(13) + if value == ChannelState.UNKNOWN: + buf.write_i32(14) + + + + +# Error +# We want to define each variant as a nested class that's also a subclass, +# which is tricky in Python. To accomplish this we're going to create each +# class separately, then manually add the child classes to the base class's +# __dict__. All of this happens in dummy class to avoid polluting the module +# namespace. +class Error(Exception): + pass + +_UniffiTempError = Error + +class Error: # type: ignore + class DuplicateNode(_UniffiTempError): + def __init__(self, *values): + if len(values) != 1: + raise TypeError(f"Expected 1 arguments, found {len(values)}") + if not isinstance(values[0], str): + raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") + super().__init__(", ".join(map(repr, values))) + self._values = values + + def __getitem__(self, index): + return self._values[index] + + def __repr__(self): + return "Error.DuplicateNode({})".format(str(self)) + _UniffiTempError.DuplicateNode = DuplicateNode # type: ignore + class NoSuchNode(_UniffiTempError): + def __init__(self, *values): + if len(values) != 1: + raise TypeError(f"Expected 1 arguments, found {len(values)}") + if not isinstance(values[0], str): + raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") + super().__init__(", ".join(map(repr, values))) + self._values = values + + def __getitem__(self, index): + return self._values[index] + + def __repr__(self): + return "Error.NoSuchNode({})".format(str(self)) + _UniffiTempError.NoSuchNode = NoSuchNode # type: ignore + class UnparseableCreds(_UniffiTempError): + def __init__(self): + pass + + def __repr__(self): + return "Error.UnparseableCreds({})".format(str(self)) + _UniffiTempError.UnparseableCreds = UnparseableCreds # type: ignore + class PhraseCorrupted(_UniffiTempError): + def __init__(self): + pass + + def __repr__(self): + return "Error.PhraseCorrupted({})".format(str(self)) + _UniffiTempError.PhraseCorrupted = PhraseCorrupted # type: ignore + class Rpc(_UniffiTempError): + def __init__(self, *values): + if len(values) != 1: + raise TypeError(f"Expected 1 arguments, found {len(values)}") + if not isinstance(values[0], str): + raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") + super().__init__(", ".join(map(repr, values))) + self._values = values + + def __getitem__(self, index): + return self._values[index] + + def __repr__(self): + return "Error.Rpc({})".format(str(self)) + _UniffiTempError.Rpc = Rpc # type: ignore + class Argument(_UniffiTempError): + def __init__(self, *values): + if len(values) != 2: + raise TypeError(f"Expected 2 arguments, found {len(values)}") + if not isinstance(values[0], str): + raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") + if not isinstance(values[1], str): + raise TypeError(f"unexpected type for tuple element 1 - expected 'str', got '{type(values[1])}'") + super().__init__(", ".join(map(repr, values))) + self._values = values + + def __getitem__(self, index): + return self._values[index] + + def __repr__(self): + return "Error.Argument({})".format(str(self)) + _UniffiTempError.Argument = Argument # type: ignore + class Other(_UniffiTempError): + def __init__(self, *values): + if len(values) != 1: + raise TypeError(f"Expected 1 arguments, found {len(values)}") + if not isinstance(values[0], str): + raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") + super().__init__(", ".join(map(repr, values))) + self._values = values + + def __getitem__(self, index): + return self._values[index] + + def __repr__(self): + return "Error.Other({})".format(str(self)) + _UniffiTempError.Other = Other # type: ignore + +Error = _UniffiTempError # type: ignore +del _UniffiTempError + + +class _UniffiConverterTypeError(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return Error.DuplicateNode( + _UniffiConverterString.read(buf), + ) + if variant == 2: + return Error.NoSuchNode( + _UniffiConverterString.read(buf), + ) + if variant == 3: + return Error.UnparseableCreds( + ) + if variant == 4: + return Error.PhraseCorrupted( + ) + if variant == 5: + return Error.Rpc( + _UniffiConverterString.read(buf), + ) + if variant == 6: + return Error.Argument( + _UniffiConverterString.read(buf), + _UniffiConverterString.read(buf), + ) + if variant == 7: + return Error.Other( + _UniffiConverterString.read(buf), + ) + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if isinstance(value, Error.DuplicateNode): + _UniffiConverterString.check_lower(value._values[0]) + return + if isinstance(value, Error.NoSuchNode): + _UniffiConverterString.check_lower(value._values[0]) + return + if isinstance(value, Error.UnparseableCreds): + return + if isinstance(value, Error.PhraseCorrupted): + return + if isinstance(value, Error.Rpc): + _UniffiConverterString.check_lower(value._values[0]) + return + if isinstance(value, Error.Argument): + _UniffiConverterString.check_lower(value._values[0]) + _UniffiConverterString.check_lower(value._values[1]) + return + if isinstance(value, Error.Other): + _UniffiConverterString.check_lower(value._values[0]) + return + + @staticmethod + def write(value, buf): + if isinstance(value, Error.DuplicateNode): + buf.write_i32(1) + _UniffiConverterString.write(value._values[0], buf) + if isinstance(value, Error.NoSuchNode): + buf.write_i32(2) + _UniffiConverterString.write(value._values[0], buf) + if isinstance(value, Error.UnparseableCreds): + buf.write_i32(3) + if isinstance(value, Error.PhraseCorrupted): + buf.write_i32(4) + if isinstance(value, Error.Rpc): + buf.write_i32(5) + _UniffiConverterString.write(value._values[0], buf) + if isinstance(value, Error.Argument): + buf.write_i32(6) + _UniffiConverterString.write(value._values[0], buf) + _UniffiConverterString.write(value._values[1], buf) + if isinstance(value, Error.Other): + buf.write_i32(7) + _UniffiConverterString.write(value._values[0], buf) + + + + + +class InvoiceStatus(enum.Enum): + UNPAID = 0 + + PAID = 1 + + EXPIRED = 2 + + + +class _UniffiConverterTypeInvoiceStatus(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return PeerChannel( - peer_id=_UniffiConverterString.read(buf), - peer_connected=_UniffiConverterBool.read(buf), - state=_UniffiConverterTypeChannelState.read(buf), - short_channel_id=_UniffiConverterOptionalString.read(buf), - channel_id=_UniffiConverterOptionalString.read(buf), - funding_txid=_UniffiConverterOptionalString.read(buf), - funding_outnum=_UniffiConverterOptionalUInt32.read(buf), - to_us_msat=_UniffiConverterOptionalUInt64.read(buf), - total_msat=_UniffiConverterOptionalUInt64.read(buf), - spendable_msat=_UniffiConverterOptionalUInt64.read(buf), - receivable_msat=_UniffiConverterOptionalUInt64.read(buf), - closer=_UniffiConverterOptionalTypeChannelSide.read(buf), - status=_UniffiConverterSequenceString.read(buf), - ) + variant = buf.read_i32() + if variant == 1: + return InvoiceStatus.UNPAID + if variant == 2: + return InvoiceStatus.PAID + if variant == 3: + return InvoiceStatus.EXPIRED + raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.peer_id) - _UniffiConverterBool.check_lower(value.peer_connected) - _UniffiConverterTypeChannelState.check_lower(value.state) - _UniffiConverterOptionalString.check_lower(value.short_channel_id) - _UniffiConverterOptionalString.check_lower(value.channel_id) - _UniffiConverterOptionalString.check_lower(value.funding_txid) - _UniffiConverterOptionalUInt32.check_lower(value.funding_outnum) - _UniffiConverterOptionalUInt64.check_lower(value.to_us_msat) - _UniffiConverterOptionalUInt64.check_lower(value.total_msat) - _UniffiConverterOptionalUInt64.check_lower(value.spendable_msat) - _UniffiConverterOptionalUInt64.check_lower(value.receivable_msat) - _UniffiConverterOptionalTypeChannelSide.check_lower(value.closer) - _UniffiConverterSequenceString.check_lower(value.status) + if value == InvoiceStatus.UNPAID: + return + if value == InvoiceStatus.PAID: + return + if value == InvoiceStatus.EXPIRED: + return + raise ValueError(value) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.peer_id, buf) - _UniffiConverterBool.write(value.peer_connected, buf) - _UniffiConverterTypeChannelState.write(value.state, buf) - _UniffiConverterOptionalString.write(value.short_channel_id, buf) - _UniffiConverterOptionalString.write(value.channel_id, buf) - _UniffiConverterOptionalString.write(value.funding_txid, buf) - _UniffiConverterOptionalUInt32.write(value.funding_outnum, buf) - _UniffiConverterOptionalUInt64.write(value.to_us_msat, buf) - _UniffiConverterOptionalUInt64.write(value.total_msat, buf) - _UniffiConverterOptionalUInt64.write(value.spendable_msat, buf) - _UniffiConverterOptionalUInt64.write(value.receivable_msat, buf) - _UniffiConverterOptionalTypeChannelSide.write(value.closer, buf) - _UniffiConverterSequenceString.write(value.status, buf) + if value == InvoiceStatus.UNPAID: + buf.write_i32(1) + if value == InvoiceStatus.PAID: + buf.write_i32(2) + if value == InvoiceStatus.EXPIRED: + buf.write_i32(3) -class ReceiveResponse: - bolt11: "str" - opening_fee_msat: "int" + + + + + +class ListIndex(enum.Enum): """ - The fee charged by the LSP for opening a JIT channel, in - millisatoshi. This is 0 if no JIT channel was needed. + Index field used by CLN's paginated list RPCs. """ - def __init__(self, *, bolt11: "str", opening_fee_msat: "int"): - self.bolt11 = bolt11 - self.opening_fee_msat = opening_fee_msat - - def __str__(self): - return "ReceiveResponse(bolt11={}, opening_fee_msat={})".format(self.bolt11, self.opening_fee_msat) + CREATED = 0 + + UPDATED = 1 + - def __eq__(self, other): - if self.bolt11 != other.bolt11: - return False - if self.opening_fee_msat != other.opening_fee_msat: - return False - return True -class _UniffiConverterTypeReceiveResponse(_UniffiConverterRustBuffer): +class _UniffiConverterTypeListIndex(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return ReceiveResponse( - bolt11=_UniffiConverterString.read(buf), - opening_fee_msat=_UniffiConverterUInt64.read(buf), - ) + variant = buf.read_i32() + if variant == 1: + return ListIndex.CREATED + if variant == 2: + return ListIndex.UPDATED + raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.bolt11) - _UniffiConverterUInt64.check_lower(value.opening_fee_msat) + if value == ListIndex.CREATED: + return + if value == ListIndex.UPDATED: + return + raise ValueError(value) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.bolt11, buf) - _UniffiConverterUInt64.write(value.opening_fee_msat, buf) + if value == ListIndex.CREATED: + buf.write_i32(1) + if value == ListIndex.UPDATED: + buf.write_i32(2) -class SendResponse: - status: "PayStatus" - preimage: "str" - """ - Payment preimage (proof of payment) as lowercase hex (64 chars). - """ - payment_hash: "str" - """ - Payment hash as lowercase hex (64 chars). - """ - destination_pubkey: "typing.Optional[str]" + + + +class LnUrlPayResult: """ - Recipient node pubkey as lowercase hex (66 chars), if known. + Result of an LNURL-pay operation. """ - amount_msat: "int" - amount_sent_msat: "int" - parts: "int" - def __init__(self, *, status: "PayStatus", preimage: "str", payment_hash: "str", destination_pubkey: "typing.Optional[str]", amount_msat: "int", amount_sent_msat: "int", parts: "int"): - self.status = status - self.preimage = preimage - self.payment_hash = payment_hash - self.destination_pubkey = destination_pubkey - self.amount_msat = amount_msat - self.amount_sent_msat = amount_sent_msat - self.parts = parts + def __init__(self): + raise RuntimeError("LnUrlPayResult cannot be instantiated directly") - def __str__(self): - return "SendResponse(status={}, preimage={}, payment_hash={}, destination_pubkey={}, amount_msat={}, amount_sent_msat={}, parts={})".format(self.status, self.preimage, self.payment_hash, self.destination_pubkey, self.amount_msat, self.amount_sent_msat, self.parts) + # Each enum variant is a nested class of the enum itself. + class ENDPOINT_SUCCESS: + """ + Payment succeeded. + """ - def __eq__(self, other): - if self.status != other.status: - return False - if self.preimage != other.preimage: - return False - if self.payment_hash != other.payment_hash: - return False - if self.destination_pubkey != other.destination_pubkey: - return False - if self.amount_msat != other.amount_msat: - return False - if self.amount_sent_msat != other.amount_sent_msat: - return False - if self.parts != other.parts: - return False - return True + data: "LnUrlPaySuccessData" -class _UniffiConverterTypeSendResponse(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - return SendResponse( - status=_UniffiConverterTypePayStatus.read(buf), - preimage=_UniffiConverterString.read(buf), - payment_hash=_UniffiConverterString.read(buf), - destination_pubkey=_UniffiConverterOptionalString.read(buf), - amount_msat=_UniffiConverterUInt64.read(buf), - amount_sent_msat=_UniffiConverterUInt64.read(buf), - parts=_UniffiConverterUInt32.read(buf), - ) + def __init__(self,data: "LnUrlPaySuccessData"): + self.data = data + + def __str__(self): + return "LnUrlPayResult.ENDPOINT_SUCCESS(data={})".format(self.data) + + def __eq__(self, other): + if not other.is_ENDPOINT_SUCCESS(): + return False + if self.data != other.data: + return False + return True + + class ENDPOINT_ERROR: + """ + The LNURL service returned an error before the invoice was paid. + """ + + data: "LnUrlErrorData" + + def __init__(self,data: "LnUrlErrorData"): + self.data = data + + def __str__(self): + return "LnUrlPayResult.ENDPOINT_ERROR(data={})".format(self.data) + + def __eq__(self, other): + if not other.is_ENDPOINT_ERROR(): + return False + if self.data != other.data: + return False + return True + + class PAY_ERROR: + """ + The invoice was fetched successfully but paying it failed. + """ + + data: "LnUrlPayErrorData" + + def __init__(self,data: "LnUrlPayErrorData"): + self.data = data + + def __str__(self): + return "LnUrlPayResult.PAY_ERROR(data={})".format(self.data) + def __eq__(self, other): + if not other.is_PAY_ERROR(): + return False + if self.data != other.data: + return False + return True + + + + # For each variant, we have `is_NAME` and `is_name` methods for easily checking + # whether an instance is that variant. + def is_ENDPOINT_SUCCESS(self) -> bool: + return isinstance(self, LnUrlPayResult.ENDPOINT_SUCCESS) + def is_endpoint_success(self) -> bool: + return isinstance(self, LnUrlPayResult.ENDPOINT_SUCCESS) + def is_ENDPOINT_ERROR(self) -> bool: + return isinstance(self, LnUrlPayResult.ENDPOINT_ERROR) + def is_endpoint_error(self) -> bool: + return isinstance(self, LnUrlPayResult.ENDPOINT_ERROR) + def is_PAY_ERROR(self) -> bool: + return isinstance(self, LnUrlPayResult.PAY_ERROR) + def is_pay_error(self) -> bool: + return isinstance(self, LnUrlPayResult.PAY_ERROR) + + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +LnUrlPayResult.ENDPOINT_SUCCESS = type("LnUrlPayResult.ENDPOINT_SUCCESS", (LnUrlPayResult.ENDPOINT_SUCCESS, LnUrlPayResult,), {}) # type: ignore +LnUrlPayResult.ENDPOINT_ERROR = type("LnUrlPayResult.ENDPOINT_ERROR", (LnUrlPayResult.ENDPOINT_ERROR, LnUrlPayResult,), {}) # type: ignore +LnUrlPayResult.PAY_ERROR = type("LnUrlPayResult.PAY_ERROR", (LnUrlPayResult.PAY_ERROR, LnUrlPayResult,), {}) # type: ignore + + + + +class _UniffiConverterTypeLnUrlPayResult(_UniffiConverterRustBuffer): @staticmethod - def check_lower(value): - _UniffiConverterTypePayStatus.check_lower(value.status) - _UniffiConverterString.check_lower(value.preimage) - _UniffiConverterString.check_lower(value.payment_hash) - _UniffiConverterOptionalString.check_lower(value.destination_pubkey) - _UniffiConverterUInt64.check_lower(value.amount_msat) - _UniffiConverterUInt64.check_lower(value.amount_sent_msat) - _UniffiConverterUInt32.check_lower(value.parts) + def read(buf): + variant = buf.read_i32() + if variant == 1: + return LnUrlPayResult.ENDPOINT_SUCCESS( + _UniffiConverterTypeLnUrlPaySuccessData.read(buf), + ) + if variant == 2: + return LnUrlPayResult.ENDPOINT_ERROR( + _UniffiConverterTypeLnUrlErrorData.read(buf), + ) + if variant == 3: + return LnUrlPayResult.PAY_ERROR( + _UniffiConverterTypeLnUrlPayErrorData.read(buf), + ) + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value.is_ENDPOINT_SUCCESS(): + _UniffiConverterTypeLnUrlPaySuccessData.check_lower(value.data) + return + if value.is_ENDPOINT_ERROR(): + _UniffiConverterTypeLnUrlErrorData.check_lower(value.data) + return + if value.is_PAY_ERROR(): + _UniffiConverterTypeLnUrlPayErrorData.check_lower(value.data) + return + raise ValueError(value) @staticmethod def write(value, buf): - _UniffiConverterTypePayStatus.write(value.status, buf) - _UniffiConverterString.write(value.preimage, buf) - _UniffiConverterString.write(value.payment_hash, buf) - _UniffiConverterOptionalString.write(value.destination_pubkey, buf) - _UniffiConverterUInt64.write(value.amount_msat, buf) - _UniffiConverterUInt64.write(value.amount_sent_msat, buf) - _UniffiConverterUInt32.write(value.parts, buf) + if value.is_ENDPOINT_SUCCESS(): + buf.write_i32(1) + _UniffiConverterTypeLnUrlPaySuccessData.write(value.data, buf) + if value.is_ENDPOINT_ERROR(): + buf.write_i32(2) + _UniffiConverterTypeLnUrlErrorData.write(value.data, buf) + if value.is_PAY_ERROR(): + buf.write_i32(3) + _UniffiConverterTypeLnUrlPayErrorData.write(value.data, buf) -class ChannelSide(enum.Enum): + + +class LnUrlWithdrawResult: """ - Which side of a channel performed a given action (e.g. initiated close). + Result of an LNURL-withdraw operation. """ - LOCAL = 0 + def __init__(self): + raise RuntimeError("LnUrlWithdrawResult cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class OK: + """ + The service accepted our invoice and will pay it. + """ + + data: "LnUrlWithdrawSuccessData" + + def __init__(self,data: "LnUrlWithdrawSuccessData"): + self.data = data + + def __str__(self): + return "LnUrlWithdrawResult.OK(data={})".format(self.data) + + def __eq__(self, other): + if not other.is_OK(): + return False + if self.data != other.data: + return False + return True + + class ERROR_STATUS: + """ + The LNURL service returned an error. + """ + + data: "LnUrlErrorData" + + def __init__(self,data: "LnUrlErrorData"): + self.data = data + + def __str__(self): + return "LnUrlWithdrawResult.ERROR_STATUS(data={})".format(self.data) + + def __eq__(self, other): + if not other.is_ERROR_STATUS(): + return False + if self.data != other.data: + return False + return True - REMOTE = 1 + # For each variant, we have `is_NAME` and `is_name` methods for easily checking + # whether an instance is that variant. + def is_OK(self) -> bool: + return isinstance(self, LnUrlWithdrawResult.OK) + def is_ok(self) -> bool: + return isinstance(self, LnUrlWithdrawResult.OK) + def is_ERROR_STATUS(self) -> bool: + return isinstance(self, LnUrlWithdrawResult.ERROR_STATUS) + def is_error_status(self) -> bool: + return isinstance(self, LnUrlWithdrawResult.ERROR_STATUS) + -class _UniffiConverterTypeChannelSide(_UniffiConverterRustBuffer): +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +LnUrlWithdrawResult.OK = type("LnUrlWithdrawResult.OK", (LnUrlWithdrawResult.OK, LnUrlWithdrawResult,), {}) # type: ignore +LnUrlWithdrawResult.ERROR_STATUS = type("LnUrlWithdrawResult.ERROR_STATUS", (LnUrlWithdrawResult.ERROR_STATUS, LnUrlWithdrawResult,), {}) # type: ignore + + + + +class _UniffiConverterTypeLnUrlWithdrawResult(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return ChannelSide.LOCAL + return LnUrlWithdrawResult.OK( + _UniffiConverterTypeLnUrlWithdrawSuccessData.read(buf), + ) if variant == 2: - return ChannelSide.REMOTE + return LnUrlWithdrawResult.ERROR_STATUS( + _UniffiConverterTypeLnUrlErrorData.read(buf), + ) raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == ChannelSide.LOCAL: + if value.is_OK(): + _UniffiConverterTypeLnUrlWithdrawSuccessData.check_lower(value.data) return - if value == ChannelSide.REMOTE: + if value.is_ERROR_STATUS(): + _UniffiConverterTypeLnUrlErrorData.check_lower(value.data) return raise ValueError(value) @staticmethod def write(value, buf): - if value == ChannelSide.LOCAL: + if value.is_OK(): buf.write_i32(1) - if value == ChannelSide.REMOTE: + _UniffiConverterTypeLnUrlWithdrawSuccessData.write(value.data, buf) + if value.is_ERROR_STATUS(): buf.write_i32(2) + _UniffiConverterTypeLnUrlErrorData.write(value.data, buf) @@ -3559,341 +4733,279 @@ def write(value, buf): -class ChannelState(enum.Enum): - OPENINGD = 0 - - CHANNELD_AWAITING_LOCKIN = 1 - - CHANNELD_NORMAL = 2 - - CHANNELD_SHUTTING_DOWN = 3 - - CLOSINGD_SIGEXCHANGE = 4 - - CLOSINGD_COMPLETE = 5 - - AWAITING_UNILATERAL = 6 - - FUNDING_SPEND_SEEN = 7 - - ONCHAIN = 8 - - DUALOPEND_OPEN_INIT = 9 +class LogLevel(enum.Enum): + """ + Log level for filtering messages. + """ + + ERROR = 0 - DUALOPEND_AWAITING_LOCKIN = 10 + WARN = 1 - DUALOPEND_OPEN_COMMITTED = 11 + INFO = 2 - DUALOPEND_OPEN_COMMIT_READY = 12 + DEBUG = 3 - UNKNOWN = 13 - """ - A state reported by the node that this SDK doesn't recognize. - Returned when CLN introduces a new channel state after this SDK - was built. Treated as neither open nor closing by balance math. - """ - + TRACE = 4 -class _UniffiConverterTypeChannelState(_UniffiConverterRustBuffer): +class _UniffiConverterTypeLogLevel(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return ChannelState.OPENINGD + return LogLevel.ERROR if variant == 2: - return ChannelState.CHANNELD_AWAITING_LOCKIN + return LogLevel.WARN if variant == 3: - return ChannelState.CHANNELD_NORMAL + return LogLevel.INFO if variant == 4: - return ChannelState.CHANNELD_SHUTTING_DOWN + return LogLevel.DEBUG if variant == 5: - return ChannelState.CLOSINGD_SIGEXCHANGE - if variant == 6: - return ChannelState.CLOSINGD_COMPLETE - if variant == 7: - return ChannelState.AWAITING_UNILATERAL - if variant == 8: - return ChannelState.FUNDING_SPEND_SEEN - if variant == 9: - return ChannelState.ONCHAIN - if variant == 10: - return ChannelState.DUALOPEND_OPEN_INIT - if variant == 11: - return ChannelState.DUALOPEND_AWAITING_LOCKIN - if variant == 12: - return ChannelState.DUALOPEND_OPEN_COMMITTED - if variant == 13: - return ChannelState.DUALOPEND_OPEN_COMMIT_READY - if variant == 14: - return ChannelState.UNKNOWN + return LogLevel.TRACE raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == ChannelState.OPENINGD: - return - if value == ChannelState.CHANNELD_AWAITING_LOCKIN: - return - if value == ChannelState.CHANNELD_NORMAL: - return - if value == ChannelState.CHANNELD_SHUTTING_DOWN: - return - if value == ChannelState.CLOSINGD_SIGEXCHANGE: - return - if value == ChannelState.CLOSINGD_COMPLETE: - return - if value == ChannelState.AWAITING_UNILATERAL: - return - if value == ChannelState.FUNDING_SPEND_SEEN: - return - if value == ChannelState.ONCHAIN: - return - if value == ChannelState.DUALOPEND_OPEN_INIT: + if value == LogLevel.ERROR: return - if value == ChannelState.DUALOPEND_AWAITING_LOCKIN: + if value == LogLevel.WARN: return - if value == ChannelState.DUALOPEND_OPEN_COMMITTED: + if value == LogLevel.INFO: return - if value == ChannelState.DUALOPEND_OPEN_COMMIT_READY: + if value == LogLevel.DEBUG: return - if value == ChannelState.UNKNOWN: + if value == LogLevel.TRACE: return raise ValueError(value) @staticmethod def write(value, buf): - if value == ChannelState.OPENINGD: + if value == LogLevel.ERROR: buf.write_i32(1) - if value == ChannelState.CHANNELD_AWAITING_LOCKIN: + if value == LogLevel.WARN: buf.write_i32(2) - if value == ChannelState.CHANNELD_NORMAL: + if value == LogLevel.INFO: buf.write_i32(3) - if value == ChannelState.CHANNELD_SHUTTING_DOWN: + if value == LogLevel.DEBUG: buf.write_i32(4) - if value == ChannelState.CLOSINGD_SIGEXCHANGE: + if value == LogLevel.TRACE: buf.write_i32(5) - if value == ChannelState.CLOSINGD_COMPLETE: - buf.write_i32(6) - if value == ChannelState.AWAITING_UNILATERAL: - buf.write_i32(7) - if value == ChannelState.FUNDING_SPEND_SEEN: - buf.write_i32(8) - if value == ChannelState.ONCHAIN: - buf.write_i32(9) - if value == ChannelState.DUALOPEND_OPEN_INIT: - buf.write_i32(10) - if value == ChannelState.DUALOPEND_AWAITING_LOCKIN: - buf.write_i32(11) - if value == ChannelState.DUALOPEND_OPEN_COMMITTED: - buf.write_i32(12) - if value == ChannelState.DUALOPEND_OPEN_COMMIT_READY: - buf.write_i32(13) - if value == ChannelState.UNKNOWN: - buf.write_i32(14) -# Error -# We want to define each variant as a nested class that's also a subclass, -# which is tricky in Python. To accomplish this we're going to create each -# class separately, then manually add the child classes to the base class's -# __dict__. All of this happens in dummy class to avoid polluting the module -# namespace. -class Error(Exception): - pass -_UniffiTempError = Error -class Error: # type: ignore - class DuplicateNode(_UniffiTempError): - def __init__(self, *values): - if len(values) != 1: - raise TypeError(f"Expected 1 arguments, found {len(values)}") - if not isinstance(values[0], str): - raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") - super().__init__(", ".join(map(repr, values))) - self._values = values - def __getitem__(self, index): - return self._values[index] +class Network(enum.Enum): + BITCOIN = 0 + + REGTEST = 1 + + + +class _UniffiConverterTypeNetwork(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return Network.BITCOIN + if variant == 2: + return Network.REGTEST + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value == Network.BITCOIN: + return + if value == Network.REGTEST: + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value == Network.BITCOIN: + buf.write_i32(1) + if value == Network.REGTEST: + buf.write_i32(2) + + + + + + + +class NodeEvent: + """ + A real-time event from the node. + """ + + def __init__(self): + raise RuntimeError("NodeEvent cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class INVOICE_PAID: + """ + An invoice was paid. + """ + + details: "InvoicePaidEvent" + + def __init__(self,details: "InvoicePaidEvent"): + self.details = details + + def __str__(self): + return "NodeEvent.INVOICE_PAID(details={})".format(self.details) + + def __eq__(self, other): + if not other.is_INVOICE_PAID(): + return False + if self.details != other.details: + return False + return True + + class UNKNOWN: + """ + An unknown event type was received. This can happen if the + server sends a new event type that this client doesn't know about. + """ + + + def __init__(self,): + pass + + def __str__(self): + return "NodeEvent.UNKNOWN()".format() + + def __eq__(self, other): + if not other.is_UNKNOWN(): + return False + return True + + - def __repr__(self): - return "Error.DuplicateNode({})".format(str(self)) - _UniffiTempError.DuplicateNode = DuplicateNode # type: ignore - class NoSuchNode(_UniffiTempError): - def __init__(self, *values): - if len(values) != 1: - raise TypeError(f"Expected 1 arguments, found {len(values)}") - if not isinstance(values[0], str): - raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") - super().__init__(", ".join(map(repr, values))) - self._values = values + # For each variant, we have `is_NAME` and `is_name` methods for easily checking + # whether an instance is that variant. + def is_INVOICE_PAID(self) -> bool: + return isinstance(self, NodeEvent.INVOICE_PAID) + def is_invoice_paid(self) -> bool: + return isinstance(self, NodeEvent.INVOICE_PAID) + def is_UNKNOWN(self) -> bool: + return isinstance(self, NodeEvent.UNKNOWN) + def is_unknown(self) -> bool: + return isinstance(self, NodeEvent.UNKNOWN) + - def __getitem__(self, index): - return self._values[index] +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +NodeEvent.INVOICE_PAID = type("NodeEvent.INVOICE_PAID", (NodeEvent.INVOICE_PAID, NodeEvent,), {}) # type: ignore +NodeEvent.UNKNOWN = type("NodeEvent.UNKNOWN", (NodeEvent.UNKNOWN, NodeEvent,), {}) # type: ignore - def __repr__(self): - return "Error.NoSuchNode({})".format(str(self)) - _UniffiTempError.NoSuchNode = NoSuchNode # type: ignore - class UnparseableCreds(_UniffiTempError): - def __init__(self): - pass - def __repr__(self): - return "Error.UnparseableCreds({})".format(str(self)) - _UniffiTempError.UnparseableCreds = UnparseableCreds # type: ignore - class PhraseCorrupted(_UniffiTempError): - def __init__(self): - pass - def __repr__(self): - return "Error.PhraseCorrupted({})".format(str(self)) - _UniffiTempError.PhraseCorrupted = PhraseCorrupted # type: ignore - class Rpc(_UniffiTempError): - def __init__(self, *values): - if len(values) != 1: - raise TypeError(f"Expected 1 arguments, found {len(values)}") - if not isinstance(values[0], str): - raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") - super().__init__(", ".join(map(repr, values))) - self._values = values - def __getitem__(self, index): - return self._values[index] +class _UniffiConverterTypeNodeEvent(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return NodeEvent.INVOICE_PAID( + _UniffiConverterTypeInvoicePaidEvent.read(buf), + ) + if variant == 2: + return NodeEvent.UNKNOWN( + ) + raise InternalError("Raw enum value doesn't match any cases") - def __repr__(self): - return "Error.Rpc({})".format(str(self)) - _UniffiTempError.Rpc = Rpc # type: ignore - class Argument(_UniffiTempError): - def __init__(self, *values): - if len(values) != 2: - raise TypeError(f"Expected 2 arguments, found {len(values)}") - if not isinstance(values[0], str): - raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") - if not isinstance(values[1], str): - raise TypeError(f"unexpected type for tuple element 1 - expected 'str', got '{type(values[1])}'") - super().__init__(", ".join(map(repr, values))) - self._values = values + @staticmethod + def check_lower(value): + if value.is_INVOICE_PAID(): + _UniffiConverterTypeInvoicePaidEvent.check_lower(value.details) + return + if value.is_UNKNOWN(): + return + raise ValueError(value) - def __getitem__(self, index): - return self._values[index] + @staticmethod + def write(value, buf): + if value.is_INVOICE_PAID(): + buf.write_i32(1) + _UniffiConverterTypeInvoicePaidEvent.write(value.details, buf) + if value.is_UNKNOWN(): + buf.write_i32(2) - def __repr__(self): - return "Error.Argument({})".format(str(self)) - _UniffiTempError.Argument = Argument # type: ignore - class Other(_UniffiTempError): - def __init__(self, *values): - if len(values) != 1: - raise TypeError(f"Expected 1 arguments, found {len(values)}") - if not isinstance(values[0], str): - raise TypeError(f"unexpected type for tuple element 0 - expected 'str', got '{type(values[0])}'") - super().__init__(", ".join(map(repr, values))) - self._values = values - def __getitem__(self, index): - return self._values[index] - def __repr__(self): - return "Error.Other({})".format(str(self)) - _UniffiTempError.Other = Other # type: ignore -Error = _UniffiTempError # type: ignore -del _UniffiTempError -class _UniffiConverterTypeError(_UniffiConverterRustBuffer): + +class OutputStatus(enum.Enum): + UNCONFIRMED = 0 + + CONFIRMED = 1 + + SPENT = 2 + + IMMATURE = 3 + + + +class _UniffiConverterTypeOutputStatus(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return Error.DuplicateNode( - _UniffiConverterString.read(buf), - ) + return OutputStatus.UNCONFIRMED if variant == 2: - return Error.NoSuchNode( - _UniffiConverterString.read(buf), - ) + return OutputStatus.CONFIRMED if variant == 3: - return Error.UnparseableCreds( - ) + return OutputStatus.SPENT if variant == 4: - return Error.PhraseCorrupted( - ) - if variant == 5: - return Error.Rpc( - _UniffiConverterString.read(buf), - ) - if variant == 6: - return Error.Argument( - _UniffiConverterString.read(buf), - _UniffiConverterString.read(buf), - ) - if variant == 7: - return Error.Other( - _UniffiConverterString.read(buf), - ) + return OutputStatus.IMMATURE raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if isinstance(value, Error.DuplicateNode): - _UniffiConverterString.check_lower(value._values[0]) - return - if isinstance(value, Error.NoSuchNode): - _UniffiConverterString.check_lower(value._values[0]) - return - if isinstance(value, Error.UnparseableCreds): - return - if isinstance(value, Error.PhraseCorrupted): + if value == OutputStatus.UNCONFIRMED: return - if isinstance(value, Error.Rpc): - _UniffiConverterString.check_lower(value._values[0]) + if value == OutputStatus.CONFIRMED: return - if isinstance(value, Error.Argument): - _UniffiConverterString.check_lower(value._values[0]) - _UniffiConverterString.check_lower(value._values[1]) + if value == OutputStatus.SPENT: return - if isinstance(value, Error.Other): - _UniffiConverterString.check_lower(value._values[0]) + if value == OutputStatus.IMMATURE: return + raise ValueError(value) @staticmethod def write(value, buf): - if isinstance(value, Error.DuplicateNode): + if value == OutputStatus.UNCONFIRMED: buf.write_i32(1) - _UniffiConverterString.write(value._values[0], buf) - if isinstance(value, Error.NoSuchNode): + if value == OutputStatus.CONFIRMED: buf.write_i32(2) - _UniffiConverterString.write(value._values[0], buf) - if isinstance(value, Error.UnparseableCreds): + if value == OutputStatus.SPENT: buf.write_i32(3) - if isinstance(value, Error.PhraseCorrupted): + if value == OutputStatus.IMMATURE: buf.write_i32(4) - if isinstance(value, Error.Rpc): - buf.write_i32(5) - _UniffiConverterString.write(value._values[0], buf) - if isinstance(value, Error.Argument): - buf.write_i32(6) - _UniffiConverterString.write(value._values[0], buf) - _UniffiConverterString.write(value._values[1], buf) - if isinstance(value, Error.Other): - buf.write_i32(7) - _UniffiConverterString.write(value._values[0], buf) -class InputType: + + +class ParsedInput: """ - The result of parsing user input. + The result of `parse_input`: an offline classification of the + input. No HTTP, no I/O. LNURL bech32 strings are returned as their + decoded URL; Lightning Addresses as the unparsed `user@host` form. """ def __init__(self): - raise RuntimeError("InputType cannot be instantiated directly") + raise RuntimeError("ParsedInput cannot be instantiated directly") # Each enum variant is a nested class of the enum itself. class BOLT11: @@ -3907,7 +5019,7 @@ def __init__(self,invoice: "ParsedInvoice"): self.invoice = invoice def __str__(self): - return "InputType.BOLT11(invoice={})".format(self.invoice) + return "ParsedInput.BOLT11(invoice={})".format(self.invoice) def __eq__(self, other): if not other.is_BOLT11(): @@ -3918,7 +5030,7 @@ def __eq__(self, other): class NODE_ID: """ - A Lightning node public key (66 hex characters, 33 bytes compressed). + A Lightning node public key. """ node_id: "str" @@ -3927,12 +5039,56 @@ def __init__(self,node_id: "str"): self.node_id = node_id def __str__(self): - return "InputType.NODE_ID(node_id={})".format(self.node_id) + return "ParsedInput.NODE_ID(node_id={})".format(self.node_id) + + def __eq__(self, other): + if not other.is_NODE_ID(): + return False + if self.node_id != other.node_id: + return False + return True + + class LN_URL: + """ + An LNURL bech32 string (LUD-01) decoded to its underlying URL. + Pass to `resolve_input` (or fetch yourself) to determine + whether it's a pay, withdraw, or auth endpoint. + """ + + url: "str" + + def __init__(self,url: "str"): + self.url = url + + def __str__(self): + return "ParsedInput.LN_URL(url={})".format(self.url) + + def __eq__(self, other): + if not other.is_LN_URL(): + return False + if self.url != other.url: + return False + return True + + class LN_URL_ADDRESS: + """ + A Lightning Address (LUD-16) in the form `user@host`. The + well-known URL is not constructed offline; call `resolve_input` + to fetch and classify. + """ + + address: "str" + + def __init__(self,address: "str"): + self.address = address + + def __str__(self): + return "ParsedInput.LN_URL_ADDRESS(address={})".format(self.address) def __eq__(self, other): - if not other.is_NODE_ID(): + if not other.is_LN_URL_ADDRESS(): return False - if self.node_id != other.node_id: + if self.address != other.address: return False return True @@ -3941,34 +5097,52 @@ def __eq__(self, other): # For each variant, we have `is_NAME` and `is_name` methods for easily checking # whether an instance is that variant. def is_BOLT11(self) -> bool: - return isinstance(self, InputType.BOLT11) + return isinstance(self, ParsedInput.BOLT11) def is_bolt11(self) -> bool: - return isinstance(self, InputType.BOLT11) + return isinstance(self, ParsedInput.BOLT11) def is_NODE_ID(self) -> bool: - return isinstance(self, InputType.NODE_ID) + return isinstance(self, ParsedInput.NODE_ID) def is_node_id(self) -> bool: - return isinstance(self, InputType.NODE_ID) + return isinstance(self, ParsedInput.NODE_ID) + def is_LN_URL(self) -> bool: + return isinstance(self, ParsedInput.LN_URL) + def is_ln_url(self) -> bool: + return isinstance(self, ParsedInput.LN_URL) + def is_LN_URL_ADDRESS(self) -> bool: + return isinstance(self, ParsedInput.LN_URL_ADDRESS) + def is_ln_url_address(self) -> bool: + return isinstance(self, ParsedInput.LN_URL_ADDRESS) # Now, a little trick - we make each nested variant class be a subclass of the main # enum class, so that method calls and instance checks etc will work intuitively. # We might be able to do this a little more neatly with a metaclass, but this'll do. -InputType.BOLT11 = type("InputType.BOLT11", (InputType.BOLT11, InputType,), {}) # type: ignore -InputType.NODE_ID = type("InputType.NODE_ID", (InputType.NODE_ID, InputType,), {}) # type: ignore +ParsedInput.BOLT11 = type("ParsedInput.BOLT11", (ParsedInput.BOLT11, ParsedInput,), {}) # type: ignore +ParsedInput.NODE_ID = type("ParsedInput.NODE_ID", (ParsedInput.NODE_ID, ParsedInput,), {}) # type: ignore +ParsedInput.LN_URL = type("ParsedInput.LN_URL", (ParsedInput.LN_URL, ParsedInput,), {}) # type: ignore +ParsedInput.LN_URL_ADDRESS = type("ParsedInput.LN_URL_ADDRESS", (ParsedInput.LN_URL_ADDRESS, ParsedInput,), {}) # type: ignore -class _UniffiConverterTypeInputType(_UniffiConverterRustBuffer): +class _UniffiConverterTypeParsedInput(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return InputType.BOLT11( + return ParsedInput.BOLT11( _UniffiConverterTypeParsedInvoice.read(buf), ) if variant == 2: - return InputType.NODE_ID( + return ParsedInput.NODE_ID( + _UniffiConverterString.read(buf), + ) + if variant == 3: + return ParsedInput.LN_URL( + _UniffiConverterString.read(buf), + ) + if variant == 4: + return ParsedInput.LN_URL_ADDRESS( _UniffiConverterString.read(buf), ) raise InternalError("Raw enum value doesn't match any cases") @@ -3981,6 +5155,12 @@ def check_lower(value): if value.is_NODE_ID(): _UniffiConverterString.check_lower(value.node_id) return + if value.is_LN_URL(): + _UniffiConverterString.check_lower(value.url) + return + if value.is_LN_URL_ADDRESS(): + _UniffiConverterString.check_lower(value.address) + return raise ValueError(value) @staticmethod @@ -3991,6 +5171,12 @@ def write(value, buf): if value.is_NODE_ID(): buf.write_i32(2) _UniffiConverterString.write(value.node_id, buf) + if value.is_LN_URL(): + buf.write_i32(3) + _UniffiConverterString.write(value.url, buf) + if value.is_LN_URL_ADDRESS(): + buf.write_i32(4) + _UniffiConverterString.write(value.address, buf) @@ -3998,44 +5184,44 @@ def write(value, buf): -class InvoiceStatus(enum.Enum): - UNPAID = 0 +class PayStatus(enum.Enum): + COMPLETE = 0 - PAID = 1 + PENDING = 1 - EXPIRED = 2 + FAILED = 2 -class _UniffiConverterTypeInvoiceStatus(_UniffiConverterRustBuffer): +class _UniffiConverterTypePayStatus(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return InvoiceStatus.UNPAID + return PayStatus.COMPLETE if variant == 2: - return InvoiceStatus.PAID + return PayStatus.PENDING if variant == 3: - return InvoiceStatus.EXPIRED + return PayStatus.FAILED raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == InvoiceStatus.UNPAID: + if value == PayStatus.COMPLETE: return - if value == InvoiceStatus.PAID: + if value == PayStatus.PENDING: return - if value == InvoiceStatus.EXPIRED: + if value == PayStatus.FAILED: return raise ValueError(value) @staticmethod def write(value, buf): - if value == InvoiceStatus.UNPAID: + if value == PayStatus.COMPLETE: buf.write_i32(1) - if value == InvoiceStatus.PAID: + if value == PayStatus.PENDING: buf.write_i32(2) - if value == InvoiceStatus.EXPIRED: + if value == PayStatus.FAILED: buf.write_i32(3) @@ -4044,41 +5230,45 @@ def write(value, buf): -class ListIndex(enum.Enum): - """ - Index field used by CLN's paginated list RPCs. - """ - - CREATED = 0 +class PaymentStatus(enum.Enum): + PENDING = 0 - UPDATED = 1 + COMPLETE = 1 + + FAILED = 2 -class _UniffiConverterTypeListIndex(_UniffiConverterRustBuffer): +class _UniffiConverterTypePaymentStatus(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return ListIndex.CREATED + return PaymentStatus.PENDING if variant == 2: - return ListIndex.UPDATED + return PaymentStatus.COMPLETE + if variant == 3: + return PaymentStatus.FAILED raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == ListIndex.CREATED: + if value == PaymentStatus.PENDING: return - if value == ListIndex.UPDATED: + if value == PaymentStatus.COMPLETE: + return + if value == PaymentStatus.FAILED: return raise ValueError(value) @staticmethod def write(value, buf): - if value == ListIndex.CREATED: + if value == PaymentStatus.PENDING: buf.write_i32(1) - if value == ListIndex.UPDATED: + if value == PaymentStatus.COMPLETE: buf.write_i32(2) + if value == PaymentStatus.FAILED: + buf.write_i32(3) @@ -4086,65 +5276,37 @@ def write(value, buf): -class LogLevel(enum.Enum): - """ - Log level for filtering messages. - """ - - ERROR = 0 - - WARN = 1 - - INFO = 2 - - DEBUG = 3 +class PaymentType(enum.Enum): + SENT = 0 - TRACE = 4 + RECEIVED = 1 -class _UniffiConverterTypeLogLevel(_UniffiConverterRustBuffer): +class _UniffiConverterTypePaymentType(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return LogLevel.ERROR + return PaymentType.SENT if variant == 2: - return LogLevel.WARN - if variant == 3: - return LogLevel.INFO - if variant == 4: - return LogLevel.DEBUG - if variant == 5: - return LogLevel.TRACE + return PaymentType.RECEIVED raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == LogLevel.ERROR: - return - if value == LogLevel.WARN: - return - if value == LogLevel.INFO: - return - if value == LogLevel.DEBUG: + if value == PaymentType.SENT: return - if value == LogLevel.TRACE: + if value == PaymentType.RECEIVED: return raise ValueError(value) @staticmethod def write(value, buf): - if value == LogLevel.ERROR: + if value == PaymentType.SENT: buf.write_i32(1) - if value == LogLevel.WARN: + if value == PaymentType.RECEIVED: buf.write_i32(2) - if value == LogLevel.INFO: - buf.write_i32(3) - if value == LogLevel.DEBUG: - buf.write_i32(4) - if value == LogLevel.TRACE: - buf.write_i32(5) @@ -4152,36 +5314,36 @@ def write(value, buf): -class Network(enum.Enum): - BITCOIN = 0 +class PaymentTypeFilter(enum.Enum): + SENT = 0 - REGTEST = 1 + RECEIVED = 1 -class _UniffiConverterTypeNetwork(_UniffiConverterRustBuffer): +class _UniffiConverterTypePaymentTypeFilter(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return Network.BITCOIN + return PaymentTypeFilter.SENT if variant == 2: - return Network.REGTEST + return PaymentTypeFilter.RECEIVED raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == Network.BITCOIN: + if value == PaymentTypeFilter.SENT: return - if value == Network.REGTEST: + if value == PaymentTypeFilter.RECEIVED: return raise ValueError(value) @staticmethod def write(value, buf): - if value == Network.BITCOIN: + if value == PaymentTypeFilter.SENT: buf.write_i32(1) - if value == Network.REGTEST: + if value == PaymentTypeFilter.RECEIVED: buf.write_i32(2) @@ -4190,175 +5352,183 @@ def write(value, buf): -class NodeEvent: +class ResolvedInput: """ - A real-time event from the node. + The result of `resolve_input`: a fully-resolved input ready for + the caller's next action. LNURL bech32 strings and Lightning + Addresses are resolved over HTTP into typed pay or withdraw + request data; BOLT11 and node IDs pass through unchanged. """ def __init__(self): - raise RuntimeError("NodeEvent cannot be instantiated directly") + raise RuntimeError("ResolvedInput cannot be instantiated directly") # Each enum variant is a nested class of the enum itself. - class INVOICE_PAID: + class BOLT11: """ - An invoice was paid. + A BOLT11 Lightning invoice. No HTTP was performed. """ - details: "InvoicePaidEvent" + invoice: "ParsedInvoice" - def __init__(self,details: "InvoicePaidEvent"): - self.details = details + def __init__(self,invoice: "ParsedInvoice"): + self.invoice = invoice def __str__(self): - return "NodeEvent.INVOICE_PAID(details={})".format(self.details) + return "ResolvedInput.BOLT11(invoice={})".format(self.invoice) def __eq__(self, other): - if not other.is_INVOICE_PAID(): + if not other.is_BOLT11(): return False - if self.details != other.details: + if self.invoice != other.invoice: return False return True - - - # For each variant, we have `is_NAME` and `is_name` methods for easily checking - # whether an instance is that variant. - def is_INVOICE_PAID(self) -> bool: - return isinstance(self, NodeEvent.INVOICE_PAID) - def is_invoice_paid(self) -> bool: - return isinstance(self, NodeEvent.INVOICE_PAID) - - -# Now, a little trick - we make each nested variant class be a subclass of the main -# enum class, so that method calls and instance checks etc will work intuitively. -# We might be able to do this a little more neatly with a metaclass, but this'll do. -NodeEvent.INVOICE_PAID = type("NodeEvent.INVOICE_PAID", (NodeEvent.INVOICE_PAID, NodeEvent,), {}) # type: ignore - - - - -class _UniffiConverterTypeNodeEvent(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - variant = buf.read_i32() - if variant == 1: - return NodeEvent.INVOICE_PAID( - _UniffiConverterTypeInvoicePaidEvent.read(buf), - ) - raise InternalError("Raw enum value doesn't match any cases") - - @staticmethod - def check_lower(value): - if value.is_INVOICE_PAID(): - _UniffiConverterTypeInvoicePaidEvent.check_lower(value.details) - return - raise ValueError(value) - - @staticmethod - def write(value, buf): - if value.is_INVOICE_PAID(): - buf.write_i32(1) - _UniffiConverterTypeInvoicePaidEvent.write(value.details, buf) - - - - - - - -class OutputStatus(enum.Enum): - UNCONFIRMED = 0 - - CONFIRMED = 1 - - SPENT = 2 - - IMMATURE = 3 - + class NODE_ID: + """ + A Lightning node public key. No HTTP was performed. + """ + node_id: "str" -class _UniffiConverterTypeOutputStatus(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - variant = buf.read_i32() - if variant == 1: - return OutputStatus.UNCONFIRMED - if variant == 2: - return OutputStatus.CONFIRMED - if variant == 3: - return OutputStatus.SPENT - if variant == 4: - return OutputStatus.IMMATURE - raise InternalError("Raw enum value doesn't match any cases") + def __init__(self,node_id: "str"): + self.node_id = node_id - @staticmethod - def check_lower(value): - if value == OutputStatus.UNCONFIRMED: - return - if value == OutputStatus.CONFIRMED: - return - if value == OutputStatus.SPENT: - return - if value == OutputStatus.IMMATURE: - return - raise ValueError(value) + def __str__(self): + return "ResolvedInput.NODE_ID(node_id={})".format(self.node_id) - @staticmethod - def write(value, buf): - if value == OutputStatus.UNCONFIRMED: - buf.write_i32(1) - if value == OutputStatus.CONFIRMED: - buf.write_i32(2) - if value == OutputStatus.SPENT: - buf.write_i32(3) - if value == OutputStatus.IMMATURE: - buf.write_i32(4) + def __eq__(self, other): + if not other.is_NODE_ID(): + return False + if self.node_id != other.node_id: + return False + return True + + class LN_URL_PAY: + """ + An LNURL-pay endpoint with the service's parameters fetched. + """ + data: "LnUrlPayRequestData" + def __init__(self,data: "LnUrlPayRequestData"): + self.data = data + def __str__(self): + return "ResolvedInput.LN_URL_PAY(data={})".format(self.data) + + def __eq__(self, other): + if not other.is_LN_URL_PAY(): + return False + if self.data != other.data: + return False + return True + + class LN_URL_WITHDRAW: + """ + An LNURL-withdraw endpoint with the service's parameters fetched. + """ + data: "LnUrlWithdrawRequestData" + def __init__(self,data: "LnUrlWithdrawRequestData"): + self.data = data + def __str__(self): + return "ResolvedInput.LN_URL_WITHDRAW(data={})".format(self.data) -class PayStatus(enum.Enum): - COMPLETE = 0 + def __eq__(self, other): + if not other.is_LN_URL_WITHDRAW(): + return False + if self.data != other.data: + return False + return True - PENDING = 1 - FAILED = 2 + + # For each variant, we have `is_NAME` and `is_name` methods for easily checking + # whether an instance is that variant. + def is_BOLT11(self) -> bool: + return isinstance(self, ResolvedInput.BOLT11) + def is_bolt11(self) -> bool: + return isinstance(self, ResolvedInput.BOLT11) + def is_NODE_ID(self) -> bool: + return isinstance(self, ResolvedInput.NODE_ID) + def is_node_id(self) -> bool: + return isinstance(self, ResolvedInput.NODE_ID) + def is_LN_URL_PAY(self) -> bool: + return isinstance(self, ResolvedInput.LN_URL_PAY) + def is_ln_url_pay(self) -> bool: + return isinstance(self, ResolvedInput.LN_URL_PAY) + def is_LN_URL_WITHDRAW(self) -> bool: + return isinstance(self, ResolvedInput.LN_URL_WITHDRAW) + def is_ln_url_withdraw(self) -> bool: + return isinstance(self, ResolvedInput.LN_URL_WITHDRAW) +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +ResolvedInput.BOLT11 = type("ResolvedInput.BOLT11", (ResolvedInput.BOLT11, ResolvedInput,), {}) # type: ignore +ResolvedInput.NODE_ID = type("ResolvedInput.NODE_ID", (ResolvedInput.NODE_ID, ResolvedInput,), {}) # type: ignore +ResolvedInput.LN_URL_PAY = type("ResolvedInput.LN_URL_PAY", (ResolvedInput.LN_URL_PAY, ResolvedInput,), {}) # type: ignore +ResolvedInput.LN_URL_WITHDRAW = type("ResolvedInput.LN_URL_WITHDRAW", (ResolvedInput.LN_URL_WITHDRAW, ResolvedInput,), {}) # type: ignore + -class _UniffiConverterTypePayStatus(_UniffiConverterRustBuffer): + + +class _UniffiConverterTypeResolvedInput(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return PayStatus.COMPLETE + return ResolvedInput.BOLT11( + _UniffiConverterTypeParsedInvoice.read(buf), + ) if variant == 2: - return PayStatus.PENDING + return ResolvedInput.NODE_ID( + _UniffiConverterString.read(buf), + ) if variant == 3: - return PayStatus.FAILED + return ResolvedInput.LN_URL_PAY( + _UniffiConverterTypeLnUrlPayRequestData.read(buf), + ) + if variant == 4: + return ResolvedInput.LN_URL_WITHDRAW( + _UniffiConverterTypeLnUrlWithdrawRequestData.read(buf), + ) raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == PayStatus.COMPLETE: + if value.is_BOLT11(): + _UniffiConverterTypeParsedInvoice.check_lower(value.invoice) return - if value == PayStatus.PENDING: + if value.is_NODE_ID(): + _UniffiConverterString.check_lower(value.node_id) return - if value == PayStatus.FAILED: + if value.is_LN_URL_PAY(): + _UniffiConverterTypeLnUrlPayRequestData.check_lower(value.data) + return + if value.is_LN_URL_WITHDRAW(): + _UniffiConverterTypeLnUrlWithdrawRequestData.check_lower(value.data) return raise ValueError(value) @staticmethod def write(value, buf): - if value == PayStatus.COMPLETE: + if value.is_BOLT11(): buf.write_i32(1) - if value == PayStatus.PENDING: + _UniffiConverterTypeParsedInvoice.write(value.invoice, buf) + if value.is_NODE_ID(): buf.write_i32(2) - if value == PayStatus.FAILED: + _UniffiConverterString.write(value.node_id, buf) + if value.is_LN_URL_PAY(): buf.write_i32(3) + _UniffiConverterTypeLnUrlPayRequestData.write(value.data, buf) + if value.is_LN_URL_WITHDRAW(): + buf.write_i32(4) + _UniffiConverterTypeLnUrlWithdrawRequestData.write(value.data, buf) @@ -4366,121 +5536,162 @@ def write(value, buf): -class PaymentStatus(enum.Enum): - PENDING = 0 - - COMPLETE = 1 - - FAILED = 2 - +class SuccessActionProcessed: + """ + A processed success action from an LNURL-pay callback. + For Message and Url this is passed through as-is. For Aes the + ciphertext has been decrypted using the payment preimage. + """ -class _UniffiConverterTypePaymentStatus(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - variant = buf.read_i32() - if variant == 1: - return PaymentStatus.PENDING - if variant == 2: - return PaymentStatus.COMPLETE - if variant == 3: - return PaymentStatus.FAILED - raise InternalError("Raw enum value doesn't match any cases") + def __init__(self): + raise RuntimeError("SuccessActionProcessed cannot be instantiated directly") - @staticmethod - def check_lower(value): - if value == PaymentStatus.PENDING: - return - if value == PaymentStatus.COMPLETE: - return - if value == PaymentStatus.FAILED: - return - raise ValueError(value) + # Each enum variant is a nested class of the enum itself. + class MESSAGE: + """ + Display a message to the user. + """ - @staticmethod - def write(value, buf): - if value == PaymentStatus.PENDING: - buf.write_i32(1) - if value == PaymentStatus.COMPLETE: - buf.write_i32(2) - if value == PaymentStatus.FAILED: - buf.write_i32(3) + message: "str" + def __init__(self,message: "str"): + self.message = message + def __str__(self): + return "SuccessActionProcessed.MESSAGE(message={})".format(self.message) + def __eq__(self, other): + if not other.is_MESSAGE(): + return False + if self.message != other.message: + return False + return True + + class URL: + """ + Display a URL to the user. + """ + description: "str" + url: "str" + def __init__(self,description: "str", url: "str"): + self.description = description + self.url = url + def __str__(self): + return "SuccessActionProcessed.URL(description={}, url={})".format(self.description, self.url) -class PaymentType(enum.Enum): - SENT = 0 - - RECEIVED = 1 + def __eq__(self, other): + if not other.is_URL(): + return False + if self.description != other.description: + return False + if self.url != other.url: + return False + return True + class AES: + """ + Decrypted AES payload (LUD-10). + """ + description: "str" + plaintext: "str" -class _UniffiConverterTypePaymentType(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - variant = buf.read_i32() - if variant == 1: - return PaymentType.SENT - if variant == 2: - return PaymentType.RECEIVED - raise InternalError("Raw enum value doesn't match any cases") - - @staticmethod - def check_lower(value): - if value == PaymentType.SENT: - return - if value == PaymentType.RECEIVED: - return - raise ValueError(value) - - @staticmethod - def write(value, buf): - if value == PaymentType.SENT: - buf.write_i32(1) - if value == PaymentType.RECEIVED: - buf.write_i32(2) - + def __init__(self,description: "str", plaintext: "str"): + self.description = description + self.plaintext = plaintext + def __str__(self): + return "SuccessActionProcessed.AES(description={}, plaintext={})".format(self.description, self.plaintext) + def __eq__(self, other): + if not other.is_AES(): + return False + if self.description != other.description: + return False + if self.plaintext != other.plaintext: + return False + return True + + + # For each variant, we have `is_NAME` and `is_name` methods for easily checking + # whether an instance is that variant. + def is_MESSAGE(self) -> bool: + return isinstance(self, SuccessActionProcessed.MESSAGE) + def is_message(self) -> bool: + return isinstance(self, SuccessActionProcessed.MESSAGE) + def is_URL(self) -> bool: + return isinstance(self, SuccessActionProcessed.URL) + def is_url(self) -> bool: + return isinstance(self, SuccessActionProcessed.URL) + def is_AES(self) -> bool: + return isinstance(self, SuccessActionProcessed.AES) + def is_aes(self) -> bool: + return isinstance(self, SuccessActionProcessed.AES) + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +SuccessActionProcessed.MESSAGE = type("SuccessActionProcessed.MESSAGE", (SuccessActionProcessed.MESSAGE, SuccessActionProcessed,), {}) # type: ignore +SuccessActionProcessed.URL = type("SuccessActionProcessed.URL", (SuccessActionProcessed.URL, SuccessActionProcessed,), {}) # type: ignore +SuccessActionProcessed.AES = type("SuccessActionProcessed.AES", (SuccessActionProcessed.AES, SuccessActionProcessed,), {}) # type: ignore -class PaymentTypeFilter(enum.Enum): - SENT = 0 - - RECEIVED = 1 - -class _UniffiConverterTypePaymentTypeFilter(_UniffiConverterRustBuffer): +class _UniffiConverterTypeSuccessActionProcessed(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return PaymentTypeFilter.SENT + return SuccessActionProcessed.MESSAGE( + _UniffiConverterString.read(buf), + ) if variant == 2: - return PaymentTypeFilter.RECEIVED + return SuccessActionProcessed.URL( + _UniffiConverterString.read(buf), + _UniffiConverterString.read(buf), + ) + if variant == 3: + return SuccessActionProcessed.AES( + _UniffiConverterString.read(buf), + _UniffiConverterString.read(buf), + ) raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - if value == PaymentTypeFilter.SENT: + if value.is_MESSAGE(): + _UniffiConverterString.check_lower(value.message) return - if value == PaymentTypeFilter.RECEIVED: + if value.is_URL(): + _UniffiConverterString.check_lower(value.description) + _UniffiConverterString.check_lower(value.url) + return + if value.is_AES(): + _UniffiConverterString.check_lower(value.description) + _UniffiConverterString.check_lower(value.plaintext) return raise ValueError(value) @staticmethod def write(value, buf): - if value == PaymentTypeFilter.SENT: + if value.is_MESSAGE(): buf.write_i32(1) - if value == PaymentTypeFilter.RECEIVED: + _UniffiConverterString.write(value.message, buf) + if value.is_URL(): buf.write_i32(2) + _UniffiConverterString.write(value.description, buf) + _UniffiConverterString.write(value.url, buf) + if value.is_AES(): + buf.write_i32(3) + _UniffiConverterString.write(value.description, buf) + _UniffiConverterString.write(value.plaintext, buf) @@ -4544,68 +5755,6 @@ def _uniffi_free(uniffi_handle): - -class NodeEventListener(typing.Protocol): - """ - Callback interface for receiving node events. - - `on_event` is invoked from the SDK's internal event-dispatch task. - Implementations should be cheap and non-blocking; to update UI, - dispatch to the main thread from inside the handler. - - Installed via `NodeBuilder::with_event_listener(...)` so events - emitted during node bring-up are captured. The polling-style - `Node::stream_node_events()` API is still available for callers - that prefer to drive events themselves. - """ - - def on_event(self, event: "NodeEvent"): - raise NotImplementedError - - -# Put all the bits inside a class to keep the top-level namespace clean -class _UniffiTraitImplNodeEventListener: - # For each method, generate a callback function to pass to Rust - - @_UNIFFI_CALLBACK_INTERFACE_NODE_EVENT_LISTENER_METHOD0 - def on_event( - uniffi_handle, - event, - uniffi_out_return, - uniffi_call_status_ptr, - ): - uniffi_obj = _UniffiConverterTypeNodeEventListener._handle_map.get(uniffi_handle) - def make_call(): - args = (_UniffiConverterTypeNodeEvent.lift(event), ) - method = uniffi_obj.on_event - return method(*args) - - - write_return_value = lambda v: None - _uniffi_trait_interface_call( - uniffi_call_status_ptr.contents, - make_call, - write_return_value, - ) - - @_UNIFFI_CALLBACK_INTERFACE_FREE - def _uniffi_free(uniffi_handle): - _UniffiConverterTypeNodeEventListener._handle_map.remove(uniffi_handle) - - # Generate the FFI VTable. This has a field for each callback interface method. - _uniffi_vtable = _UniffiVTableCallbackInterfaceNodeEventListener( - on_event, - _uniffi_free - ) - # Send Rust a pointer to the VTable. Note: this means we need to keep the struct alive forever, - # or else bad things will happen when Rust tries to access it. - _UniffiLib.uniffi_glsdk_fn_init_callback_vtable_nodeeventlistener(ctypes.byref(_uniffi_vtable)) - -# The _UniffiConverter which transforms the Callbacks in to Handles to pass to Rust. -_UniffiConverterTypeNodeEventListener = _UniffiCallbackInterfaceFfiConverter() - - - class _UniffiConverterOptionalUInt32(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -4789,17 +5938,44 @@ def read(cls, buf): if flag == 0: return None elif flag == 1: - return _UniffiConverterTypeListIndex.read(buf) + return _UniffiConverterTypeListIndex.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") + + + +class _UniffiConverterOptionalTypeNodeEvent(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + if value is not None: + _UniffiConverterTypeNodeEvent.check_lower(value) + + @classmethod + def write(cls, value, buf): + if value is None: + buf.write_u8(0) + return + + buf.write_u8(1) + _UniffiConverterTypeNodeEvent.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.read_u8() + if flag == 0: + return None + elif flag == 1: + return _UniffiConverterTypeNodeEvent.read(buf) else: raise InternalError("Unexpected flag byte for optional type") -class _UniffiConverterOptionalTypeNodeEvent(_UniffiConverterRustBuffer): +class _UniffiConverterOptionalTypePayStatus(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): if value is not None: - _UniffiConverterTypeNodeEvent.check_lower(value) + _UniffiConverterTypePayStatus.check_lower(value) @classmethod def write(cls, value, buf): @@ -4808,7 +5984,7 @@ def write(cls, value, buf): return buf.write_u8(1) - _UniffiConverterTypeNodeEvent.write(value, buf) + _UniffiConverterTypePayStatus.write(value, buf) @classmethod def read(cls, buf): @@ -4816,17 +5992,17 @@ def read(cls, buf): if flag == 0: return None elif flag == 1: - return _UniffiConverterTypeNodeEvent.read(buf) + return _UniffiConverterTypePayStatus.read(buf) else: raise InternalError("Unexpected flag byte for optional type") -class _UniffiConverterOptionalTypePayStatus(_UniffiConverterRustBuffer): +class _UniffiConverterOptionalTypeSuccessActionProcessed(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): if value is not None: - _UniffiConverterTypePayStatus.check_lower(value) + _UniffiConverterTypeSuccessActionProcessed.check_lower(value) @classmethod def write(cls, value, buf): @@ -4835,7 +6011,7 @@ def write(cls, value, buf): return buf.write_u8(1) - _UniffiConverterTypePayStatus.write(value, buf) + _UniffiConverterTypeSuccessActionProcessed.write(value, buf) @classmethod def read(cls, buf): @@ -4843,7 +6019,7 @@ def read(cls, buf): if flag == 0: return None elif flag == 1: - return _UniffiConverterTypePayStatus.read(buf) + return _UniffiConverterTypeSuccessActionProcessed.read(buf) else: raise InternalError("Unexpected flag byte for optional type") @@ -5567,6 +6743,34 @@ def list_peers(self, ): status. """ + raise NotImplementedError + def lnurl_pay(self, request: "LnUrlPayRequest"): + """ + Execute an LNURL-pay flow (LUD-06). + + Sends the chosen amount (and optional comment) to the service's + callback, receives and validates a BOLT11 invoice, pays it, and + processes any success action (LUD-09/10). + + Call the top-level `parse_input` first to obtain the + `LnUrlPayRequestData`, then build an `LnUrlPayRequest` with the + user's chosen amount. + """ + + raise NotImplementedError + def lnurl_withdraw(self, request: "LnUrlWithdrawRequest"): + """ + Execute an LNURL-withdraw flow (LUD-03). + + Creates an invoice on this node for the requested amount, sends + it to the service's callback URL, and the service pays it + asynchronously. + + Call the top-level `parse_input` first to obtain the + `LnUrlWithdrawRequestData`, then build an `LnUrlWithdrawRequest` + with the user's chosen amount. + """ + raise NotImplementedError def node_state(self, ): """ @@ -5648,9 +6852,11 @@ class Node(): """ _pointer: ctypes.c_void_p - - def __init__(self, *args, **kwargs): - raise ValueError("This class has no default constructor") + def __init__(self, credentials: "Credentials"): + _UniffiConverterTypeCredentials.check_lower(credentials) + + self._pointer = _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_constructor_node_new, + _UniffiConverterTypeCredentials.lower(credentials)) def __del__(self): # In case of partial initialization of instances. @@ -5873,6 +7079,54 @@ def list_peers(self, ) -> "ListPeersResponse": + def lnurl_pay(self, request: "LnUrlPayRequest") -> "LnUrlPayResult": + """ + Execute an LNURL-pay flow (LUD-06). + + Sends the chosen amount (and optional comment) to the service's + callback, receives and validates a BOLT11 invoice, pays it, and + processes any success action (LUD-09/10). + + Call the top-level `parse_input` first to obtain the + `LnUrlPayRequestData`, then build an `LnUrlPayRequest` with the + user's chosen amount. + """ + + _UniffiConverterTypeLnUrlPayRequest.check_lower(request) + + return _UniffiConverterTypeLnUrlPayResult.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_node_lnurl_pay,self._uniffi_clone_pointer(), + _UniffiConverterTypeLnUrlPayRequest.lower(request)) + ) + + + + + + def lnurl_withdraw(self, request: "LnUrlWithdrawRequest") -> "LnUrlWithdrawResult": + """ + Execute an LNURL-withdraw flow (LUD-03). + + Creates an invoice on this node for the requested amount, sends + it to the service's callback URL, and the service pays it + asynchronously. + + Call the top-level `parse_input` first to obtain the + `LnUrlWithdrawRequestData`, then build an `LnUrlWithdrawRequest` + with the user's chosen amount. + """ + + _UniffiConverterTypeLnUrlWithdrawRequest.check_lower(request) + + return _UniffiConverterTypeLnUrlWithdrawResult.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_node_lnurl_withdraw,self._uniffi_clone_pointer(), + _UniffiConverterTypeLnUrlWithdrawRequest.lower(request)) + ) + + + + + def node_state(self, ) -> "NodeState": """ Get a snapshot of the node's balances, capacity, and connectivity. @@ -6042,246 +7296,6 @@ def read(cls, buf: _UniffiRustBuffer): @classmethod def write(cls, value: NodeProtocol, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) -class NodeBuilderProtocol(typing.Protocol): - """ - Configurable Node construction. See module docs. - """ - - def connect(self, credentials: "bytes",mnemonic: "typing.Optional[str]"): - """ - Connect to an existing node using saved credentials and return - a connected Node with any configured modifiers applied. - - If `mnemonic` is `Some(...)`, the SDK spawns a signer for the - connected Node. If `None`, the Node is signerless and signing - happens elsewhere (paired device, CLN node's local signer, - hardware signer). - """ - - raise NotImplementedError - def recover(self, mnemonic: "str"): - """ - Recover credentials for an existing node and return a - connected Node with any configured modifiers applied. - - `mnemonic` is required — recovery drives the signer to - authenticate. - """ - - raise NotImplementedError - def register(self, mnemonic: "str",invite_code: "typing.Optional[str]"): - """ - Register a new Greenlight node and return a connected Node - with the SDK signer running and any configured modifiers - applied. - - `mnemonic` is required — registration drives the signer to - sign the registration challenge, so the SDK must hold the - seed for this call. - """ - - raise NotImplementedError - def register_or_recover(self, mnemonic: "str",invite_code: "typing.Optional[str]"): - """ - Try to recover; if the node doesn't exist, register a new one. - - `mnemonic` is required — both recover and register drive the - signer. - """ - - raise NotImplementedError - def with_event_listener(self, listener: "NodeEventListener"): - """ - Install a node event listener. Events fire from the moment the - gRPC stream is established by the build call (`register` / - `recover` / `connect` / …), so attach the listener via the - builder rather than after the fact to capture events from the - very first moment. - - Returns the same builder for fluent chaining. - """ - - raise NotImplementedError -# NodeBuilder is a Rust-only trait - it's a wrapper around a Rust implementation. -class NodeBuilder(): - """ - Configurable Node construction. See module docs. - """ - - _pointer: ctypes.c_void_p - def __init__(self, config: "Config"): - """ - Create a builder for a Node with `config`. No I/O happens - until you call `connect` / `register` / `recover` / - `register_or_recover`. - """ - - _UniffiConverterTypeConfig.check_lower(config) - - self._pointer = _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_constructor_nodebuilder_new, - _UniffiConverterTypeConfig.lower(config)) - - def __del__(self): - # In case of partial initialization of instances. - pointer = getattr(self, "_pointer", None) - if pointer is not None: - _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_free_nodebuilder, pointer) - - def _uniffi_clone_pointer(self): - return _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_clone_nodebuilder, self._pointer) - - # Used by alternative constructors or any methods which return this type. - @classmethod - def _make_instance_(cls, pointer): - # Lightly yucky way to bypass the usual __init__ logic - # and just create a new instance with the required pointer. - inst = cls.__new__(cls) - inst._pointer = pointer - return inst - - - def connect(self, credentials: "bytes",mnemonic: "typing.Optional[str]") -> "Node": - """ - Connect to an existing node using saved credentials and return - a connected Node with any configured modifiers applied. - - If `mnemonic` is `Some(...)`, the SDK spawns a signer for the - connected Node. If `None`, the Node is signerless and signing - happens elsewhere (paired device, CLN node's local signer, - hardware signer). - """ - - _UniffiConverterBytes.check_lower(credentials) - - _UniffiConverterOptionalString.check_lower(mnemonic) - - return _UniffiConverterTypeNode.lift( - _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_connect,self._uniffi_clone_pointer(), - _UniffiConverterBytes.lower(credentials), - _UniffiConverterOptionalString.lower(mnemonic)) - ) - - - - - - def recover(self, mnemonic: "str") -> "Node": - """ - Recover credentials for an existing node and return a - connected Node with any configured modifiers applied. - - `mnemonic` is required — recovery drives the signer to - authenticate. - """ - - _UniffiConverterString.check_lower(mnemonic) - - return _UniffiConverterTypeNode.lift( - _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_recover,self._uniffi_clone_pointer(), - _UniffiConverterString.lower(mnemonic)) - ) - - - - - - def register(self, mnemonic: "str",invite_code: "typing.Optional[str]") -> "Node": - """ - Register a new Greenlight node and return a connected Node - with the SDK signer running and any configured modifiers - applied. - - `mnemonic` is required — registration drives the signer to - sign the registration challenge, so the SDK must hold the - seed for this call. - """ - - _UniffiConverterString.check_lower(mnemonic) - - _UniffiConverterOptionalString.check_lower(invite_code) - - return _UniffiConverterTypeNode.lift( - _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register,self._uniffi_clone_pointer(), - _UniffiConverterString.lower(mnemonic), - _UniffiConverterOptionalString.lower(invite_code)) - ) - - - - - - def register_or_recover(self, mnemonic: "str",invite_code: "typing.Optional[str]") -> "Node": - """ - Try to recover; if the node doesn't exist, register a new one. - - `mnemonic` is required — both recover and register drive the - signer. - """ - - _UniffiConverterString.check_lower(mnemonic) - - _UniffiConverterOptionalString.check_lower(invite_code) - - return _UniffiConverterTypeNode.lift( - _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register_or_recover,self._uniffi_clone_pointer(), - _UniffiConverterString.lower(mnemonic), - _UniffiConverterOptionalString.lower(invite_code)) - ) - - - - - - def with_event_listener(self, listener: "NodeEventListener") -> "NodeBuilder": - """ - Install a node event listener. Events fire from the moment the - gRPC stream is established by the build call (`register` / - `recover` / `connect` / …), so attach the listener via the - builder rather than after the fact to capture events from the - very first moment. - - Returns the same builder for fluent chaining. - """ - - _UniffiConverterTypeNodeEventListener.check_lower(listener) - - return _UniffiConverterTypeNodeBuilder.lift( - _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_with_event_listener,self._uniffi_clone_pointer(), - _UniffiConverterTypeNodeEventListener.lower(listener)) - ) - - - - - - -class _UniffiConverterTypeNodeBuilder: - - @staticmethod - def lift(value: int): - return NodeBuilder._make_instance_(value) - - @staticmethod - def check_lower(value: NodeBuilder): - if not isinstance(value, NodeBuilder): - raise TypeError("Expected NodeBuilder instance, {} found".format(type(value).__name__)) - - @staticmethod - def lower(value: NodeBuilderProtocol): - if not isinstance(value, NodeBuilder): - raise TypeError("Expected NodeBuilder instance, {} found".format(type(value).__name__)) - return value._uniffi_clone_pointer() - - @classmethod - def read(cls, buf: _UniffiRustBuffer): - ptr = buf.read_u64() - if ptr == 0: - raise InternalError("Raw pointer value was null") - return cls.lift(ptr) - - @classmethod - def write(cls, value: NodeBuilderProtocol, buf: _UniffiRustBuffer): - buf.write_u64(cls.lower(value)) class NodeEventStreamProtocol(typing.Protocol): """ A stream of node events. Call `next()` to receive the next event. @@ -6610,22 +7624,192 @@ def read(cls, buf: _UniffiRustBuffer): def write(cls, value: SignerProtocol, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) -# Async support +# Async support# RustFuturePoll values +_UNIFFI_RUST_FUTURE_POLL_READY = 0 +_UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1 + +# Stores futures for _uniffi_continuation_callback +_UniffiContinuationHandleMap = _UniffiHandleMap() + +_UNIFFI_GLOBAL_EVENT_LOOP = None + +""" +Set the event loop to use for async functions + +This is needed if some async functions run outside of the eventloop, for example: + - A non-eventloop thread is spawned, maybe from `EventLoop.run_in_executor` or maybe from the + Rust code spawning its own thread. + - The Rust code calls an async callback method from a sync callback function, using something + like `pollster` to block on the async call. + +In this case, we need an event loop to run the Python async function, but there's no eventloop set +for the thread. Use `uniffi_set_event_loop` to force an eventloop to be used in this case. +""" +def uniffi_set_event_loop(eventloop: asyncio.BaseEventLoop): + global _UNIFFI_GLOBAL_EVENT_LOOP + _UNIFFI_GLOBAL_EVENT_LOOP = eventloop + +def _uniffi_get_event_loop(): + if _UNIFFI_GLOBAL_EVENT_LOOP is not None: + return _UNIFFI_GLOBAL_EVENT_LOOP + else: + return asyncio.get_running_loop() + +# Continuation callback for async functions +# lift the return value or error and resolve the future, causing the async function to resume. +@_UNIFFI_RUST_FUTURE_CONTINUATION_CALLBACK +def _uniffi_continuation_callback(future_ptr, poll_code): + (eventloop, future) = _UniffiContinuationHandleMap.remove(future_ptr) + eventloop.call_soon_threadsafe(_uniffi_set_future_result, future, poll_code) + +def _uniffi_set_future_result(future, poll_code): + if not future.cancelled(): + future.set_result(poll_code) + +async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, lift_func, error_ffi_converter): + try: + eventloop = _uniffi_get_event_loop() + + # Loop and poll until we see a _UNIFFI_RUST_FUTURE_POLL_READY value + while True: + future = eventloop.create_future() + ffi_poll( + rust_future, + _uniffi_continuation_callback, + _UniffiContinuationHandleMap.insert((eventloop, future)), + ) + poll_code = await future + if poll_code == _UNIFFI_RUST_FUTURE_POLL_READY: + break + + return lift_func( + _uniffi_rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) + ) + finally: + ffi_free(rust_future) -def parse_input(input: "str") -> "InputType": +def connect(mnemonic: "str",credentials: "bytes",config: "Config") -> "Node": + """ + Connect to an existing Greenlight node using previously saved credentials. """ - Parse a string and identify whether it's a BOLT11 invoice or a node ID. - Strips `lightning:` / `LIGHTNING:` prefixes automatically. - Works offline — no node connection needed. + _UniffiConverterString.check_lower(mnemonic) + + _UniffiConverterBytes.check_lower(credentials) + + _UniffiConverterTypeConfig.check_lower(config) + + return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_connect, + _UniffiConverterString.lower(mnemonic), + _UniffiConverterBytes.lower(credentials), + _UniffiConverterTypeConfig.lower(config))) + + +def parse_input(input: "str") -> "ParsedInput": + """ + Synchronously classify the input. **No HTTP, no I/O.** + + Recognises BOLT11 invoices, node IDs, LNURL bech32 strings, and + Lightning Addresses. Strips `lightning:` / `LIGHTNING:` prefixes + automatically. LNURL inputs are decoded to their underlying URL + but **not fetched** — the caller chooses whether to resolve + further (via `resolve_input`) or to surface the URL to the user + as-is. + + Use this for offline operations like clipboard validation or + invoice sanity checks. Use `resolve_input` for the QR-scan flow + where you want the resolved pay/withdraw data in one call. """ _UniffiConverterString.check_lower(input) - return _UniffiConverterTypeInputType.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_parse_input, + return _UniffiConverterTypeParsedInput.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_parse_input, _UniffiConverterString.lower(input))) +def recover(mnemonic: "str",config: "Config") -> "Node": + """ + Recover credentials for an existing Greenlight node and return a connected Node. + + The app should call `node.credentials()` to get the credential bytes + and persist them for future `connect()` calls. + """ + + _UniffiConverterString.check_lower(mnemonic) + + _UniffiConverterTypeConfig.check_lower(config) + + return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_recover, + _UniffiConverterString.lower(mnemonic), + _UniffiConverterTypeConfig.lower(config))) + + +def register(mnemonic: "str",invite_code: "typing.Optional[str]",config: "Config") -> "Node": + """ + Register a new Greenlight node and return a connected Node with signer running. + + The app should call `node.credentials()` to get the credential bytes + and persist them for future `connect()` calls. + """ + + _UniffiConverterString.check_lower(mnemonic) + + _UniffiConverterOptionalString.check_lower(invite_code) + + _UniffiConverterTypeConfig.check_lower(config) + + return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_register, + _UniffiConverterString.lower(mnemonic), + _UniffiConverterOptionalString.lower(invite_code), + _UniffiConverterTypeConfig.lower(config))) + + +def register_or_recover(mnemonic: "str",invite_code: "typing.Optional[str]",config: "Config") -> "Node": + """ + Try to recover an existing node; if none exists, register a new one. + """ + + _UniffiConverterString.check_lower(mnemonic) + + _UniffiConverterOptionalString.check_lower(invite_code) + + _UniffiConverterTypeConfig.check_lower(config) + + return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_register_or_recover, + _UniffiConverterString.lower(mnemonic), + _UniffiConverterOptionalString.lower(invite_code), + _UniffiConverterTypeConfig.lower(config))) + +async def resolve_input(input: "str") -> "ResolvedInput": + + """ + Asynchronously classify and resolve the input. + + Internally calls `parse_input` for offline classification, then + for LNURL bech32 strings and Lightning Addresses performs the + HTTP GET to the LNURL endpoint and returns typed pay or withdraw + request data. For BOLT11 invoices and node IDs it returns + immediately without I/O. + + Strips `lightning:` / `LIGHTNING:` prefixes automatically. + """ + + _UniffiConverterString.check_lower(input) + + return await _uniffi_rust_call_async( + _UniffiLib.uniffi_glsdk_fn_func_resolve_input( + _UniffiConverterString.lower(input)), + _UniffiLib.ffi_glsdk_rust_future_poll_rust_buffer, + _UniffiLib.ffi_glsdk_rust_future_complete_rust_buffer, + _UniffiLib.ffi_glsdk_rust_future_free_rust_buffer, + # lift function + _UniffiConverterTypeResolvedInput.lift, + + # Error FFI converter +_UniffiConverterTypeError, + + ) + def set_log_level(level: "LogLevel") -> None: """ Change the log filter at runtime without reinstalling the listener. @@ -6662,17 +7846,21 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "ChannelSide", "ChannelState", "Error", - "InputType", "InvoiceStatus", "ListIndex", + "LnUrlPayResult", + "LnUrlWithdrawResult", "LogLevel", "Network", "NodeEvent", "OutputStatus", + "ParsedInput", "PayStatus", "PaymentStatus", "PaymentType", "PaymentTypeFilter", + "ResolvedInput", + "SuccessActionProcessed", "FundChannel", "FundOutput", "GetInfoResponse", @@ -6684,6 +7872,14 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "ListPaysResponse", "ListPeerChannelsResponse", "ListPeersResponse", + "LnUrlErrorData", + "LnUrlPayErrorData", + "LnUrlPayRequest", + "LnUrlPayRequestData", + "LnUrlPaySuccessData", + "LnUrlWithdrawRequest", + "LnUrlWithdrawRequestData", + "LnUrlWithdrawSuccessData", "LogEntry", "NodeState", "OnchainReceiveResponse", @@ -6695,7 +7891,12 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "PeerChannel", "ReceiveResponse", "SendResponse", + "connect", "parse_input", + "recover", + "register", + "register_or_recover", + "resolve_input", "set_log_level", "set_logger", "Config", @@ -6703,11 +7904,9 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "DeveloperCert", "Handle", "Node", - "NodeBuilder", "NodeEventStream", "Scheduler", "Signer", "LogListener", - "NodeEventListener", ] diff --git a/libs/gl-sdk/src/input.rs b/libs/gl-sdk/src/input.rs index c5140702c..439d675e6 100644 --- a/libs/gl-sdk/src/input.rs +++ b/libs/gl-sdk/src/input.rs @@ -1,6 +1,26 @@ -// Input parsing for BOLT11 invoices and Lightning node IDs. -// Works offline — no node connection needed. +// Input parsing for BOLT11 invoices, Lightning node IDs, LNURL +// strings, and Lightning Addresses. +// +// Two entry points with explicit cost contracts: +// +// * `parse_input(input)` — synchronous, offline, no I/O. Returns +// `ParsedInput` identifying *what* the input is. LNURL bech32 +// strings are decoded to their underlying URL; Lightning +// Addresses are returned as the unparsed `user@host` form. The +// caller decides whether to resolve further. +// +// * `resolve_input(input)` — asynchronous, network-touching. Calls +// `parse_input` internally, then for the LNURL / Lightning +// Address branches fetches the endpoint to produce typed pay or +// withdraw request data. BOLT11 and node IDs pass through +// without I/O. +// +// Wallets that want offline classification (clipboard checks, +// invoice sanity-checks on the send screen) call `parse_input`. +// Wallets handling a QR scan that should proceed straight to the +// pay/withdraw screen call `resolve_input`. +use crate::lnurl::{LnUrlPayRequestData, LnUrlWithdrawRequestData}; use crate::Error; /// Parsed BOLT11 invoice with extracted fields. @@ -22,20 +42,51 @@ pub struct ParsedInvoice { pub timestamp: u64, } -/// The result of parsing user input. +/// The result of `parse_input`: an offline classification of the +/// input. No HTTP, no I/O. LNURL bech32 strings are returned as their +/// decoded URL; Lightning Addresses as the unparsed `user@host` form. #[derive(Clone, uniffi::Enum)] -pub enum InputType { +pub enum ParsedInput { /// A BOLT11 Lightning invoice. Bolt11 { invoice: ParsedInvoice }, - /// A Lightning node public key (66 hex characters, 33 bytes compressed). + /// A Lightning node public key. NodeId { node_id: String }, + /// An LNURL bech32 string (LUD-01) decoded to its underlying URL. + /// Pass to `resolve_input` (or fetch yourself) to determine + /// whether it's a pay, withdraw, or auth endpoint. + LnUrl { url: String }, + /// A Lightning Address (LUD-16) in the form `user@host`. The + /// well-known URL is not constructed offline; call `resolve_input` + /// to fetch and classify. + LnUrlAddress { address: String }, } -/// Parse a string and identify whether it's a BOLT11 invoice or a node ID. +/// The result of `resolve_input`: a fully-resolved input ready for +/// the caller's next action. LNURL bech32 strings and Lightning +/// Addresses are resolved over HTTP into typed pay or withdraw +/// request data; BOLT11 and node IDs pass through unchanged. +#[derive(Clone, uniffi::Enum)] +pub enum ResolvedInput { + /// A BOLT11 Lightning invoice. No HTTP was performed. + Bolt11 { invoice: ParsedInvoice }, + /// A Lightning node public key. No HTTP was performed. + NodeId { node_id: String }, + /// An LNURL-pay endpoint with the service's parameters fetched. + LnUrlPay { data: LnUrlPayRequestData }, + /// An LNURL-withdraw endpoint with the service's parameters fetched. + LnUrlWithdraw { data: LnUrlWithdrawRequestData }, +} + +/// Synchronously classify the input. **No HTTP, no I/O.** /// -/// Strips `lightning:` / `LIGHTNING:` prefixes automatically. -/// Returns an error if the input is not recognized or is malformed. -pub fn parse_input(input: String) -> Result { +/// Recognises BOLT11 invoices, node IDs, LNURL bech32 strings, and +/// Lightning Addresses. Strips `lightning:` / `LIGHTNING:` prefixes +/// automatically. +/// +/// LNURL inputs are decoded to their underlying URL but **not +/// fetched** — the caller chooses whether to resolve further (via +/// `resolve_input`) or to surface the URL to the user as-is. +pub fn parse_input(input: String) -> Result { let trimmed = input.trim(); if trimmed.is_empty() { return Err(Error::Other("Empty input".to_string())); @@ -50,22 +101,118 @@ pub fn parse_input(input: String) -> Result { trimmed }; + // Try LNURL bech32 (must come before BOLT11 since both start with "ln") + if let Some(result) = try_parse_lnurl(stripped) { + return result; + } + // Try BOLT11 - if let Some(input_type) = try_parse_bolt11(stripped) { - return input_type; + if let Some(result) = try_parse_bolt11(stripped) { + return result; + } + + // Try Lightning Address (user@domain) + if let Some(result) = try_parse_lightning_address(stripped) { + return Ok(result); } // Try Node ID - if let Some(input_type) = try_parse_node_id(stripped) { - return Ok(input_type); + if let Some(result) = try_parse_node_id(stripped) { + return Ok(result); } Err(Error::Other("Unrecognized input".to_string())) } +/// Asynchronously classify and resolve the input. +/// +/// Internally calls `parse_input`. For BOLT11 and node IDs returns +/// immediately without I/O. For LNURL bech32 strings and Lightning +/// Addresses, performs the HTTP GET and returns the typed pay or +/// withdraw request data. +pub async fn resolve_input(input: String) -> Result { + use gl_client::lnurl::models::LnUrlHttpClearnetClient; + use gl_client::lnurl::{LnUrlResponse, LNURL}; + + // Capture the user's original input (post-trim) so that + // `data.lnurl` on the resolved response carries the exact string + // the caller handed us. + let original = input.trim().to_string(); + + // The two LNURL-shaped branches converge to a single HTTP fetch + // — the only branch-specific bit is how the URL is derived. + let url = match parse_input(input)? { + ParsedInput::Bolt11 { invoice } => return Ok(ResolvedInput::Bolt11 { invoice }), + ParsedInput::NodeId { node_id } => return Ok(ResolvedInput::NodeId { node_id }), + ParsedInput::LnUrl { url } => url, + ParsedInput::LnUrlAddress { address } => { + gl_client::lnurl::pay::parse_lightning_address(&address) + .map_err(|e| Error::Other(e.to_string()))? + } + }; + + let client = LNURL::new(LnUrlHttpClearnetClient::new()); + let response = client + .resolve(&url) + .await + .map_err(|e| Error::Other(e.to_string()))?; + + Ok(match response { + LnUrlResponse::Pay(d) => { + let mut data: LnUrlPayRequestData = d.into(); + data.lnurl = original; + ResolvedInput::LnUrlPay { data } + } + LnUrlResponse::Withdraw(d) => { + let mut data: LnUrlWithdrawRequestData = d.into(); + data.lnurl = original; + ResolvedInput::LnUrlWithdraw { data } + } + }) +} + +/// Try parsing as an LNURL bech32 string (LUD-01). +/// Returns None if the input doesn't look like an LNURL. +fn try_parse_lnurl(input: &str) -> Option> { + if !input.to_uppercase().starts_with("LNURL1") { + return None; + } + match gl_client::lnurl::utils::parse_lnurl(input) { + Ok(url) => Some(Ok(ParsedInput::LnUrl { url })), + Err(e) => Some(Err(Error::Other(format!("Invalid LNURL: {}", e)))), + } +} + +/// Try parsing as a Lightning Address (LUD-16): `user@domain.tld`. +fn try_parse_lightning_address(input: &str) -> Option { + let parts: Vec<&str> = input.split('@').collect(); + if parts.len() != 2 { + return None; + } + let (username, domain) = (parts[0], parts[1]); + if username.is_empty() || domain.is_empty() { + return None; + } + // Domain must contain a dot (rules out bare hostnames and emails + // to local domains which aren't valid Lightning Addresses). + if !domain.contains('.') { + return None; + } + // Username: alphanumeric + limited symbols per LUD-16. + if !username + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') + { + return None; + } + Some(ParsedInput::LnUrlAddress { + address: input.to_string(), + }) +} + /// Try parsing as a BOLT11 invoice. Returns None if the input doesn't /// look like an invoice, or Some(Result) if it does (even if malformed). -fn try_parse_bolt11(input: &str) -> Option> { +fn try_parse_bolt11(input: &str) -> Option> { let lower = input.to_lowercase(); if !lower.starts_with("lnbc") && !lower.starts_with("lntb") && !lower.starts_with("lnbcrt") { return None; @@ -92,16 +239,14 @@ fn try_parse_bolt11(input: &str) -> Option> { }; let amount_msat = parsed.amount_milli_satoshis(); - let expiry = parsed.expiry_time().as_secs(); - let timestamp = parsed .timestamp() .duration_since(std::time::SystemTime::UNIX_EPOCH) .unwrap_or_default() .as_secs(); - Some(Ok(InputType::Bolt11 { + Some(Ok(ParsedInput::Bolt11 { invoice: ParsedInvoice { bolt11: input.to_string(), payee_pubkey: Some(payee_pubkey), @@ -115,7 +260,7 @@ fn try_parse_bolt11(input: &str) -> Option> { } /// Try parsing as a node ID (66-char hex → 33-byte compressed pubkey). -fn try_parse_node_id(input: &str) -> Option { +fn try_parse_node_id(input: &str) -> Option { if input.len() != 66 { return None; } @@ -127,7 +272,140 @@ fn try_parse_node_id(input: &str) -> Option { if bytes[0] != 0x02 && bytes[0] != 0x03 { return None; } - Some(InputType::NodeId { + Some(ParsedInput::NodeId { node_id: input.to_string(), }) } + +#[cfg(test)] +mod tests { + use super::*; + + fn parsed_variant_name(t: &ParsedInput) -> &'static str { + match t { + ParsedInput::Bolt11 { .. } => "Bolt11", + ParsedInput::NodeId { .. } => "NodeId", + ParsedInput::LnUrl { .. } => "LnUrl", + ParsedInput::LnUrlAddress { .. } => "LnUrlAddress", + } + } + + fn resolved_variant_name(t: &ResolvedInput) -> &'static str { + match t { + ResolvedInput::Bolt11 { .. } => "Bolt11", + ResolvedInput::NodeId { .. } => "NodeId", + ResolvedInput::LnUrlPay { .. } => "LnUrlPay", + ResolvedInput::LnUrlWithdraw { .. } => "LnUrlWithdraw", + } + } + + // ── parse_input (sync) ────────────────────────────────────── + + #[test] + fn test_parse_input_bolt11() { + let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqclj9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9dha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58aguqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphmsywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0vp62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh38s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5j5r6drg6k6zcqj0fcwg"; + match parse_input(invoice.to_string()).unwrap() { + ParsedInput::Bolt11 { invoice: parsed } => assert_eq!(parsed.amount_msat, Some(10)), + other => panic!("Expected Bolt11, got {}", parsed_variant_name(&other)), + } + } + + #[test] + fn test_parse_input_bolt11_with_lightning_prefix() { + let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqclj9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9dha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58aguqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphmsywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0vp62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh38s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5j5r6drg6k6zcqj0fcwg"; + let result = parse_input(format!("lightning:{}", invoice)).unwrap(); + assert!(matches!(result, ParsedInput::Bolt11 { .. })); + } + + #[test] + fn test_parse_input_node_id() { + let node_id = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"; + match parse_input(node_id.to_string()).unwrap() { + ParsedInput::NodeId { node_id: id } => assert_eq!(id, node_id), + other => panic!("Expected NodeId, got {}", parsed_variant_name(&other)), + } + } + + #[test] + fn test_parse_input_lnurl_decodes_url() { + // Bech32-encoded "https://service.com/lnurl" + let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; + match parse_input(lnurl.to_string()).unwrap() { + ParsedInput::LnUrl { url } => assert!(url.starts_with("https://")), + other => panic!("Expected LnUrl, got {}", parsed_variant_name(&other)), + } + } + + #[test] + fn test_parse_input_lightning_address_returns_address() { + match parse_input("user@example.com".to_string()).unwrap() { + ParsedInput::LnUrlAddress { address } => assert_eq!(address, "user@example.com"), + other => panic!("Expected LnUrlAddress, got {}", parsed_variant_name(&other)), + } + } + + #[test] + fn test_parse_input_invalid_lnurl_errors() { + assert!(parse_input("LNURL1INVALIDDATA".to_string()).is_err()); + } + + #[test] + fn test_parse_input_address_no_dot_in_domain_errors() { + assert!(parse_input("user@localhost".to_string()).is_err()); + } + + #[test] + fn test_parse_input_empty_address_parts_errors() { + assert!(parse_input("@example.com".to_string()).is_err()); + assert!(parse_input("user@".to_string()).is_err()); + } + + #[test] + fn test_parse_input_unrecognized_errors() { + assert!(parse_input("hello world".to_string()).is_err()); + assert!(parse_input("".to_string()).is_err()); + assert!(parse_input(" ".to_string()).is_err()); + assert!(parse_input("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string()).is_err()); + } + + #[test] + fn test_parse_input_invalid_node_id_errors() { + // 66 chars but starts with 0x04 (uncompressed pubkey prefix) + assert!(parse_input( + "04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".to_string() + ) + .is_err()); + // 66 non-hex chars + assert!(parse_input( + "not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string() + ) + .is_err()); + } + + // ── resolve_input pass-through paths (no HTTP needed) ─────── + + #[test] + fn test_resolve_input_bolt11_passes_through() { + let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqclj9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9dha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58aguqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphmsywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0vp62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh38s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5j5r6drg6k6zcqj0fcwg"; + match crate::util::exec(resolve_input(invoice.to_string())).unwrap() { + ResolvedInput::Bolt11 { invoice: parsed } => { + assert_eq!(parsed.amount_msat, Some(10)) + } + other => panic!("Expected Bolt11, got {}", resolved_variant_name(&other)), + } + } + + #[test] + fn test_resolve_input_node_id_passes_through() { + let node_id = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"; + match crate::util::exec(resolve_input(node_id.to_string())).unwrap() { + ResolvedInput::NodeId { node_id: id } => assert_eq!(id, node_id), + other => panic!("Expected NodeId, got {}", resolved_variant_name(&other)), + } + } + + #[test] + fn test_resolve_input_invalid_lnurl_errors_before_http() { + assert!(crate::util::exec(resolve_input("LNURL1INVALIDDATA".to_string())).is_err()); + } +} diff --git a/libs/gl-sdk/src/lib.rs b/libs/gl-sdk/src/lib.rs index 4fbd70b1f..c5a97e050 100644 --- a/libs/gl-sdk/src/lib.rs +++ b/libs/gl-sdk/src/lib.rs @@ -27,6 +27,7 @@ pub enum Error { mod config; mod credentials; mod input; +mod lnurl; mod logging; mod node; mod node_builder; @@ -45,8 +46,13 @@ pub use crate::{ OnchainSendResponse, OutputStatus, Pay, PayStatus, Payment, PaymentStatus, PaymentType, PaymentTypeFilter, Peer, PeerChannel, ReceiveResponse, SendResponse, }, - input::{InputType, ParsedInvoice}, + input::{ParsedInput, ParsedInvoice, ResolvedInput}, logging::{LogEntry, LogLevel, LogListener}, + lnurl::{ + LnUrlErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlPayResult, + LnUrlPaySuccessData, LnUrlWithdrawRequest, LnUrlWithdrawRequestData, + LnUrlWithdrawResult, LnUrlWithdrawSuccessData, SuccessActionProcessed, + }, node_builder::NodeBuilder, scheduler::Scheduler, signer::{Handle, Signer}, @@ -106,7 +112,7 @@ fn schedule_node( .map_err(|e| Error::Other(e.to_string()))?; let handle = signer::Handle::spawn(authenticated_signer); - let node = node::Node::with_signer(credentials, handle)?; + let node = node::Node::with_signer(credentials, handle, network)?; Ok(Arc::new(node)) } @@ -169,7 +175,7 @@ pub(crate) fn connect_internal( .map_err(|e| Error::Other(e.to_string()))?; let handle = signer::Handle::spawn(authenticated_signer); - let node = node::Node::with_signer(creds, handle)?; + let node = node::Node::with_signer(creds, handle, network)?; Ok(Arc::new(node)) } @@ -227,15 +233,37 @@ pub(crate) fn connect_signerless_internal( Ok(Arc::new(node)) } -/// Parse a string and identify whether it's a BOLT11 invoice or a node ID. +/// Synchronously classify the input. **No HTTP, no I/O.** /// -/// Strips `lightning:` / `LIGHTNING:` prefixes automatically. -/// Works offline — no node connection needed. +/// Recognises BOLT11 invoices, node IDs, LNURL bech32 strings, and +/// Lightning Addresses. Strips `lightning:` / `LIGHTNING:` prefixes +/// automatically. LNURL inputs are decoded to their underlying URL +/// but **not fetched** — the caller chooses whether to resolve +/// further (via `resolve_input`) or to surface the URL to the user +/// as-is. +/// +/// Use this for offline operations like clipboard validation or +/// invoice sanity checks. Use `resolve_input` for the QR-scan flow +/// where you want the resolved pay/withdraw data in one call. #[uniffi::export] -pub fn parse_input(input: String) -> Result { +pub fn parse_input(input: String) -> Result { input::parse_input(input) } +/// Asynchronously classify and resolve the input. +/// +/// Internally calls `parse_input` for offline classification, then +/// for LNURL bech32 strings and Lightning Addresses performs the +/// HTTP GET to the LNURL endpoint and returns typed pay or withdraw +/// request data. For BOLT11 invoices and node IDs it returns +/// immediately without I/O. +/// +/// Strips `lightning:` / `LIGHTNING:` prefixes automatically. +#[uniffi::export(async_runtime = "tokio")] +pub async fn resolve_input(input: String) -> Result { + input::resolve_input(input).await +} + /// Set up SDK logging. Call once before any other SDK function. /// /// The listener receives all log messages from the SDK and the diff --git a/libs/gl-sdk/src/lnurl.rs b/libs/gl-sdk/src/lnurl.rs new file mode 100644 index 000000000..53030c1f9 --- /dev/null +++ b/libs/gl-sdk/src/lnurl.rs @@ -0,0 +1,315 @@ +// LNURL types for UniFFI language bindings. +// +// These are thin wrappers around gl-client's protocol types, adding +// UniFFI annotations so they can be exported to Python, Kotlin, Swift, +// and Ruby. Protocol logic lives in gl-client; this module only does +// type conversion. + +use gl_client::lnurl::models as wire; + +// ── Resolved endpoint data ────────────────────────────────────────── + +/// Data from an LNURL-pay endpoint (LUD-06). +/// +/// Contains the service's accepted amount range and metadata. +/// Returned inside `InputType::LnUrlPay` after `parse_input` resolves +/// an LNURL or Lightning Address. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlPayRequestData { + /// The callback URL to request an invoice from. + pub callback: String, + /// Minimum amount the service accepts, in millisatoshis. + pub min_sendable: u64, + /// Maximum amount the service accepts, in millisatoshis. + pub max_sendable: u64, + /// Raw metadata JSON string (array of `["mime", "content"]` pairs). + pub metadata: String, + /// Maximum comment length the service accepts. 0 means no comments. + pub comment_allowed: u64, + /// Human-readable description extracted from metadata. + pub description: String, + /// The original LNURL or lightning address that was resolved. + pub lnurl: String, +} + +/// Data from an LNURL-withdraw endpoint (LUD-03). +/// +/// Contains the service's accepted withdrawal range and session key. +/// Returned inside `InputType::LnUrlWithdraw` after `parse_input` +/// resolves an LNURL. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlWithdrawRequestData { + /// The callback URL to submit the invoice to. + pub callback: String, + /// Ephemeral secret linking this wallet session to the service. + pub k1: String, + /// Default description for the invoice. + pub default_description: String, + /// Minimum withdrawable amount in millisatoshis. + pub min_withdrawable: u64, + /// Maximum withdrawable amount in millisatoshis. + pub max_withdrawable: u64, + /// The original LNURL that was resolved. + pub lnurl: String, +} + +// ── User request types ────────────────────────────────────────────── + +/// Request to execute an LNURL-pay flow. +/// +/// Combines the resolved service data with the user's chosen amount. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlPayRequest { + /// The resolved pay request data from `parse_input()`. + pub data: LnUrlPayRequestData, + /// Amount to pay in millisatoshis. + pub amount_msat: u64, + /// Optional comment to send with the payment. + pub comment: Option, + /// When true (the default), a URL success action is rejected if its + /// domain differs from the callback's domain. + /// + /// This is a wallet-side safety convention, not a LUD-09 requirement: + /// LUD-09 does not mandate same-domain URLs, but a divergent domain + /// can be used to phish users, so the SDK rejects it by default. + /// Set to `Some(false)` only if you have a specific reason to trust + /// cross-domain success-action URLs from this service. + pub validate_success_action_url: Option, +} + +/// Request to execute an LNURL-withdraw flow. +/// +/// Combines the resolved service data with the user's chosen amount. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlWithdrawRequest { + /// The resolved withdraw request data from `parse_input()`. + pub data: LnUrlWithdrawRequestData, + /// Amount to withdraw in millisatoshis. + pub amount_msat: u64, + /// Optional description for the invoice (overrides default). + pub description: Option, +} + +// ── Result types ──────────────────────────────────────────────────── + +/// Result of an LNURL-pay operation. +#[derive(Clone, uniffi::Enum)] +pub enum LnUrlPayResult { + /// Payment succeeded. + EndpointSuccess { data: LnUrlPaySuccessData }, + /// The LNURL service returned an error before the invoice was paid. + EndpointError { data: LnUrlErrorData }, + /// The invoice was fetched successfully but paying it failed. + PayError { data: LnUrlPayErrorData }, +} + +/// Successful LNURL-pay result data. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlPaySuccessData { + /// The payment preimage (proof of payment), hex-encoded. + pub payment_preimage: String, + /// Optional success action from the service (LUD-09). + pub success_action: Option, +} + +/// Details of a failed LNURL-pay attempt on the pay phase. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlPayErrorData { + /// Hex-encoded payment hash of the invoice the service returned. + pub payment_hash: String, + /// Human-readable reason the pay attempt failed. + pub reason: String, +} + +/// Result of an LNURL-withdraw operation. +#[derive(Clone, uniffi::Enum)] +pub enum LnUrlWithdrawResult { + /// The service accepted our invoice and will pay it. + Ok { data: LnUrlWithdrawSuccessData }, + /// The LNURL service returned an error. + ErrorStatus { data: LnUrlErrorData }, +} + +/// Successful LNURL-withdraw result data. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlWithdrawSuccessData { + /// The BOLT11 invoice that was submitted for withdrawal. + pub invoice: String, +} + +/// Error returned by an LNURL service endpoint. +#[derive(Clone, uniffi::Record)] +pub struct LnUrlErrorData { + pub reason: String, +} + +// ── Success action types (LUD-09 / LUD-10) ───────────────────────── + +/// A processed success action from an LNURL-pay callback. +/// +/// For Message and Url this is passed through as-is. For Aes the +/// ciphertext has been decrypted using the payment preimage. +#[derive(Clone, uniffi::Enum)] +pub enum SuccessActionProcessed { + /// Display a message to the user. + Message { message: String }, + /// Display a URL to the user. + Url { description: String, url: String }, + /// Decrypted AES payload (LUD-10). + Aes { description: String, plaintext: String }, +} + +// ── From conversions (gl-client → gl-sdk) ─────────────────────────── + +impl From for LnUrlPayRequestData { + fn from(r: wire::PayRequestResponse) -> Self { + Self { + description: r.description().unwrap_or_default(), + callback: r.callback, + min_sendable: r.min_sendable, + max_sendable: r.max_sendable, + metadata: r.metadata, + comment_allowed: r.comment_allowed.unwrap_or(0), + lnurl: String::new(), // caller sets this after conversion + } + } +} + +impl From for LnUrlWithdrawRequestData { + fn from(r: wire::WithdrawRequestResponse) -> Self { + Self { + callback: r.callback, + k1: r.k1, + default_description: r.default_description, + min_withdrawable: r.min_withdrawable, + max_withdrawable: r.max_withdrawable, + lnurl: String::new(), // caller sets this after conversion + } + } +} + +impl From for SuccessActionProcessed { + fn from(a: wire::ProcessedSuccessAction) -> Self { + match a { + wire::ProcessedSuccessAction::Message { message } => { + SuccessActionProcessed::Message { message } + } + wire::ProcessedSuccessAction::Url { description, url } => { + SuccessActionProcessed::Url { description, url } + } + wire::ProcessedSuccessAction::Aes { + description, + plaintext, + } => SuccessActionProcessed::Aes { + description, + plaintext, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pay_request_data_from_conversion() { + let wire_resp = wire::PayRequestResponse { + callback: "https://example.com/cb".to_string(), + max_sendable: 100000, + min_sendable: 1000, + tag: "payRequest".to_string(), + metadata: r#"[["text/plain", "Buy coffee"]]"#.to_string(), + comment_allowed: Some(140), + }; + + let data: LnUrlPayRequestData = wire_resp.into(); + assert_eq!(data.callback, "https://example.com/cb"); + assert_eq!(data.min_sendable, 1000); + assert_eq!(data.max_sendable, 100000); + assert_eq!(data.comment_allowed, 140); + assert_eq!(data.description, "Buy coffee"); + assert!(data.lnurl.is_empty()); // caller sets this + } + + #[test] + fn test_pay_request_data_no_comment_allowed() { + let wire_resp = wire::PayRequestResponse { + callback: "https://example.com/cb".to_string(), + max_sendable: 100000, + min_sendable: 1000, + tag: "payRequest".to_string(), + metadata: r#"[["text/plain", "test"]]"#.to_string(), + comment_allowed: None, + }; + + let data: LnUrlPayRequestData = wire_resp.into(); + assert_eq!(data.comment_allowed, 0); + } + + #[test] + fn test_withdraw_request_data_from_conversion() { + let wire_resp = wire::WithdrawRequestResponse { + tag: "withdrawRequest".to_string(), + callback: "https://example.com/withdraw".to_string(), + k1: "secret123".to_string(), + default_description: "Withdraw from service".to_string(), + min_withdrawable: 1000, + max_withdrawable: 50000, + }; + + let data: LnUrlWithdrawRequestData = wire_resp.into(); + assert_eq!(data.callback, "https://example.com/withdraw"); + assert_eq!(data.k1, "secret123"); + assert_eq!(data.default_description, "Withdraw from service"); + assert_eq!(data.min_withdrawable, 1000); + assert_eq!(data.max_withdrawable, 50000); + } + + #[test] + fn test_processed_success_action_from_message() { + let processed = wire::ProcessedSuccessAction::Message { + message: "Thanks!".to_string(), + }; + let sdk: SuccessActionProcessed = processed.into(); + match sdk { + SuccessActionProcessed::Message { message } => assert_eq!(message, "Thanks!"), + _ => panic!("Expected Message variant"), + } + } + + #[test] + fn test_processed_success_action_from_url() { + let processed = wire::ProcessedSuccessAction::Url { + description: "View order".to_string(), + url: "https://example.com/order".to_string(), + }; + let sdk: SuccessActionProcessed = processed.into(); + match sdk { + SuccessActionProcessed::Url { description, url } => { + assert_eq!(description, "View order"); + assert_eq!(url, "https://example.com/order"); + } + _ => panic!("Expected Url variant"), + } + } + + #[test] + fn test_processed_success_action_from_aes() { + let processed = wire::ProcessedSuccessAction::Aes { + description: "Your code".to_string(), + plaintext: "ABC-123".to_string(), + }; + let sdk: SuccessActionProcessed = processed.into(); + match sdk { + SuccessActionProcessed::Aes { + description, + plaintext, + } => { + assert_eq!(description, "Your code"); + assert_eq!(plaintext, "ABC-123"); + } + _ => panic!("Expected Aes variant"), + } + } +} diff --git a/libs/gl-sdk/src/node.rs b/libs/gl-sdk/src/node.rs index 2d9bc4497..bdd8637ba 100644 --- a/libs/gl-sdk/src/node.rs +++ b/libs/gl-sdk/src/node.rs @@ -1,6 +1,7 @@ use crate::{credentials::Credentials, signer::Handle, util::exec, Error}; use std::sync::atomic::{AtomicBool, Ordering}; use gl_client::credentials::NodeIdProvider; +use gl_client::lnurl::models::LnUrlHttpClient as _; use gl_client::node::{Client as GlClient, ClnClient, Node as ClientNode}; use gl_client::pb::{self as glpb, cln as clnpb}; use lightning_invoice::Bolt11Invoice; @@ -22,6 +23,7 @@ pub struct Node { /// events to the installed listener. A single listener per node; /// installing a new one aborts the previous task. Aborted on Drop. event_task: Mutex>>, + network: gl_client::bitcoin::Network, } impl Drop for Node { @@ -62,6 +64,7 @@ impl Node { signer_handle: None, disconnected: AtomicBool::new(false), event_task: Mutex::new(None), + network: gl_client::bitcoin::Network::Bitcoin, }) } } @@ -643,6 +646,160 @@ impl Node { node_state, ) } + + // ── LNURL methods ─────────────────────────────────────────── + + /// Execute an LNURL-pay flow (LUD-06). + /// + /// Sends the chosen amount (and optional comment) to the service's + /// callback, receives and validates a BOLT11 invoice, pays it, and + /// processes any success action (LUD-09/10). + /// + /// Call the top-level `parse_input` first to obtain the + /// `LnUrlPayRequestData`, then build an `LnUrlPayRequest` with the + /// user's chosen amount. + pub fn lnurl_pay( + &self, + request: crate::lnurl::LnUrlPayRequest, + ) -> Result { + self.check_connected()?; + validate_lnurl_pay_input(&request)?; + + let http_client = gl_client::lnurl::models::LnUrlHttpClearnetClient::new(); + + // Phase 1: Get invoice from service callback + let comment = request.comment.as_deref(); + let (invoice_str, success_action) = match exec( + gl_client::lnurl::pay::fetch_invoice( + &http_client, + &request.data.callback, + request.amount_msat, + comment, + ), + ) { + Ok(v) => v, + Err(e) => { + let msg = e.to_string(); + let reason = msg + .strip_prefix(gl_client::lnurl::pay::LNURL_SERVICE_ERROR_PREFIX) + .unwrap_or(&msg) + .to_string(); + return Ok(crate::lnurl::LnUrlPayResult::EndpointError { + data: crate::lnurl::LnUrlErrorData { reason }, + }); + } + }; + + if let Some(reason) = invoice_network_mismatch(&invoice_str, self.network) { + return Ok(crate::lnurl::LnUrlPayResult::EndpointError { + data: crate::lnurl::LnUrlErrorData { reason }, + }); + } + + // Phase 2: Pay the invoice + let mut cln_client = exec(self.get_cln_client())?.clone(); + let pay_response = match exec(cln_client.pay(clnpb::PayRequest { + bolt11: invoice_str.clone(), + ..Default::default() + })) { + Ok(r) => r.into_inner(), + Err(e) => { + let payment_hash = invoice_str + .parse::() + .ok() + .map(|inv| inv.payment_hash().to_string()) + .unwrap_or_default(); + return Ok(crate::lnurl::LnUrlPayResult::PayError { + data: crate::lnurl::LnUrlPayErrorData { + payment_hash, + reason: e.to_string(), + }, + }); + } + }; + + // Phase 3: Process success action if present + let validate_url = request.validate_success_action_url.unwrap_or(true); + let processed_action = match success_action { + Some(action) => { + let processed = action + .process(&pay_response.payment_preimage) + .map_err(|e| Error::Other(e.to_string()))?; + if validate_url { + if let gl_client::lnurl::models::ProcessedSuccessAction::Url { + url, .. + } = &processed + { + if let Some(reason) = + url_action_domain_mismatch(&request.data.callback, url) + { + return Err(Error::Other(reason)); + } + } + } + Some(processed.into()) + } + None => None, + }; + + Ok(crate::lnurl::LnUrlPayResult::EndpointSuccess { + data: crate::lnurl::LnUrlPaySuccessData { + payment_preimage: hex::encode(&pay_response.payment_preimage), + success_action: processed_action, + }, + }) + } + + /// Execute an LNURL-withdraw flow (LUD-03). + /// + /// Creates an invoice on this node for the requested amount, sends + /// it to the service's callback URL, and the service pays it + /// asynchronously. + /// + /// Call the top-level `parse_input` first to obtain the + /// `LnUrlWithdrawRequestData`, then build an `LnUrlWithdrawRequest` + /// with the user's chosen amount. + pub fn lnurl_withdraw( + &self, + request: crate::lnurl::LnUrlWithdrawRequest, + ) -> Result { + self.check_connected()?; + + let http_client = gl_client::lnurl::models::LnUrlHttpClearnetClient::new(); + + // Step 1: Create an invoice on our node + let description = request + .description + .unwrap_or(request.data.default_description.clone()); + + let invoice_response = self.receive( + format!("lnurl-withdraw-{}", request.data.k1), + description, + Some(request.amount_msat), + )?; + + // Step 2: Build callback URL and submit invoice to service + let callback_url = gl_client::lnurl::withdraw::build_withdraw_callback_url( + &request.data.callback, + &request.data.k1, + &invoice_response.bolt11, + ) + .map_err(|e| Error::Other(e.to_string()))?; + + // Step 3: Send invoice to service + match exec(http_client.send_invoice_for_withdraw_request(&callback_url)) { + Ok(_) => Ok(crate::lnurl::LnUrlWithdrawResult::Ok { + data: crate::lnurl::LnUrlWithdrawSuccessData { + invoice: invoice_response.bolt11, + }, + }), + Err(e) => Ok(crate::lnurl::LnUrlWithdrawResult::ErrorStatus { + data: crate::lnurl::LnUrlErrorData { + reason: e.to_string(), + }, + }), + } + } } fn render_section(result: Result) -> serde_json::Value { @@ -676,6 +833,83 @@ fn build_diagnostic_json( serde_json::to_string_pretty(&envelope).map_err(|e| Error::Other(e.to_string())) } +/// Returns a human-readable reason if the invoice's BOLT-11 currency +/// prefix does not match the node's configured network. +/// +/// Not a LUD-06 requirement; this is a wallet-side safety check that +/// prevents attempting to pay e.g. a testnet invoice from a mainnet +/// wallet. The payment would fail at the node layer regardless, but +/// this surfaces a clean error earlier. +fn invoice_network_mismatch( + invoice_str: &str, + node_network: gl_client::bitcoin::Network, +) -> Option { + use lightning_invoice::Currency; + let invoice = invoice_str.parse::().ok()?; + let expected = match node_network { + gl_client::bitcoin::Network::Bitcoin => Currency::Bitcoin, + gl_client::bitcoin::Network::Testnet => Currency::BitcoinTestnet, + gl_client::bitcoin::Network::Signet => Currency::Signet, + gl_client::bitcoin::Network::Regtest => Currency::Regtest, + _ => return None, + }; + if invoice.currency() == expected { + None + } else { + Some(format!( + "invoice is for {:?}, but this node is on {:?}", + invoice.currency(), + node_network + )) + } +} + +fn url_action_domain_mismatch(callback_url: &str, action_url: &str) -> Option { + let cb = url::Url::parse(callback_url).ok()?; + let action = url::Url::parse(action_url).ok()?; + let cb_domain = cb.domain()?; + let action_domain = action.domain()?; + if cb_domain == action_domain { + None + } else { + Some(format!( + "success action URL domain ({}) does not match the callback domain ({})", + action_domain, cb_domain + )) + } +} + +fn validate_lnurl_pay_input(request: &crate::lnurl::LnUrlPayRequest) -> Result<(), Error> { + let data = &request.data; + if request.amount_msat < data.min_sendable { + return Err(Error::Other(format!( + "amount_msat {} is below the service's min_sendable ({})", + request.amount_msat, data.min_sendable + ))); + } + if request.amount_msat > data.max_sendable { + return Err(Error::Other(format!( + "amount_msat {} is above the service's max_sendable ({})", + request.amount_msat, data.max_sendable + ))); + } + if let Some(comment) = request.comment.as_deref() { + if data.comment_allowed == 0 && !comment.is_empty() { + return Err(Error::Other( + "this LNURL service does not accept comments".to_string(), + )); + } + if (comment.len() as u64) > data.comment_allowed { + return Err(Error::Other(format!( + "comment length {} exceeds the service's comment_allowed ({})", + comment.len(), + data.comment_allowed + ))); + } + } + Ok(()) +} + // Not exported through uniffi impl Node { /// Install a listener that receives real-time node events. @@ -738,6 +972,7 @@ impl Node { pub(crate) fn with_signer( credentials: Credentials, handle: Handle, + network: gl_client::bitcoin::Network, ) -> Result { let node_id = credentials .inner @@ -756,6 +991,7 @@ impl Node { signer_handle: Some(handle), disconnected: AtomicBool::new(false), event_task: Mutex::new(None), + network, }) } diff --git a/libs/gl-sdk/tests/test_lnurl.py b/libs/gl-sdk/tests/test_lnurl.py new file mode 100644 index 000000000..acd526b3c --- /dev/null +++ b/libs/gl-sdk/tests/test_lnurl.py @@ -0,0 +1,256 @@ +"""End-to-end integration tests for LNURL flows. + +These tests spin up real CLN nodes, a CLN-backed LNURL server, and a +Greenlight SDK node, then exercise the full LNURL-pay and +LNURL-withdraw protocols. + +Network topology: + gl_sdk_node ── channel ── relay ── channel ── service_node (LNURL server) +""" + +import asyncio + +from gltesting.fixtures import * # noqa: F401, F403 +from pyln.testing.utils import wait_for + +import glsdk + + +MNEMONIC = ( + "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about" +) + +CHANNEL_SATS = 1_000_000 # 1M sats + + +def make_sdk_node(nobody_id, scheduler): + """Register a GL node via the SDK and return it with signer running.""" + dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) + config = glsdk.Config().with_developer_cert(dev_cert) + node = glsdk.register(MNEMONIC, None, config) + return node, config + + +def fund_and_connect(node_factory, bitcoind, lnurl_service): + """Create a relay node with channels to the LNURL service node. + + Returns the relay node, already funded and with a NORMAL channel to + the service. + """ + relay = node_factory.get_node(options={"disable-plugin": "cln-grpc"}) + service_node = lnurl_service.cln_node + service_id = service_node.info["id"] + + # Connect relay <-> service + relay.rpc.connect(service_id, "127.0.0.1", service_node.daemon.port) + + # Fund relay + addr = relay.rpc.newaddr()["bech32"] + bitcoind.rpc.sendtoaddress(addr, 1) + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: len(relay.rpc.listfunds()["outputs"]) > 0) + + # Open channel relay -> service + relay.rpc.fundchannel(service_id, CHANNEL_SATS) + bitcoind.generate_block(6, wait_for_mempool=1) + wait_for( + lambda: any( + ch["state"] == "CHANNELD_NORMAL" + for ch in relay.rpc.listpeerchannels(service_id)["channels"] + ) + ) + + return relay + + +def test_parse_input_lnurl_pay(lnurl_service): + """parse_input on an LNURL-pay URL returns LnUrlPay with fetched data.""" + resolved = asyncio.run(glsdk.resolve_input(lnurl_service.pay_url)) + + assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) + data = resolved.data + assert data.min_sendable == lnurl_service.min_sendable + assert data.max_sendable == lnurl_service.max_sendable + assert len(data.description) > 0 + assert data.callback.startswith(lnurl_service.base_url) + assert data.lnurl == lnurl_service.pay_url + + +def test_parse_input_lnurl_withdraw(lnurl_service): + """parse_input on an LNURL-withdraw URL returns LnUrlWithdraw with fetched data.""" + resolved = asyncio.run(glsdk.resolve_input(lnurl_service.withdraw_url)) + + assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_WITHDRAW) + data = resolved.data + assert data.min_withdrawable == lnurl_service.min_withdrawable + assert data.max_withdrawable == lnurl_service.max_withdrawable + assert len(data.k1) > 0 + assert data.lnurl == lnurl_service.withdraw_url + + +def test_parse_input_lightning_address_url(lnurl_service): + """parse_input on a well-known LUD-16 URL returns LnUrlPay.""" + resolved = asyncio.run(glsdk.resolve_input(lnurl_service.lightning_address_url)) + + assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) + assert resolved.data.min_sendable == lnurl_service.min_sendable + assert resolved.data.lnurl == lnurl_service.lightning_address_url + + +def test_parse_input_bolt11_no_http(lnurl_service): + """parse_input on a BOLT11 invoice returns Bolt11 without touching HTTP.""" + invoice = ( + "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45t" + "qcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tn" + "k2zunwj5jalqtyxqulh0l5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw" + "3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993h5pcm4dxzpvttgza8zhkqxpgffc" + "rf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqclj9424x9m8" + "h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrz" + "jqwldmj9dha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqq" + "yqqqqlgqqqqjcqq9qrzjqf9e58aguqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmu" + "wvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphmsywntrrhqjcraumvc4y6r8" + "v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0vp62" + "g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqq" + "qqqzsqygarl9fh38s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5" + "m7ke54n2d5sym4xcmxtl8238xxvw5h5h5j5r6drg6k6zcqj0fcwg" + ) + resolved = asyncio.run(glsdk.resolve_input(invoice)) + + assert isinstance(resolved, glsdk.ResolvedInput.BOLT11) + assert resolved.invoice.amount_msat == 10 + # No callback recorded on the LNURL service since we never hit it. + assert len(lnurl_service.pay_callbacks) == 0 + assert len(lnurl_service.withdraw_callbacks) == 0 + + +def test_lnurl_pay_end_to_end( + scheduler, nobody_id, clients, node_factory, bitcoind, lnurl_service +): + """Full LNURL-pay flow: resolve → pay → verify. + + Uses a GL SDK node with outbound liquidity to pay an LNURL service. + """ + # Use the low-level client to set up channels, since the SDK node + # doesn't expose connect_peer / fund_channel directly. + relay = fund_and_connect(node_factory, bitcoind, lnurl_service) + + c = clients.new() + c.register(configure=True) + gl1 = c.node() + s = c.signer().run_in_thread() + + relay_id = relay.info["id"] + + # Connect GL node to relay and open channel + gl1.connect_peer(relay_id, f"127.0.0.1:{relay.daemon.port}") + gl_addr = gl1.new_address().bech32 + bitcoind.rpc.sendtoaddress(gl_addr, 0.5) + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: len(gl1.list_funds().outputs) > 0) + + from pyln import grpc as clnpb + + gl1.fund_channel( + bytes.fromhex(relay_id), + clnpb.AmountOrAll(amount=clnpb.Amount(msat=CHANNEL_SATS * 1000)), + ) + bitcoind.generate_block(6, wait_for_mempool=1) + wait_for( + lambda: any( + ch.state == 2 # CHANNELD_NORMAL + for ch in gl1.list_peer_channels().channels + ) + ) + + # Now build an SDK-level Node for LNURL operations + creds_bytes = c.creds().to_bytes() + sdk_node = glsdk.Node(glsdk.Credentials.load(creds_bytes)) + + try: + # Resolve + resolved = asyncio.run(glsdk.resolve_input(lnurl_service.pay_url)) + assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) + pay_data = resolved.data + + amount_msat = 50_000 # 50 sats + + # Pay + result = sdk_node.lnurl_pay( + glsdk.LnUrlPayRequest( + data=pay_data, + amount_msat=amount_msat, + comment=None, + ) + ) + + assert isinstance(result, glsdk.LnUrlPayResult.ENDPOINT_SUCCESS) + assert len(result.data.payment_preimage) == 64 # hex-encoded 32 bytes + + # Verify the LNURL server saw the callback + assert len(lnurl_service.pay_callbacks) == 1 + assert lnurl_service.pay_callbacks[0]["amount_msat"] == amount_msat + finally: + sdk_node.disconnect() + + +def test_lnurl_pay_with_message_success_action( + scheduler, nobody_id, clients, node_factory, bitcoind, lnurl_service +): + """LNURL-pay with a message-type success action (LUD-09).""" + lnurl_service.success_action = { + "tag": "message", + "message": "Thank you for your payment!", + } + + relay = fund_and_connect(node_factory, bitcoind, lnurl_service) + + c = clients.new() + c.register(configure=True) + gl1 = c.node() + s = c.signer().run_in_thread() + + relay_id = relay.info["id"] + gl1.connect_peer(relay_id, f"127.0.0.1:{relay.daemon.port}") + gl_addr = gl1.new_address().bech32 + bitcoind.rpc.sendtoaddress(gl_addr, 0.5) + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: len(gl1.list_funds().outputs) > 0) + + from pyln import grpc as clnpb + + gl1.fund_channel( + bytes.fromhex(relay_id), + clnpb.AmountOrAll(amount=clnpb.Amount(msat=CHANNEL_SATS * 1000)), + ) + bitcoind.generate_block(6, wait_for_mempool=1) + wait_for( + lambda: any( + ch.state == 2 # CHANNELD_NORMAL + for ch in gl1.list_peer_channels().channels + ) + ) + + creds_bytes = c.creds().to_bytes() + sdk_node = glsdk.Node(glsdk.Credentials.load(creds_bytes)) + + try: + resolved = asyncio.run(glsdk.resolve_input(lnurl_service.pay_url)) + assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) + pay_data = resolved.data + + result = sdk_node.lnurl_pay( + glsdk.LnUrlPayRequest( + data=pay_data, + amount_msat=50_000, + comment=None, + ) + ) + + assert isinstance(result, glsdk.LnUrlPayResult.ENDPOINT_SUCCESS) + sa = result.data.success_action + assert sa is not None + assert isinstance(sa, glsdk.SuccessActionProcessed.MESSAGE) + assert sa.message == "Thank you for your payment!" + finally: + sdk_node.disconnect() diff --git a/libs/gl-sdk/tests/test_parse_input.py b/libs/gl-sdk/tests/test_parse_input.py index 30089b8d3..46fd806c8 100644 --- a/libs/gl-sdk/tests/test_parse_input.py +++ b/libs/gl-sdk/tests/test_parse_input.py @@ -1,6 +1,9 @@ -"""Tests for the parse_input() free function. +"""Tests for the synchronous parse_input() free function. -Verifies BOLT11 invoice and node ID parsing from arbitrary string input. +`parse_input` is offline — no HTTP, no I/O. Tests cover BOLT11 +invoices, node IDs, and the offline LNURL / Lightning Address +classification (not the HTTP resolution — that's `resolve_input`, +exercised in test_lnurl.py against the live LNURL fixture). """ import pytest @@ -18,12 +21,20 @@ # Valid compressed secp256k1 public key (starts with 02 or 03, 33 bytes = 66 hex chars) VALID_NODE_ID = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" +# Bech32-encoded "https://service.com/lnurl" (LUD-01 example) +VALID_LNURL_BECH32 = ( + "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2" +) + class TestParseInputTypes: """Test that parse_input types exist in the bindings.""" - def test_input_type_enum_exists(self): - assert hasattr(glsdk, "InputType") + def test_parsed_input_enum_exists(self): + assert hasattr(glsdk, "ParsedInput") + + def test_resolved_input_enum_exists(self): + assert hasattr(glsdk, "ResolvedInput") def test_bolt11_invoice_type_exists(self): assert hasattr(glsdk, "ParsedInvoice") @@ -31,24 +42,23 @@ def test_bolt11_invoice_type_exists(self): def test_parse_input_function_exists(self): assert hasattr(glsdk, "parse_input") + def test_resolve_input_function_exists(self): + assert hasattr(glsdk, "resolve_input") + class TestParseInputNodeId: - """Test node ID parsing.""" + """Test node ID parsing — no HTTP required.""" def test_parse_valid_node_id(self): result = glsdk.parse_input(VALID_NODE_ID) - assert isinstance(result, glsdk.InputType) - - def test_parse_node_id_returns_correct_value(self): - result = glsdk.parse_input(VALID_NODE_ID) - # Access the NodeId variant - assert result.is_node_id() if hasattr(result, 'is_node_id') else True - # UniFFI enums in Python: check the variant - assert hasattr(result, 'node_id') or hasattr(result, 'invoice') + assert isinstance(result, glsdk.ParsedInput.NODE_ID) + assert result.node_id == VALID_NODE_ID def test_invalid_hex_returns_error(self): with pytest.raises(glsdk.Error): - glsdk.parse_input("not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx") + glsdk.parse_input( + "not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx" + ) def test_wrong_length_hex_returns_error(self): with pytest.raises(glsdk.Error): @@ -57,31 +67,60 @@ def test_wrong_length_hex_returns_error(self): def test_wrong_prefix_hex_returns_error(self): # 04 prefix = uncompressed pubkey, not valid for Lightning with pytest.raises(glsdk.Error): - glsdk.parse_input("04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619") + glsdk.parse_input( + "04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" + ) class TestParseInputBolt11: - """Test BOLT11 invoice parsing.""" + """Test BOLT11 invoice parsing — no HTTP required.""" def test_parse_valid_bolt11(self): result = glsdk.parse_input(BOLT11_INVOICE) - assert isinstance(result, glsdk.InputType) + assert isinstance(result, glsdk.ParsedInput.BOLT11) def test_parse_bolt11_with_lightning_prefix(self): result = glsdk.parse_input("lightning:" + BOLT11_INVOICE) - assert isinstance(result, glsdk.InputType) + assert isinstance(result, glsdk.ParsedInput.BOLT11) def test_parse_bolt11_with_uppercase_prefix(self): result = glsdk.parse_input("LIGHTNING:" + BOLT11_INVOICE) - assert isinstance(result, glsdk.InputType) + assert isinstance(result, glsdk.ParsedInput.BOLT11) def test_parse_bolt11_with_whitespace(self): result = glsdk.parse_input(" " + BOLT11_INVOICE + " ") - assert isinstance(result, glsdk.InputType) + assert isinstance(result, glsdk.ParsedInput.BOLT11) + + +class TestParseInputLnUrl: + """Test LNURL bech32 / Lightning Address classification — offline.""" + + def test_lnurl_bech32_decodes_to_url(self): + result = glsdk.parse_input(VALID_LNURL_BECH32) + assert isinstance(result, glsdk.ParsedInput.LN_URL) + assert result.url.startswith("https://") + + def test_lnurl_bech32_lowercase(self): + result = glsdk.parse_input(VALID_LNURL_BECH32.lower()) + assert isinstance(result, glsdk.ParsedInput.LN_URL) + + def test_lnurl_with_lightning_prefix(self): + result = glsdk.parse_input("lightning:" + VALID_LNURL_BECH32) + assert isinstance(result, glsdk.ParsedInput.LN_URL) + + def test_lightning_address_returns_address_form(self): + result = glsdk.parse_input("user@example.com") + assert isinstance(result, glsdk.ParsedInput.LN_URL_ADDRESS) + assert result.address == "user@example.com" + + def test_lightning_address_with_symbols(self): + # LUD-16 allows a-z0-9-_. + result = glsdk.parse_input("sat.oshi-99@example.com") + assert isinstance(result, glsdk.ParsedInput.LN_URL_ADDRESS) class TestParseInputErrors: - """Test error cases.""" + """Test error cases that don't require HTTP.""" def test_empty_string_returns_error(self): with pytest.raises(glsdk.Error): @@ -99,3 +138,17 @@ def test_bitcoin_address_returns_error(self): # We don't support bitcoin addresses yet with pytest.raises(glsdk.Error): glsdk.parse_input("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + + def test_invalid_lnurl_bech32_errors(self): + with pytest.raises(glsdk.Error): + glsdk.parse_input("LNURL1INVALIDDATA") + + def test_lightning_address_no_dot_in_domain_errors(self): + with pytest.raises(glsdk.Error): + glsdk.parse_input("user@localhost") + + def test_lightning_address_empty_parts_error(self): + with pytest.raises(glsdk.Error): + glsdk.parse_input("@example.com") + with pytest.raises(glsdk.Error): + glsdk.parse_input("user@") diff --git a/libs/gl-testing/gltesting/fixtures.py b/libs/gl-testing/gltesting/fixtures.py index bf7f62588..655848d2e 100644 --- a/libs/gl-testing/gltesting/fixtures.py +++ b/libs/gl-testing/gltesting/fixtures.py @@ -25,6 +25,7 @@ from pyln.testing.fixtures import directory as str_directory from decimal import Decimal from gltesting.grpcweb import GrpcWebProxy, NodeHandler +from gltesting.lnurl_server import LnurlServer from clnvm import ClnVersionManager @@ -235,6 +236,26 @@ def node_grpc_web_proxy(scheduler): p.stop() +@pytest.fixture +def lnurl_service(node_factory): + """A CLN-backed LNURL service. + + Spins up a dedicated CLN node and an HTTP server that exposes + LNURL-pay, LNURL-withdraw and Lightning Address endpoints backed + by that node. Tests can then open channels to/from this node and + exercise the full LNURL flow end-to-end. + + Returns an `LnurlServer` instance. Use `.cln_rpc` to access the + backing CLN node's RPC; `.pay_url`, `.withdraw_url` and + `.lightning_address` to get the endpoints. + """ + cln_node = node_factory.get_node(options={"disable-plugin": "cln-grpc"}) + server = LnurlServer(cln_node) + server.start() + yield server + server.stop() + + @pytest.fixture def lsps_server(node_factory): """Provision and start an LSPs server.""" diff --git a/libs/gl-testing/gltesting/lnurl_server.py b/libs/gl-testing/gltesting/lnurl_server.py new file mode 100644 index 000000000..e95366bd1 --- /dev/null +++ b/libs/gl-testing/gltesting/lnurl_server.py @@ -0,0 +1,277 @@ +# A CLN-backed LNURL server for integration testing. +# +# Implements enough of LUD-01, LUD-03, LUD-06, LUD-09 and LUD-16 to +# exercise gl-client and gl-sdk LNURL flows end-to-end. Backed by a +# real CLN node (via pyln.client) so invoices are real BOLT11s that +# the system-under-test can actually pay / receive. + +from ephemeral_port_reserve import reserve +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread +from urllib.parse import urlparse, parse_qs +import hashlib +import json +import logging +import secrets + + +class LnurlServer: + """HTTP server that exposes LNURL-pay, LNURL-withdraw and Lightning + Address endpoints backed by a CLN node. + + Routes: + GET /lnurlp → LUD-06 payRequest response + GET /lnurlp/callback?amount=&comment= + → BOLT11 invoice + optional successAction + GET /.well-known/lnurlp/{username} → LUD-16 (same payRequest) + GET /lnurlw → LUD-03 withdrawRequest response + GET /lnurlw/callback?k1=&pr= → service pays the invoice + """ + + def __init__( + self, + cln_node, + *, + domain: str = "127.0.0.1", + username: str = "alice", + min_sendable: int = 1_000, + max_sendable: int = 100_000_000, + min_withdrawable: int = 1_000, + max_withdrawable: int = 100_000_000, + comment_allowed: int = 0, + success_action: dict | None = None, + ): + self.logger = logging.getLogger("gltesting.lnurl_server") + self.cln_node = cln_node + self.cln_rpc = cln_node.rpc + self.domain = domain + self.username = username + self.min_sendable = min_sendable + self.max_sendable = max_sendable + self.min_withdrawable = min_withdrawable + self.max_withdrawable = max_withdrawable + self.comment_allowed = comment_allowed + self.success_action = success_action + + self.port = reserve() + self._thread: Thread | None = None + self._httpd: ThreadingHTTPServer | None = None + + # Metadata for the pay request (LUD-06 mandates text/plain, + # LUD-16 requires a text/identifier entry for lightning addresses). + # We include the port in the identifier because the test domain is + # localhost-based and the lightning address includes the port. + self.metadata = json.dumps( + [ + ["text/plain", f"Pay to {username}"], + ["text/identifier", f"{username}@{domain}:{self.port}"], + ] + ) + + # Each withdraw session issues a fresh k1 and remembers it until consumed + self._pending_withdrawals: dict[str, dict] = {} + + # Logs of all incoming callback requests — tests inspect these + self.pay_callbacks: list[dict] = [] + self.withdraw_callbacks: list[dict] = [] + + # ── URLs ────────────────────────────────────────────────── + + @property + def base_url(self) -> str: + return f"http://{self.domain}:{self.port}" + + @property + def pay_url(self) -> str: + return f"{self.base_url}/lnurlp" + + @property + def lightning_address(self) -> str: + return f"{self.username}@{self.domain}:{self.port}" + + @property + def lightning_address_url(self) -> str: + return f"{self.base_url}/.well-known/lnurlp/{self.username}" + + @property + def withdraw_url(self) -> str: + return f"{self.base_url}/lnurlw" + + # ── Lifecycle ──────────────────────────────────────────── + + def start(self): + server_address = ("127.0.0.1", self.port) + handler_cls = _handler_factory(self) + self._httpd = ThreadingHTTPServer(server_address, handler_cls) + self._thread = Thread(target=self._httpd.serve_forever, daemon=True) + self._thread.start() + self.logger.info(f"LnurlServer running on {self.base_url}") + + def stop(self): + if self._httpd is not None: + self._httpd.shutdown() + self._httpd.server_close() + if self._thread is not None: + self._thread.join() + self.logger.info("LnurlServer stopped") + + # ── Handler callbacks (invoked from the HTTP thread) ───── + + def build_pay_response(self, callback_path: str) -> dict: + return { + "tag": "payRequest", + "callback": f"{self.base_url}{callback_path}", + "minSendable": self.min_sendable, + "maxSendable": self.max_sendable, + "metadata": self.metadata, + "commentAllowed": self.comment_allowed, + } + + def handle_pay_callback(self, amount_msat: int, comment: str | None) -> dict: + """Generate an invoice on the CLN backend for the requested amount. + + The description is set to the raw metadata string so the client's + BOLT11 description-hash check passes (as mandated by LUD-06). + """ + self.pay_callbacks.append({"amount_msat": amount_msat, "comment": comment}) + + # CLN requires a unique label per invoice + label = f"lnurl-pay-{secrets.token_hex(8)}" + + # LUD-06: the BOLT11 description hash must equal SHA256(metadata). + # pyln's `invoice` accepts `description` as a string; when CLN + # encodes the invoice it hashes that string into the description + # hash field, so passing our raw metadata JSON matches what the + # client re-computes. + invoice = self.cln_rpc.invoice( + amount_msat=amount_msat, + label=label, + description=self.metadata, + deschashonly=True, + ) + + response = { + "pr": invoice["bolt11"], + "routes": [], + } + if self.success_action is not None: + response["successAction"] = self.success_action + return response + + def build_withdraw_response(self, callback_path: str) -> dict: + k1 = secrets.token_hex(16) + self._pending_withdrawals[k1] = {"used": False} + return { + "tag": "withdrawRequest", + "callback": f"{self.base_url}{callback_path}", + "k1": k1, + "defaultDescription": f"Withdraw from {self.domain}", + "minWithdrawable": self.min_withdrawable, + "maxWithdrawable": self.max_withdrawable, + } + + def handle_withdraw_callback(self, k1: str, invoice: str) -> dict: + """Pay the supplied BOLT11 invoice from the CLN backend.""" + self.withdraw_callbacks.append({"k1": k1, "pr": invoice}) + + session = self._pending_withdrawals.get(k1) + if session is None: + return {"status": "ERROR", "reason": f"unknown k1: {k1}"} + if session["used"]: + return {"status": "ERROR", "reason": "k1 already used"} + session["used"] = True + + try: + self.cln_rpc.pay(invoice) + except Exception as e: + return {"status": "ERROR", "reason": f"pay failed: {e}"} + + return {"status": "OK"} + + +def _handler_factory(server: LnurlServer): + """Build a BaseHTTPRequestHandler class bound to a specific server. + + Using a closure avoids squirreling state onto the ThreadingHTTPServer + itself (as grpcweb.py does) and keeps the handler readable. + """ + + class _Handler(BaseHTTPRequestHandler): + def log_message(self, format, *args): + server.logger.debug("%s - - %s" % (self.address_string(), format % args)) + + def _reply_json(self, code: int, payload: dict): + body = json.dumps(payload).encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + parsed = urlparse(self.path) + path = parsed.path + query = parse_qs(parsed.query) + + try: + if path == "/lnurlp": + self._reply_json(200, server.build_pay_response("/lnurlp/callback")) + return + + if path == f"/.well-known/lnurlp/{server.username}": + # LUD-16: use a different callback path so tests can + # distinguish address vs raw-lnurl code paths if they + # want to. + self._reply_json( + 200, + server.build_pay_response( + f"/.well-known/lnurlp/{server.username}/callback" + ), + ) + return + + if path in ("/lnurlp/callback", f"/.well-known/lnurlp/{server.username}/callback"): + amount = query.get("amount", [None])[0] + if amount is None: + self._reply_json( + 200, + {"status": "ERROR", "reason": "missing amount"}, + ) + return + comment = query.get("comment", [None])[0] + self._reply_json( + 200, + server.handle_pay_callback(int(amount), comment), + ) + return + + if path == "/lnurlw": + self._reply_json( + 200, server.build_withdraw_response("/lnurlw/callback") + ) + return + + if path == "/lnurlw/callback": + k1 = query.get("k1", [None])[0] + pr = query.get("pr", [None])[0] + if not k1 or not pr: + self._reply_json( + 200, {"status": "ERROR", "reason": "missing k1 or pr"} + ) + return + self._reply_json(200, server.handle_withdraw_callback(k1, pr)) + return + + self.send_response(404) + self.end_headers() + except Exception as e: + server.logger.exception("Unhandled error in LnurlServer handler") + self._reply_json(500, {"status": "ERROR", "reason": str(e)}) + + return _Handler + + +def metadata_sha256(metadata: str) -> str: + """Helper for tests that want to assert on description-hash matching.""" + return hashlib.sha256(metadata.encode("utf-8")).hexdigest() diff --git a/libs/gl-testing/tests/test_lnurl_server.py b/libs/gl-testing/tests/test_lnurl_server.py new file mode 100644 index 000000000..e9f9424e8 --- /dev/null +++ b/libs/gl-testing/tests/test_lnurl_server.py @@ -0,0 +1,92 @@ +"""Tests for the CLN-backed LNURL mock server. + +These tests verify the server itself works correctly (invoice generation, +callback handling, etc.) before we layer in gl-client/gl-sdk tests that +depend on it. +""" + +from gltesting.fixtures import * # noqa: F401, F403 +from gltesting.lnurl_server import metadata_sha256 + +import httpx +import json + + +def test_pay_request_response(lnurl_service): + """GET /lnurlp returns a valid payRequest response.""" + r = httpx.get(lnurl_service.pay_url) + assert r.status_code == 200 + body = r.json() + + assert body["tag"] == "payRequest" + assert body["callback"].startswith(lnurl_service.base_url) + assert body["minSendable"] == lnurl_service.min_sendable + assert body["maxSendable"] == lnurl_service.max_sendable + # metadata must parse as a JSON array of [mime, content] pairs + meta = json.loads(body["metadata"]) + assert any(entry[0] == "text/plain" for entry in meta) + + +def test_lightning_address_endpoint(lnurl_service): + """LUD-16: GET /.well-known/lnurlp/{user} returns a payRequest.""" + r = httpx.get(lnurl_service.lightning_address_url) + assert r.status_code == 200 + body = r.json() + assert body["tag"] == "payRequest" + # Metadata must include a text/identifier entry for LUD-16 + meta = json.loads(body["metadata"]) + assert any( + entry[0] == "text/identifier" and entry[1] == f"{lnurl_service.username}@{lnurl_service.domain}:{lnurl_service.port}" + for entry in meta + ) + + +def test_pay_callback_returns_valid_invoice(lnurl_service): + """GET /lnurlp/callback?amount=X returns a BOLT11 with the right + description hash and amount.""" + # Fetch the pay request to get the callback URL + pay_req = httpx.get(lnurl_service.pay_url).json() + amount_msat = 10_000 + + # Call the callback + r = httpx.get(pay_req["callback"], params={"amount": amount_msat}) + assert r.status_code == 200 + body = r.json() + assert "pr" in body + + # Decode the BOLT11 using the backing CLN node + decoded = lnurl_service.cln_rpc.decodepay(body["pr"]) + assert decoded["amount_msat"] == amount_msat + # description hash must match SHA256(metadata) + expected_hash = metadata_sha256(pay_req["metadata"]) + assert decoded["description_hash"] == expected_hash + + +def test_pay_callback_tracked_in_callbacks_list(lnurl_service): + """The server records every callback invocation for test inspection.""" + pay_req = httpx.get(lnurl_service.pay_url).json() + httpx.get(pay_req["callback"], params={"amount": 5_000, "comment": "hello"}) + + assert len(lnurl_service.pay_callbacks) == 1 + assert lnurl_service.pay_callbacks[0]["amount_msat"] == 5_000 + assert lnurl_service.pay_callbacks[0]["comment"] == "hello" + + +def test_withdraw_request_response(lnurl_service): + """GET /lnurlw returns a valid withdrawRequest response with a fresh k1.""" + r = httpx.get(lnurl_service.withdraw_url) + assert r.status_code == 200 + body = r.json() + + assert body["tag"] == "withdrawRequest" + assert body["callback"].startswith(lnurl_service.base_url) + assert len(body["k1"]) > 0 + assert body["minWithdrawable"] == lnurl_service.min_withdrawable + assert body["maxWithdrawable"] == lnurl_service.max_withdrawable + + +def test_withdraw_issues_distinct_k1s(lnurl_service): + """Each call to /lnurlw returns a fresh k1.""" + r1 = httpx.get(lnurl_service.withdraw_url).json() + r2 = httpx.get(lnurl_service.withdraw_url).json() + assert r1["k1"] != r2["k1"]