diff --git a/Cargo.lock b/Cargo.lock index bc5fcc7a0..2350cc829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,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" @@ -4821,6 +4834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" dependencies = [ "anyhow", + "async-compat", "bytes", "once_cell", "static_assertions", 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 index d3384c891..fb59a2bbb 100644 --- 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 @@ -1,80 +1,23 @@ -// Instrumented tests for parse_input() LNURL handling. -// Covers LNURL bech32 strings, lightning addresses, prefix handling, -// and error cases. Pure parsing only — no node, no network. +// 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.Assert.* import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LnurlParseTest { - // LNURL bech32 encoding of https://service.com/lnurl (LUD-01 example). - private val lnurlBech32 = - "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2" - - // ============================================================ - // LNURL bech32 parsing - // ============================================================ - - @Test - fun parse_lnurl_bech32_uppercase() { - val result = parseInput(lnurlBech32) - assertTrue( - "Expected LnUrl, got $result", - result is InputType.LnUrl, - ) - } - - @Test - fun parse_lnurl_bech32_lowercase() { - val result = parseInput(lnurlBech32.lowercase()) - assertTrue( - "Expected LnUrl, got $result", - result is InputType.LnUrl, - ) - } - - @Test - fun parse_lnurl_with_lightning_prefix() { - val result = parseInput("lightning:$lnurlBech32") - assertTrue( - "Expected LnUrl, got $result", - result is InputType.LnUrl, - ) - } - @Test(expected = Exception::class) fun parse_invalid_lnurl_bech32_returns_error() { parseInput("LNURL1INVALIDDATA") } - // ============================================================ - // Lightning address parsing - // ============================================================ - - @Test - fun parse_lightning_address_simple() { - val result = parseInput("user@example.com") - assertTrue( - "Expected LnUrlAddress, got $result", - result is InputType.LnUrlAddress, - ) - assertEquals("user@example.com", (result as InputType.LnUrlAddress).address) - } - - @Test - fun parse_lightning_address_with_symbols() { - val result = parseInput("sat.oshi-99@example.com") - assertTrue( - "Expected LnUrlAddress, got $result", - result is InputType.LnUrlAddress, - ) - } - @Test(expected = Exception::class) fun parse_lightning_address_no_dot_in_domain_returns_error() { parseInput("user@localhost") @@ -89,4 +32,4 @@ class LnurlParseTest { fun parse_lightning_address_empty_domain_returns_error() { parseInput("user@") } -} \ No newline at end of file +} 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 73befa987..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,58 @@ 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 // ============================================================================ @@ -275,17 +330,6 @@ pub struct LnUrlWithdrawRequestData { pub lnurl: String, } -/// Result of resolving an LNURL. Discriminated by `type` field. -#[napi(object)] -pub struct ResolvedLnUrl { - /// "pay" or "withdraw" - pub r#type: String, - /// Present when type == "pay" - pub pay: Option, - /// Present when type == "withdraw" - pub withdraw: Option, -} - #[napi(object)] pub struct LnUrlPayRequest { pub data: LnUrlPayRequestData, @@ -975,28 +1019,10 @@ impl Node { // ── LNURL methods ─────────────────────────────────────────── - /// Resolve an LNURL or Lightning Address to its endpoint data. - /// - /// Accepts an LNURL bech32 string, a decoded URL, or a Lightning - /// Address (user@domain). - #[napi] - pub async fn resolve_lnurl(&self, input: String) -> Result { - let inner = self.inner.clone(); - let resolved = tokio::task::spawn_blocking(move || { - inner - .resolve_lnurl(input) - .map_err(|e| Error::from_reason(e.to_string())) - }) - .await - .map_err(|e| Error::from_reason(e.to_string()))??; - - Ok(napi_resolved_lnurl_from_gl(resolved)) - } - /// Execute an LNURL-pay flow. /// - /// Call `resolve_lnurl()` first, then pass the pay data with a - /// chosen amount. + /// 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(); @@ -1014,8 +1040,8 @@ impl Node { /// Execute an LNURL-withdraw flow. /// - /// Call `resolve_lnurl()` first, then pass the withdraw data with - /// a chosen amount. + /// 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(); @@ -1081,9 +1107,21 @@ fn output_status_to_string(status: &GlOutputStatus) -> String { } // ============================================================================ -// LNURL Conversion Helpers +// 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, @@ -1109,21 +1147,103 @@ fn napi_withdraw_request_data_from_gl( } } -fn napi_resolved_lnurl_from_gl(resolved: glsdk::ResolvedLnUrl) -> ResolvedLnUrl { - match resolved { - glsdk::ResolvedLnUrl::Pay { data } => ResolvedLnUrl { - r#type: "pay".to_string(), - pay: Some(napi_pay_request_data_from_gl(data)), - withdraw: None, +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, }, - glsdk::ResolvedLnUrl::Withdraw { data } => ResolvedLnUrl { - r#type: "withdraw".to_string(), - pay: None, - withdraw: Some(napi_withdraw_request_data_from_gl(data)), + 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, 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/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 81f7e306b..03b908c2a 100644 --- a/libs/gl-sdk/Cargo.toml +++ b/libs/gl-sdk/Cargo.toml @@ -24,7 +24,7 @@ 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] diff --git a/libs/gl-sdk/glsdk/glsdk.py b/libs/gl-sdk/glsdk/glsdk.py index 44694fe2d..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,9 +507,9 @@ 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() != 2068: + 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() != 36503: + 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") @@ -508,24 +519,12 @@ 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_receive() != 39761: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_node_resolve_lnurl() != 5774: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_send() != 4348: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_stop() != 20186: 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: @@ -546,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") @@ -556,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. @@ -667,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), @@ -772,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), @@ -873,12 +867,6 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_node_receive.restype = _UniffiRustBuffer -_UniffiLib.uniffi_glsdk_fn_method_node_resolve_lnurl.argtypes = ( - ctypes.c_void_p, - _UniffiRustBuffer, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_method_node_resolve_lnurl.restype = _UniffiRustBuffer _UniffiLib.uniffi_glsdk_fn_method_node_send.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, @@ -896,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), @@ -1033,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), @@ -1321,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 @@ -1393,9 +1375,6 @@ class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_node_receive.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_receive.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_method_node_resolve_lnurl.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_method_node_resolve_lnurl.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_method_node_send.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_send.restype = ctypes.c_uint16 @@ -1405,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 @@ -1450,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 @@ -1465,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 @@ -1625,8 +1586,6 @@ def write(value, buf): - - class FundChannel: peer_id: "str" """ @@ -2449,7 +2408,7 @@ class LnUrlPayRequest: data: "LnUrlPayRequestData" """ - The resolved pay request data from `resolve_lnurl()`. + The resolved pay request data from `parse_input()`. """ amount_msat: "int" @@ -2524,7 +2483,8 @@ class LnUrlPayRequestData: Data from an LNURL-pay endpoint (LUD-06). Contains the service's accepted amount range and metadata. - Returned inside `ResolvedLnUrl::Pay` after resolving an LNURL. + Returned inside `InputType::LnUrlPay` after `parse_input` resolves + an LNURL or Lightning Address. """ callback: "str" @@ -2682,7 +2642,7 @@ class LnUrlWithdrawRequest: data: "LnUrlWithdrawRequestData" """ - The resolved withdraw request data from `resolve_lnurl()`. + The resolved withdraw request data from `parse_input()`. """ amount_msat: "int" @@ -2739,7 +2699,8 @@ class LnUrlWithdrawRequestData: Data from an LNURL-withdraw endpoint (LUD-03). Contains the service's accepted withdrawal range and session key. - Returned inside `ResolvedLnUrl::Withdraw` after resolving an LNURL. + Returned inside `InputType::LnUrlWithdraw` after `parse_input` + resolves an LNURL. """ callback: "str" @@ -4427,190 +4388,6 @@ def write(value, buf): -class InputType: - """ - The result of parsing user input. - """ - - def __init__(self): - raise RuntimeError("InputType cannot be instantiated directly") - - # Each enum variant is a nested class of the enum itself. - class BOLT11: - """ - A BOLT11 Lightning invoice. - """ - - invoice: "ParsedInvoice" - - def __init__(self,invoice: "ParsedInvoice"): - self.invoice = invoice - - def __str__(self): - return "InputType.BOLT11(invoice={})".format(self.invoice) - - def __eq__(self, other): - if not other.is_BOLT11(): - return False - if self.invoice != other.invoice: - return False - return True - - class NODE_ID: - """ - A Lightning node public key (66 hex characters, 33 bytes compressed). - """ - - node_id: "str" - - def __init__(self,node_id: "str"): - self.node_id = node_id - - def __str__(self): - return "InputType.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). The `url` field contains the - decoded URL. Call `Node::resolve_lnurl()` to determine whether - this is a pay or withdraw endpoint. - """ - - url: "str" - - def __init__(self,url: "str"): - self.url = url - - def __str__(self): - return "InputType.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), e.g. `user@domain.com`. - Call `Node::resolve_lnurl()` to resolve it to a pay request. - """ - - address: "str" - - def __init__(self,address: "str"): - self.address = address - - def __str__(self): - return "InputType.LN_URL_ADDRESS(address={})".format(self.address) - - def __eq__(self, other): - if not other.is_LN_URL_ADDRESS(): - return False - if self.address != other.address: - 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_BOLT11(self) -> bool: - return isinstance(self, InputType.BOLT11) - def is_bolt11(self) -> bool: - return isinstance(self, InputType.BOLT11) - def is_NODE_ID(self) -> bool: - return isinstance(self, InputType.NODE_ID) - def is_node_id(self) -> bool: - return isinstance(self, InputType.NODE_ID) - def is_LN_URL(self) -> bool: - return isinstance(self, InputType.LN_URL) - def is_ln_url(self) -> bool: - return isinstance(self, InputType.LN_URL) - def is_LN_URL_ADDRESS(self) -> bool: - return isinstance(self, InputType.LN_URL_ADDRESS) - def is_ln_url_address(self) -> bool: - return isinstance(self, InputType.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 -InputType.LN_URL = type("InputType.LN_URL", (InputType.LN_URL, InputType,), {}) # type: ignore -InputType.LN_URL_ADDRESS = type("InputType.LN_URL_ADDRESS", (InputType.LN_URL_ADDRESS, InputType,), {}) # type: ignore - - - - -class _UniffiConverterTypeInputType(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - variant = buf.read_i32() - if variant == 1: - return InputType.BOLT11( - _UniffiConverterTypeParsedInvoice.read(buf), - ) - if variant == 2: - return InputType.NODE_ID( - _UniffiConverterString.read(buf), - ) - if variant == 3: - return InputType.LN_URL( - _UniffiConverterString.read(buf), - ) - if variant == 4: - return InputType.LN_URL_ADDRESS( - _UniffiConverterString.read(buf), - ) - raise InternalError("Raw enum value doesn't match any cases") - - @staticmethod - def check_lower(value): - if value.is_BOLT11(): - _UniffiConverterTypeParsedInvoice.check_lower(value.invoice) - return - 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 - def write(value, buf): - if value.is_BOLT11(): - buf.write_i32(1) - _UniffiConverterTypeParsedInvoice.write(value.invoice, 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) - - - - - - - class InvoiceStatus(enum.Enum): UNPAID = 0 @@ -5089,6 +4866,24 @@ def __eq__(self, other): 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 + # For each variant, we have `is_NAME` and `is_name` methods for easily checking @@ -5097,12 +4892,17 @@ 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) # 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 @@ -5115,6 +4915,9 @@ def read(buf): return NodeEvent.INVOICE_PAID( _UniffiConverterTypeInvoicePaidEvent.read(buf), ) + if variant == 2: + return NodeEvent.UNKNOWN( + ) raise InternalError("Raw enum value doesn't match any cases") @staticmethod @@ -5122,6 +4925,8 @@ def check_lower(value): if value.is_INVOICE_PAID(): _UniffiConverterTypeInvoicePaidEvent.check_lower(value.details) return + if value.is_UNKNOWN(): + return raise ValueError(value) @staticmethod @@ -5129,6 +4934,8 @@ 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) @@ -5190,6 +4997,193 @@ def write(value, buf): +class ParsedInput: + """ + 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("ParsedInput cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class BOLT11: + """ + A BOLT11 Lightning invoice. + """ + + invoice: "ParsedInvoice" + + def __init__(self,invoice: "ParsedInvoice"): + self.invoice = invoice + + def __str__(self): + return "ParsedInput.BOLT11(invoice={})".format(self.invoice) + + def __eq__(self, other): + if not other.is_BOLT11(): + return False + if self.invoice != other.invoice: + return False + return True + + class NODE_ID: + """ + A Lightning node public key. + """ + + node_id: "str" + + def __init__(self,node_id: "str"): + self.node_id = node_id + + def __str__(self): + 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_LN_URL_ADDRESS(): + return False + if self.address != other.address: + 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_BOLT11(self) -> bool: + return isinstance(self, ParsedInput.BOLT11) + def is_bolt11(self) -> bool: + return isinstance(self, ParsedInput.BOLT11) + def is_NODE_ID(self) -> bool: + return isinstance(self, ParsedInput.NODE_ID) + def is_node_id(self) -> bool: + 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. +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 _UniffiConverterTypeParsedInput(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return ParsedInput.BOLT11( + _UniffiConverterTypeParsedInvoice.read(buf), + ) + if variant == 2: + 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") + + @staticmethod + def check_lower(value): + if value.is_BOLT11(): + _UniffiConverterTypeParsedInvoice.check_lower(value.invoice) + return + 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 + def write(value, buf): + if value.is_BOLT11(): + buf.write_i32(1) + _UniffiConverterTypeParsedInvoice.write(value.invoice, 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) + + + + + + + class PayStatus(enum.Enum): COMPLETE = 0 @@ -5358,18 +5352,61 @@ def write(value, buf): -class ResolvedLnUrl: +class ResolvedInput: """ - Result of resolving an LNURL or lightning address via HTTP. + 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("ResolvedLnUrl cannot be instantiated directly") + raise RuntimeError("ResolvedInput cannot be instantiated directly") # Each enum variant is a nested class of the enum itself. - class PAY: + class BOLT11: + """ + A BOLT11 Lightning invoice. No HTTP was performed. + """ + + invoice: "ParsedInvoice" + + def __init__(self,invoice: "ParsedInvoice"): + self.invoice = invoice + + def __str__(self): + return "ResolvedInput.BOLT11(invoice={})".format(self.invoice) + + def __eq__(self, other): + if not other.is_BOLT11(): + return False + if self.invoice != other.invoice: + return False + return True + + class NODE_ID: """ - The endpoint is an LNURL-pay service (LUD-06). + A Lightning node public key. No HTTP was performed. + """ + + node_id: "str" + + def __init__(self,node_id: "str"): + self.node_id = node_id + + def __str__(self): + return "ResolvedInput.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_PAY: + """ + An LNURL-pay endpoint with the service's parameters fetched. """ data: "LnUrlPayRequestData" @@ -5378,18 +5415,18 @@ def __init__(self,data: "LnUrlPayRequestData"): self.data = data def __str__(self): - return "ResolvedLnUrl.PAY(data={})".format(self.data) + return "ResolvedInput.LN_URL_PAY(data={})".format(self.data) def __eq__(self, other): - if not other.is_PAY(): + if not other.is_LN_URL_PAY(): return False if self.data != other.data: return False return True - class WITHDRAW: + class LN_URL_WITHDRAW: """ - The endpoint is an LNURL-withdraw service (LUD-03). + An LNURL-withdraw endpoint with the service's parameters fetched. """ data: "LnUrlWithdrawRequestData" @@ -5398,10 +5435,10 @@ def __init__(self,data: "LnUrlWithdrawRequestData"): self.data = data def __str__(self): - return "ResolvedLnUrl.WITHDRAW(data={})".format(self.data) + return "ResolvedInput.LN_URL_WITHDRAW(data={})".format(self.data) def __eq__(self, other): - if not other.is_WITHDRAW(): + if not other.is_LN_URL_WITHDRAW(): return False if self.data != other.data: return False @@ -5411,56 +5448,86 @@ 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_PAY(self) -> bool: - return isinstance(self, ResolvedLnUrl.PAY) - def is_pay(self) -> bool: - return isinstance(self, ResolvedLnUrl.PAY) - def is_WITHDRAW(self) -> bool: - return isinstance(self, ResolvedLnUrl.WITHDRAW) - def is_withdraw(self) -> bool: - return isinstance(self, ResolvedLnUrl.WITHDRAW) + 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. -ResolvedLnUrl.PAY = type("ResolvedLnUrl.PAY", (ResolvedLnUrl.PAY, ResolvedLnUrl,), {}) # type: ignore -ResolvedLnUrl.WITHDRAW = type("ResolvedLnUrl.WITHDRAW", (ResolvedLnUrl.WITHDRAW, ResolvedLnUrl,), {}) # type: ignore +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 _UniffiConverterTypeResolvedLnUrl(_UniffiConverterRustBuffer): +class _UniffiConverterTypeResolvedInput(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() if variant == 1: - return ResolvedLnUrl.PAY( - _UniffiConverterTypeLnUrlPayRequestData.read(buf), + return ResolvedInput.BOLT11( + _UniffiConverterTypeParsedInvoice.read(buf), ) if variant == 2: - return ResolvedLnUrl.WITHDRAW( + return ResolvedInput.NODE_ID( + _UniffiConverterString.read(buf), + ) + if variant == 3: + 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.is_PAY(): + if value.is_BOLT11(): + _UniffiConverterTypeParsedInvoice.check_lower(value.invoice) + return + if value.is_NODE_ID(): + _UniffiConverterString.check_lower(value.node_id) + return + if value.is_LN_URL_PAY(): _UniffiConverterTypeLnUrlPayRequestData.check_lower(value.data) return - if value.is_WITHDRAW(): + if value.is_LN_URL_WITHDRAW(): _UniffiConverterTypeLnUrlWithdrawRequestData.check_lower(value.data) return raise ValueError(value) @staticmethod def write(value, buf): - if value.is_PAY(): + if value.is_BOLT11(): buf.write_i32(1) - _UniffiConverterTypeLnUrlPayRequestData.write(value.data, buf) - if value.is_WITHDRAW(): + _UniffiConverterTypeParsedInvoice.write(value.invoice, buf) + if value.is_NODE_ID(): buf.write_i32(2) + _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) @@ -5688,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): @@ -6747,8 +6752,9 @@ def lnurl_pay(self, request: "LnUrlPayRequest"): callback, receives and validates a BOLT11 invoice, pays it, and processes any success action (LUD-09/10). - Call `resolve_lnurl()` first to get the `LnUrlPayRequestData`, - then build an `LnUrlPayRequest` with the user's chosen amount. + Call the top-level `parse_input` first to obtain the + `LnUrlPayRequestData`, then build an `LnUrlPayRequest` with the + user's chosen amount. """ raise NotImplementedError @@ -6760,8 +6766,9 @@ def lnurl_withdraw(self, request: "LnUrlWithdrawRequest"): it to the service's callback URL, and the service pays it asynchronously. - Call `resolve_lnurl()` first to get the `LnUrlWithdrawRequestData`, - then build an `LnUrlWithdrawRequest` with the user's chosen amount. + Call the top-level `parse_input` first to obtain the + `LnUrlWithdrawRequestData`, then build an `LnUrlWithdrawRequest` + with the user's chosen amount. """ raise NotImplementedError @@ -6814,19 +6821,6 @@ def receive(self, label: "str",description: "str",amount_msat: "typing.Optional[ to be created, and the incoming payment to be forwarded. """ - raise NotImplementedError - def resolve_lnurl(self, input: "str"): - """ - Resolve an LNURL or Lightning Address to its endpoint data. - - Performs the HTTP GET to the LNURL endpoint and returns the - typed request data. The result tells you whether this is a - pay or withdraw request, and includes the service's parameters. - - Accepts an LNURL bech32 string, a decoded URL (from - `parse_input()`), or a Lightning Address (`user@domain`). - """ - raise NotImplementedError def send(self, invoice: "str",amount_msat: "typing.Optional[int]"): raise NotImplementedError @@ -6858,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. @@ -7091,8 +7087,9 @@ def lnurl_pay(self, request: "LnUrlPayRequest") -> "LnUrlPayResult": callback, receives and validates a BOLT11 invoice, pays it, and processes any success action (LUD-09/10). - Call `resolve_lnurl()` first to get the `LnUrlPayRequestData`, - then build an `LnUrlPayRequest` with the user's chosen amount. + 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) @@ -7114,8 +7111,9 @@ def lnurl_withdraw(self, request: "LnUrlWithdrawRequest") -> "LnUrlWithdrawResul it to the service's callback URL, and the service pays it asynchronously. - Call `resolve_lnurl()` first to get the `LnUrlWithdrawRequestData`, - then build an `LnUrlWithdrawRequest` with the user's chosen amount. + 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) @@ -7222,29 +7220,6 @@ def receive(self, label: "str",description: "str",amount_msat: "typing.Optional[ - def resolve_lnurl(self, input: "str") -> "ResolvedLnUrl": - """ - Resolve an LNURL or Lightning Address to its endpoint data. - - Performs the HTTP GET to the LNURL endpoint and returns the - typed request data. The result tells you whether this is a - pay or withdraw request, and includes the service's parameters. - - Accepts an LNURL bech32 string, a decoded URL (from - `parse_input()`), or a Lightning Address (`user@domain`). - """ - - _UniffiConverterString.check_lower(input) - - return _UniffiConverterTypeResolvedLnUrl.lift( - _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_node_resolve_lnurl,self._uniffi_clone_pointer(), - _UniffiConverterString.lower(input)) - ) - - - - - def send(self, invoice: "str",amount_msat: "typing.Optional[int]") -> "SendResponse": _UniffiConverterString.check_lower(invoice) @@ -7321,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. @@ -7889,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() -def parse_input(input: "str") -> "InputType": +_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 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. @@ -7941,7 +7846,6 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "ChannelSide", "ChannelState", "Error", - "InputType", "InvoiceStatus", "ListIndex", "LnUrlPayResult", @@ -7950,11 +7854,12 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "Network", "NodeEvent", "OutputStatus", + "ParsedInput", "PayStatus", "PaymentStatus", "PaymentType", "PaymentTypeFilter", - "ResolvedLnUrl", + "ResolvedInput", "SuccessActionProcessed", "FundChannel", "FundOutput", @@ -7986,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", @@ -7994,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 5ebf4e68e..439d675e6 100644 --- a/libs/gl-sdk/src/input.rs +++ b/libs/gl-sdk/src/input.rs @@ -1,7 +1,26 @@ // Input parsing for BOLT11 invoices, Lightning node IDs, LNURL // strings, and Lightning Addresses. -// Works offline — no node connection or HTTP calls needed. - +// +// 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. @@ -23,28 +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). The `url` field contains the - /// decoded URL. Call `Node::resolve_lnurl()` to determine whether - /// this is a pay or withdraw endpoint. + /// 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), e.g. `user@domain.com`. - /// Call `Node::resolve_lnurl()` to resolve it to a pay request. + /// 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 its type. +/// 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.** /// -/// Recognizes BOLT11 invoices, node IDs, LNURL bech32 strings, and +/// Recognises BOLT11 invoices, node IDs, LNURL bech32 strings, and /// Lightning Addresses. Strips `lightning:` / `LIGHTNING:` prefixes -/// automatically. Works offline — no node connection needed. -pub fn parse_input(input: String) -> Result { +/// 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())); @@ -65,37 +107,84 @@ pub fn parse_input(input: String) -> 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(input_type) = try_parse_lightning_address(stripped) { - return Ok(input_type); + 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> { +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(InputType::LnUrl { url })), + 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 { +fn try_parse_lightning_address(input: &str) -> Option { let parts: Vec<&str> = input.split('@').collect(); if parts.len() != 2 { return None; @@ -116,14 +205,14 @@ fn try_parse_lightning_address(input: &str) -> Option { { return None; } - Some(InputType::LnUrlAddress { + 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; @@ -157,7 +246,7 @@ fn try_parse_bolt11(input: &str) -> Option> { .unwrap_or_default() .as_secs(); - Some(Ok(InputType::Bolt11 { + Some(Ok(ParsedInput::Bolt11 { invoice: ParsedInvoice { bolt11: input.to_string(), payee_pubkey: Some(payee_pubkey), @@ -171,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; } @@ -183,7 +272,7 @@ 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(), }) } @@ -192,105 +281,131 @@ fn try_parse_node_id(input: &str) -> Option { mod tests { use super::*; - #[test] - fn test_parse_lnurl_string() { - let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; - match parse_input(lnurl.to_string()).unwrap() { - InputType::LnUrl { url } => { - assert!(url.starts_with("https://")); - } - other => panic!("Expected LnUrl, got {:?}", variant_name(&other)), + 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_lnurl_lowercase() { - let lnurl = "lnurl1dp68gurn8ghj7cmfwp5x2unsw4hxktnrdakj7ctsdyhhvvf0d3h82unv9ucsaxqze2"; - match parse_input(lnurl.to_string()).unwrap() { - InputType::LnUrl { .. } => {} - other => panic!("Expected LnUrl, got {:?}", variant_name(&other)), + 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_lnurl_with_lightning_prefix() { - let input = "lightning:LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; - match parse_input(input.to_string()).unwrap() { - InputType::LnUrl { .. } => {} - other => panic!("Expected LnUrl, got {:?}", variant_name(&other)), + 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_invalid_lnurl() { - let result = parse_input("LNURL1INVALIDDATA".to_string()); - assert!(result.is_err()); + 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_lightning_address() { + fn test_parse_input_lightning_address_returns_address() { match parse_input("user@example.com".to_string()).unwrap() { - InputType::LnUrlAddress { address } => { - assert_eq!(address, "user@example.com"); - } - other => panic!("Expected LnUrlAddress, got {:?}", variant_name(&other)), + ParsedInput::LnUrlAddress { address } => assert_eq!(address, "user@example.com"), + other => panic!("Expected LnUrlAddress, got {}", parsed_variant_name(&other)), } } #[test] - fn test_parse_lightning_address_with_symbols() { - // LUD-16 allows a-z0-9-_. - match parse_input("sat.oshi-99@example.com".to_string()).unwrap() { - InputType::LnUrlAddress { address } => { - assert_eq!(address, "sat.oshi-99@example.com"); - } - other => panic!("Expected LnUrlAddress, got {:?}", variant_name(&other)), - } + fn test_parse_input_invalid_lnurl_errors() { + assert!(parse_input("LNURL1INVALIDDATA".to_string()).is_err()); } #[test] - fn test_parse_lightning_address_no_dot_in_domain() { - // "user@localhost" is not a valid Lightning Address - let result = parse_input("user@localhost".to_string()); - // Should fall through to "Unrecognized input" - assert!(result.is_err()); + fn test_parse_input_address_no_dot_in_domain_errors() { + assert!(parse_input("user@localhost".to_string()).is_err()); } #[test] - fn test_parse_lightning_address_empty_parts() { + 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_existing_bolt11_still_works() { - // A known valid mainnet invoice + 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 parse_input(invoice.to_string()).unwrap() { - InputType::Bolt11 { invoice: parsed } => { - assert_eq!(parsed.amount_msat, Some(10)); + 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 {:?}", variant_name(&other)), + other => panic!("Expected Bolt11, got {}", resolved_variant_name(&other)), } } #[test] - fn test_existing_node_id_still_works() { - // A compressed pubkey (starts with 02 or 03) + fn test_resolve_input_node_id_passes_through() { let node_id = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"; - match parse_input(node_id.to_string()).unwrap() { - InputType::NodeId { node_id: id } => assert_eq!(id, node_id), - other => panic!("Expected NodeId, got {:?}", variant_name(&other)), + 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)), } } - /// Helper for readable test failures. - fn variant_name(input: &InputType) -> &'static str { - match input { - InputType::Bolt11 { .. } => "Bolt11", - InputType::NodeId { .. } => "NodeId", - InputType::LnUrl { .. } => "LnUrl", - InputType::LnUrlAddress { .. } => "LnUrlAddress", - } + #[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 ec3772553..c5a97e050 100644 --- a/libs/gl-sdk/src/lib.rs +++ b/libs/gl-sdk/src/lib.rs @@ -46,12 +46,12 @@ 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, ResolvedLnUrl, SuccessActionProcessed, + LnUrlWithdrawResult, LnUrlWithdrawSuccessData, SuccessActionProcessed, }, node_builder::NodeBuilder, scheduler::Scheduler, @@ -233,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 index 5ff01ecd3..53030c1f9 100644 --- a/libs/gl-sdk/src/lnurl.rs +++ b/libs/gl-sdk/src/lnurl.rs @@ -9,19 +9,11 @@ use gl_client::lnurl::models as wire; // ── Resolved endpoint data ────────────────────────────────────────── -/// Result of resolving an LNURL or lightning address via HTTP. -#[derive(Clone, uniffi::Enum)] -pub enum ResolvedLnUrl { - /// The endpoint is an LNURL-pay service (LUD-06). - Pay { data: LnUrlPayRequestData }, - /// The endpoint is an LNURL-withdraw service (LUD-03). - Withdraw { data: LnUrlWithdrawRequestData }, -} - /// Data from an LNURL-pay endpoint (LUD-06). /// /// Contains the service's accepted amount range and metadata. -/// Returned inside `ResolvedLnUrl::Pay` after resolving an LNURL. +/// 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. @@ -43,7 +35,8 @@ pub struct LnUrlPayRequestData { /// Data from an LNURL-withdraw endpoint (LUD-03). /// /// Contains the service's accepted withdrawal range and session key. -/// Returned inside `ResolvedLnUrl::Withdraw` after resolving an LNURL. +/// 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. @@ -67,7 +60,7 @@ pub struct LnUrlWithdrawRequestData { /// Combines the resolved service data with the user's chosen amount. #[derive(Clone, uniffi::Record)] pub struct LnUrlPayRequest { - /// The resolved pay request data from `resolve_lnurl()`. + /// The resolved pay request data from `parse_input()`. pub data: LnUrlPayRequestData, /// Amount to pay in millisatoshis. pub amount_msat: u64, @@ -89,7 +82,7 @@ pub struct LnUrlPayRequest { /// Combines the resolved service data with the user's chosen amount. #[derive(Clone, uniffi::Record)] pub struct LnUrlWithdrawRequest { - /// The resolved withdraw request data from `resolve_lnurl()`. + /// The resolved withdraw request data from `parse_input()`. pub data: LnUrlWithdrawRequestData, /// Amount to withdraw in millisatoshis. pub amount_msat: u64, @@ -215,19 +208,6 @@ impl From for SuccessActionProcessed { } } -impl From for ResolvedLnUrl { - fn from(r: gl_client::lnurl::LnUrlResponse) -> Self { - match r { - gl_client::lnurl::LnUrlResponse::Pay(data) => ResolvedLnUrl::Pay { - data: data.into(), - }, - gl_client::lnurl::LnUrlResponse::Withdraw(data) => ResolvedLnUrl::Withdraw { - data: data.into(), - }, - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/libs/gl-sdk/src/node.rs b/libs/gl-sdk/src/node.rs index e3b9b2fa6..bdd8637ba 100644 --- a/libs/gl-sdk/src/node.rs +++ b/libs/gl-sdk/src/node.rs @@ -649,62 +649,15 @@ impl Node { // ── LNURL methods ─────────────────────────────────────────── - /// Resolve an LNURL or Lightning Address to its endpoint data. - /// - /// Performs the HTTP GET to the LNURL endpoint and returns the - /// typed request data. The result tells you whether this is a - /// pay or withdraw request, and includes the service's parameters. - /// - /// Accepts an LNURL bech32 string, a decoded URL (from - /// `parse_input()`), or a Lightning Address (`user@domain`). - pub fn resolve_lnurl( - &self, - input: String, - ) -> Result { - use gl_client::lnurl::models::LnUrlHttpClearnetClient; - use gl_client::lnurl::LNURL; - - let lnurl_client = LNURL::new(LnUrlHttpClearnetClient::new()); - let trimmed = input.trim(); - - // Determine the URL to fetch - let url = if trimmed.contains('@') { - gl_client::lnurl::pay::parse_lightning_address(trimmed) - .map_err(|e| Error::Other(e.to_string()))? - } else if trimmed.to_uppercase().starts_with("LNURL1") { - gl_client::lnurl::utils::parse_lnurl(trimmed) - .map_err(|e| Error::Other(e.to_string()))? - } else { - // Assume it's already a decoded URL - trimmed.to_string() - }; - - let response = exec(lnurl_client.resolve(&url)) - .map_err(|e| Error::Other(e.to_string()))?; - - let mut resolved: crate::lnurl::ResolvedLnUrl = response.into(); - - // Preserve the original input as the lnurl field - match &mut resolved { - crate::lnurl::ResolvedLnUrl::Pay { data } => { - data.lnurl = trimmed.to_string(); - } - crate::lnurl::ResolvedLnUrl::Withdraw { data } => { - data.lnurl = trimmed.to_string(); - } - } - - Ok(resolved) - } - /// 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 `resolve_lnurl()` first to get the `LnUrlPayRequestData`, - /// then build an `LnUrlPayRequest` with the user's chosen amount. + /// 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, @@ -803,8 +756,9 @@ impl Node { /// it to the service's callback URL, and the service pays it /// asynchronously. /// - /// Call `resolve_lnurl()` first to get the `LnUrlWithdrawRequestData`, - /// then build an `LnUrlWithdrawRequest` with the user's chosen amount. + /// 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, diff --git a/libs/gl-sdk/tests/test_lnurl.py b/libs/gl-sdk/tests/test_lnurl.py index 5c59b98bb..acd526b3c 100644 --- a/libs/gl-sdk/tests/test_lnurl.py +++ b/libs/gl-sdk/tests/test_lnurl.py @@ -8,6 +8,8 @@ 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 @@ -62,57 +64,64 @@ def fund_and_connect(node_factory, bitcoind, lnurl_service): return relay -def test_resolve_lnurl_pay( - scheduler, nobody_id, node_factory, bitcoind, lnurl_service -): - """Resolve an LNURL-pay endpoint via the SDK.""" - sdk_node, config = make_sdk_node(nobody_id, scheduler) - - try: - resolved = sdk_node.resolve_lnurl(lnurl_service.pay_url) - - assert isinstance(resolved, glsdk.ResolvedLnUrl.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) - finally: - sdk_node.disconnect() - - -def test_resolve_lnurl_withdraw( - scheduler, nobody_id, node_factory, bitcoind, lnurl_service -): - """Resolve an LNURL-withdraw endpoint via the SDK.""" - sdk_node, config = make_sdk_node(nobody_id, scheduler) - - try: - resolved = sdk_node.resolve_lnurl(lnurl_service.withdraw_url) - - assert isinstance(resolved, glsdk.ResolvedLnUrl.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 - finally: - sdk_node.disconnect() - - -def test_resolve_lightning_address_url( - scheduler, nobody_id, node_factory, bitcoind, lnurl_service -): - """Resolve a Lightning Address well-known URL (LUD-16).""" - sdk_node, config = make_sdk_node(nobody_id, scheduler) - - try: - resolved = sdk_node.resolve_lnurl(lnurl_service.lightning_address_url) +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.ResolvedLnUrl.PAY) - data = resolved.data - assert data.min_sendable == lnurl_service.min_sendable - finally: - sdk_node.disconnect() + 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( @@ -160,8 +169,8 @@ def test_lnurl_pay_end_to_end( try: # Resolve - resolved = sdk_node.resolve_lnurl(lnurl_service.pay_url) - assert isinstance(resolved, glsdk.ResolvedLnUrl.PAY) + 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 @@ -226,7 +235,8 @@ def test_lnurl_pay_with_message_success_action( sdk_node = glsdk.Node(glsdk.Credentials.load(creds_bytes)) try: - resolved = sdk_node.resolve_lnurl(lnurl_service.pay_url) + 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( 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@")