|
1 | 1 | #[cfg(test)] |
2 | 2 | mod tests { |
3 | 3 | use crate::lnurl::implementation::{ |
4 | | - create_channel_request_url, create_withdraw_callback_url, lnurl_auth, |
| 4 | + build_lnurl_pay_callback_url, create_channel_request_url, create_withdraw_callback_url, |
| 5 | + get_lnurl_invoice_for_pay_data, lnurl_auth, validate_lnurl_pay_invoice, |
5 | 6 | }; |
6 | 7 | use crate::lnurl::{ChannelRequestParams, LnurlAuthParams, LnurlError, WithdrawCallbackParams}; |
| 8 | + use crate::LnurlPayData; |
| 9 | + use bitcoin::hashes::{sha256, Hash as _}; |
| 10 | + use bitcoin::secp256k1::{Secp256k1, SecretKey}; |
| 11 | + use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret}; |
7 | 12 | use lnurl::get_derivation_path; |
8 | 13 |
|
9 | 14 | const TEST_MNEMONIC: &str = "stable inch effort skull suggest circle charge lemon amazing clean giant quantum party grow visa best rule icon gown disagree win drop smile love"; |
| 15 | + const TEST_METADATA: &str = "[[\"text/plain\",\"test payment\"]]"; |
| 16 | + const TEST_AMOUNT_MSATS: u64 = 12_345_000; |
| 17 | + |
| 18 | + fn create_test_invoice(amount_msats: Option<u64>, metadata: &str, hashed: bool) -> String { |
| 19 | + let secp = Secp256k1::new(); |
| 20 | + let secret_key = SecretKey::from_slice(&[0xab; 32]).unwrap(); |
| 21 | + |
| 22 | + let mut builder = InvoiceBuilder::new(Currency::Bitcoin) |
| 23 | + .payment_hash(sha256::Hash::from_byte_array([1u8; 32])) |
| 24 | + .payment_secret(PaymentSecret([2u8; 32])) |
| 25 | + .current_timestamp() |
| 26 | + .min_final_cltv_expiry_delta(144); |
| 27 | + |
| 28 | + if let Some(amount_msats) = amount_msats { |
| 29 | + builder = builder.amount_milli_satoshis(amount_msats); |
| 30 | + } |
| 31 | + |
| 32 | + let builder = if hashed { |
| 33 | + builder.description_hash(sha256::Hash::hash(metadata.as_bytes())) |
| 34 | + } else { |
| 35 | + builder.description(metadata.to_string()) |
| 36 | + }; |
| 37 | + |
| 38 | + builder |
| 39 | + .build_signed(|hash| secp.sign_ecdsa_recoverable(hash, &secret_key)) |
| 40 | + .unwrap() |
| 41 | + .to_string() |
| 42 | + } |
| 43 | + |
| 44 | + fn test_pay_data() -> LnurlPayData { |
| 45 | + LnurlPayData { |
| 46 | + uri: "lnurl1test".to_string(), |
| 47 | + callback: "https://example.com/callback?existing=1".to_string(), |
| 48 | + min_sendable: 1_000, |
| 49 | + max_sendable: 20_000_000, |
| 50 | + metadata_str: TEST_METADATA.to_string(), |
| 51 | + comment_allowed: Some(100), |
| 52 | + allows_nostr: false, |
| 53 | + nostr_pubkey: None, |
| 54 | + } |
| 55 | + } |
10 | 56 |
|
11 | 57 | #[test] |
12 | 58 | fn test_create_channel_request_url() { |
@@ -136,6 +182,118 @@ mod tests { |
136 | 182 | assert!(matches!(result, Err(LnurlError::InvalidAddress))); |
137 | 183 | } |
138 | 184 |
|
| 185 | + #[test] |
| 186 | + fn test_lnurl_pay_callback_url_preserves_existing_params() { |
| 187 | + let url = build_lnurl_pay_callback_url( |
| 188 | + "https://example.com/callback?existing=param", |
| 189 | + TEST_AMOUNT_MSATS, |
| 190 | + Some("hello"), |
| 191 | + ) |
| 192 | + .unwrap(); |
| 193 | + |
| 194 | + assert_eq!(url.scheme(), "https"); |
| 195 | + assert!(url.as_str().contains("existing=param")); |
| 196 | + assert!(url.as_str().contains("amount=12345000")); |
| 197 | + assert!(url.as_str().contains("comment=hello")); |
| 198 | + } |
| 199 | + |
| 200 | + #[test] |
| 201 | + fn test_validate_lnurl_pay_invoice_exact_match() { |
| 202 | + let invoice = create_test_invoice(Some(TEST_AMOUNT_MSATS), TEST_METADATA, false); |
| 203 | + |
| 204 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 205 | + |
| 206 | + assert!(result.is_ok()); |
| 207 | + } |
| 208 | + |
| 209 | + #[test] |
| 210 | + fn test_validate_lnurl_pay_invoice_larger_mismatch() { |
| 211 | + let invoice = create_test_invoice(Some(TEST_AMOUNT_MSATS + 1_000), TEST_METADATA, false); |
| 212 | + |
| 213 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 214 | + |
| 215 | + assert!(matches!( |
| 216 | + result, |
| 217 | + Err(LnurlError::AmountMismatch { |
| 218 | + requested_msats: TEST_AMOUNT_MSATS, |
| 219 | + invoice_msats |
| 220 | + }) if invoice_msats == TEST_AMOUNT_MSATS + 1_000 |
| 221 | + )); |
| 222 | + } |
| 223 | + |
| 224 | + #[test] |
| 225 | + fn test_validate_lnurl_pay_invoice_smaller_mismatch() { |
| 226 | + let invoice = create_test_invoice(Some(TEST_AMOUNT_MSATS - 1_000), TEST_METADATA, false); |
| 227 | + |
| 228 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 229 | + |
| 230 | + assert!(matches!( |
| 231 | + result, |
| 232 | + Err(LnurlError::AmountMismatch { |
| 233 | + requested_msats: TEST_AMOUNT_MSATS, |
| 234 | + invoice_msats |
| 235 | + }) if invoice_msats == TEST_AMOUNT_MSATS - 1_000 |
| 236 | + )); |
| 237 | + } |
| 238 | + |
| 239 | + #[test] |
| 240 | + fn test_validate_lnurl_pay_invoice_amountless() { |
| 241 | + let invoice = create_test_invoice(None, TEST_METADATA, false); |
| 242 | + |
| 243 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 244 | + |
| 245 | + assert!(matches!( |
| 246 | + result, |
| 247 | + Err(LnurlError::AmountMismatch { |
| 248 | + requested_msats: TEST_AMOUNT_MSATS, |
| 249 | + invoice_msats: 0 |
| 250 | + }) |
| 251 | + )); |
| 252 | + } |
| 253 | + |
| 254 | + #[test] |
| 255 | + fn test_validate_lnurl_pay_invoice_malformed() { |
| 256 | + let result = validate_lnurl_pay_invoice("lnbc1malformed", TEST_AMOUNT_MSATS); |
| 257 | + |
| 258 | + assert!(matches!(result, Err(LnurlError::InvalidResponse))); |
| 259 | + } |
| 260 | + |
| 261 | + #[tokio::test] |
| 262 | + async fn test_get_lnurl_invoice_for_pay_data_amount_outside_range() { |
| 263 | + let data = test_pay_data(); |
| 264 | + |
| 265 | + let result = get_lnurl_invoice_for_pay_data(data, 999, None).await; |
| 266 | + |
| 267 | + assert!(matches!(result, Err(LnurlError::InvalidAmount { .. }))); |
| 268 | + } |
| 269 | + |
| 270 | + #[test] |
| 271 | + fn test_validate_lnurl_pay_invoice_matching_amount_with_hash_description() { |
| 272 | + let invoice = create_test_invoice(Some(TEST_AMOUNT_MSATS), TEST_METADATA, true); |
| 273 | + |
| 274 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 275 | + |
| 276 | + assert!(result.is_ok()); |
| 277 | + } |
| 278 | + |
| 279 | + #[test] |
| 280 | + fn test_validate_lnurl_pay_invoice_matching_amount_with_text_description() { |
| 281 | + let invoice = create_test_invoice(Some(TEST_AMOUNT_MSATS), "test payment", false); |
| 282 | + |
| 283 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 284 | + |
| 285 | + assert!(result.is_ok()); |
| 286 | + } |
| 287 | + |
| 288 | + #[test] |
| 289 | + fn test_validate_lnurl_pay_invoice_matching_amount_with_different_description() { |
| 290 | + let invoice = create_test_invoice(Some(TEST_AMOUNT_MSATS), "other metadata", false); |
| 291 | + |
| 292 | + let result = validate_lnurl_pay_invoice(&invoice, TEST_AMOUNT_MSATS); |
| 293 | + |
| 294 | + assert!(result.is_ok()); |
| 295 | + } |
| 296 | + |
139 | 297 | #[test] |
140 | 298 | fn test_get_derivation_path() { |
141 | 299 | use url::Url; |
|
0 commit comments