Skip to main content

glsdk/
lnurl.rs

1// LNURL types for UniFFI language bindings.
2//
3// These are thin wrappers around gl-client's protocol types, adding
4// UniFFI annotations so they can be exported to Python, Kotlin, Swift,
5// and Ruby. Protocol logic lives in gl-client; this module only does
6// type conversion.
7
8use gl_client::lnurl::models as wire;
9
10// ── Resolved endpoint data ──────────────────────────────────────────
11
12/// Data from an LNURL-pay endpoint (LUD-06).
13///
14/// Contains the service's accepted amount range and metadata.
15/// Returned inside `InputType::LnUrlPay` after `parse_input` resolves
16/// an LNURL or Lightning Address.
17#[derive(Clone, uniffi::Record)]
18pub struct LnUrlPayRequestData {
19    /// The callback URL to request an invoice from.
20    pub callback: String,
21    /// Minimum amount the service accepts, in millisatoshis.
22    pub min_sendable: u64,
23    /// Maximum amount the service accepts, in millisatoshis.
24    pub max_sendable: u64,
25    /// Raw metadata JSON string (array of `["mime", "content"]` pairs).
26    pub metadata: String,
27    /// Maximum comment length the service accepts. 0 means no comments.
28    pub comment_allowed: u64,
29    /// Human-readable description extracted from metadata.
30    pub description: String,
31    /// The original LNURL or lightning address that was resolved.
32    pub lnurl: String,
33}
34
35/// Data from an LNURL-withdraw endpoint (LUD-03).
36///
37/// Contains the service's accepted withdrawal range and session key.
38/// Returned inside `InputType::LnUrlWithdraw` after `parse_input`
39/// resolves an LNURL.
40#[derive(Clone, uniffi::Record)]
41pub struct LnUrlWithdrawRequestData {
42    /// The callback URL to submit the invoice to.
43    pub callback: String,
44    /// Ephemeral secret linking this wallet session to the service.
45    pub k1: String,
46    /// Default description for the invoice.
47    pub default_description: String,
48    /// Minimum withdrawable amount in millisatoshis.
49    pub min_withdrawable: u64,
50    /// Maximum withdrawable amount in millisatoshis.
51    pub max_withdrawable: u64,
52    /// The original LNURL that was resolved.
53    pub lnurl: String,
54}
55
56// ── User request types ──────────────────────────────────────────────
57
58/// Request to execute an LNURL-pay flow.
59///
60/// Combines the resolved service data with the user's chosen amount.
61#[derive(Clone, uniffi::Record)]
62pub struct LnUrlPayRequest {
63    /// The resolved pay request data from `parse_input()`.
64    pub data: LnUrlPayRequestData,
65    /// Amount to pay in millisatoshis.
66    pub amount_msat: u64,
67    /// Optional comment to send with the payment.
68    pub comment: Option<String>,
69    /// When true (the default), a URL success action is rejected if its
70    /// domain differs from the callback's domain.
71    ///
72    /// This is a wallet-side safety convention, not a LUD-09 requirement:
73    /// LUD-09 does not mandate same-domain URLs, but a divergent domain
74    /// can be used to phish users, so the SDK rejects it by default.
75    /// Set to `Some(false)` only if you have a specific reason to trust
76    /// cross-domain success-action URLs from this service.
77    pub validate_success_action_url: Option<bool>,
78}
79
80/// Request to execute an LNURL-withdraw flow.
81///
82/// Combines the resolved service data with the user's chosen amount.
83#[derive(Clone, uniffi::Record)]
84pub struct LnUrlWithdrawRequest {
85    /// The resolved withdraw request data from `parse_input()`.
86    pub data: LnUrlWithdrawRequestData,
87    /// Amount to withdraw in millisatoshis.
88    pub amount_msat: u64,
89    /// Optional description for the invoice (overrides default).
90    pub description: Option<String>,
91}
92
93// ── Result types ────────────────────────────────────────────────────
94
95/// Result of an LNURL-pay operation.
96#[derive(Clone, uniffi::Enum)]
97pub enum LnUrlPayResult {
98    /// Payment succeeded.
99    EndpointSuccess { data: LnUrlPaySuccessData },
100    /// The LNURL service returned an error before the invoice was paid.
101    EndpointError { data: LnUrlErrorData },
102    /// The invoice was fetched successfully but paying it failed.
103    PayError { data: LnUrlPayErrorData },
104}
105
106/// Successful LNURL-pay result data.
107#[derive(Clone, uniffi::Record)]
108pub struct LnUrlPaySuccessData {
109    /// The payment preimage (proof of payment), hex-encoded.
110    pub payment_preimage: String,
111    /// Optional success action from the service (LUD-09).
112    pub success_action: Option<SuccessActionProcessed>,
113}
114
115/// Details of a failed LNURL-pay attempt on the pay phase.
116#[derive(Clone, uniffi::Record)]
117pub struct LnUrlPayErrorData {
118    /// Hex-encoded payment hash of the invoice the service returned.
119    pub payment_hash: String,
120    /// Human-readable reason the pay attempt failed.
121    pub reason: String,
122}
123
124/// Result of an LNURL-withdraw operation.
125#[derive(Clone, uniffi::Enum)]
126pub enum LnUrlWithdrawResult {
127    /// The service accepted our invoice and will pay it.
128    Ok { data: LnUrlWithdrawSuccessData },
129    /// The LNURL service returned an error.
130    ErrorStatus { data: LnUrlErrorData },
131}
132
133/// Successful LNURL-withdraw result data.
134#[derive(Clone, uniffi::Record)]
135pub struct LnUrlWithdrawSuccessData {
136    /// The BOLT11 invoice that was submitted for withdrawal.
137    pub invoice: String,
138}
139
140/// Error returned by an LNURL service endpoint.
141#[derive(Clone, uniffi::Record)]
142pub struct LnUrlErrorData {
143    pub reason: String,
144}
145
146// ── Success action types (LUD-09 / LUD-10) ─────────────────────────
147
148/// A processed success action from an LNURL-pay callback.
149///
150/// For Message and Url this is passed through as-is. For Aes the
151/// ciphertext has been decrypted using the payment preimage.
152#[derive(Clone, uniffi::Enum)]
153pub enum SuccessActionProcessed {
154    /// Display a message to the user.
155    Message { message: String },
156    /// Display a URL to the user.
157    Url { description: String, url: String },
158    /// Decrypted AES payload (LUD-10).
159    Aes { description: String, plaintext: String },
160}
161
162// ── From conversions (gl-client → gl-sdk) ───────────────────────────
163
164impl From<wire::PayRequestResponse> for LnUrlPayRequestData {
165    fn from(r: wire::PayRequestResponse) -> Self {
166        Self {
167            description: r.description().unwrap_or_default(),
168            callback: r.callback,
169            min_sendable: r.min_sendable,
170            max_sendable: r.max_sendable,
171            metadata: r.metadata,
172            comment_allowed: r.comment_allowed.unwrap_or(0),
173            lnurl: String::new(), // caller sets this after conversion
174        }
175    }
176}
177
178impl From<wire::WithdrawRequestResponse> for LnUrlWithdrawRequestData {
179    fn from(r: wire::WithdrawRequestResponse) -> Self {
180        Self {
181            callback: r.callback,
182            k1: r.k1,
183            default_description: r.default_description,
184            min_withdrawable: r.min_withdrawable,
185            max_withdrawable: r.max_withdrawable,
186            lnurl: String::new(), // caller sets this after conversion
187        }
188    }
189}
190
191impl From<wire::ProcessedSuccessAction> for SuccessActionProcessed {
192    fn from(a: wire::ProcessedSuccessAction) -> Self {
193        match a {
194            wire::ProcessedSuccessAction::Message { message } => {
195                SuccessActionProcessed::Message { message }
196            }
197            wire::ProcessedSuccessAction::Url { description, url } => {
198                SuccessActionProcessed::Url { description, url }
199            }
200            wire::ProcessedSuccessAction::Aes {
201                description,
202                plaintext,
203            } => SuccessActionProcessed::Aes {
204                description,
205                plaintext,
206            },
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_pay_request_data_from_conversion() {
217        let wire_resp = wire::PayRequestResponse {
218            callback: "https://example.com/cb".to_string(),
219            max_sendable: 100000,
220            min_sendable: 1000,
221            tag: "payRequest".to_string(),
222            metadata: r#"[["text/plain", "Buy coffee"]]"#.to_string(),
223            comment_allowed: Some(140),
224        };
225
226        let data: LnUrlPayRequestData = wire_resp.into();
227        assert_eq!(data.callback, "https://example.com/cb");
228        assert_eq!(data.min_sendable, 1000);
229        assert_eq!(data.max_sendable, 100000);
230        assert_eq!(data.comment_allowed, 140);
231        assert_eq!(data.description, "Buy coffee");
232        assert!(data.lnurl.is_empty()); // caller sets this
233    }
234
235    #[test]
236    fn test_pay_request_data_no_comment_allowed() {
237        let wire_resp = wire::PayRequestResponse {
238            callback: "https://example.com/cb".to_string(),
239            max_sendable: 100000,
240            min_sendable: 1000,
241            tag: "payRequest".to_string(),
242            metadata: r#"[["text/plain", "test"]]"#.to_string(),
243            comment_allowed: None,
244        };
245
246        let data: LnUrlPayRequestData = wire_resp.into();
247        assert_eq!(data.comment_allowed, 0);
248    }
249
250    #[test]
251    fn test_withdraw_request_data_from_conversion() {
252        let wire_resp = wire::WithdrawRequestResponse {
253            tag: "withdrawRequest".to_string(),
254            callback: "https://example.com/withdraw".to_string(),
255            k1: "secret123".to_string(),
256            default_description: "Withdraw from service".to_string(),
257            min_withdrawable: 1000,
258            max_withdrawable: 50000,
259        };
260
261        let data: LnUrlWithdrawRequestData = wire_resp.into();
262        assert_eq!(data.callback, "https://example.com/withdraw");
263        assert_eq!(data.k1, "secret123");
264        assert_eq!(data.default_description, "Withdraw from service");
265        assert_eq!(data.min_withdrawable, 1000);
266        assert_eq!(data.max_withdrawable, 50000);
267    }
268
269    #[test]
270    fn test_processed_success_action_from_message() {
271        let processed = wire::ProcessedSuccessAction::Message {
272            message: "Thanks!".to_string(),
273        };
274        let sdk: SuccessActionProcessed = processed.into();
275        match sdk {
276            SuccessActionProcessed::Message { message } => assert_eq!(message, "Thanks!"),
277            _ => panic!("Expected Message variant"),
278        }
279    }
280
281    #[test]
282    fn test_processed_success_action_from_url() {
283        let processed = wire::ProcessedSuccessAction::Url {
284            description: "View order".to_string(),
285            url: "https://example.com/order".to_string(),
286        };
287        let sdk: SuccessActionProcessed = processed.into();
288        match sdk {
289            SuccessActionProcessed::Url { description, url } => {
290                assert_eq!(description, "View order");
291                assert_eq!(url, "https://example.com/order");
292            }
293            _ => panic!("Expected Url variant"),
294        }
295    }
296
297    #[test]
298    fn test_processed_success_action_from_aes() {
299        let processed = wire::ProcessedSuccessAction::Aes {
300            description: "Your code".to_string(),
301            plaintext: "ABC-123".to_string(),
302        };
303        let sdk: SuccessActionProcessed = processed.into();
304        match sdk {
305            SuccessActionProcessed::Aes {
306                description,
307                plaintext,
308            } => {
309                assert_eq!(description, "Your code");
310                assert_eq!(plaintext, "ABC-123");
311            }
312            _ => panic!("Expected Aes variant"),
313        }
314    }
315}