diff --git a/openapi.yaml b/openapi.yaml index acb344b0..0dbf4070 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2239,6 +2239,12 @@ components: - integer - 'null' example: 42 + payment_hash: + type: + - string + - 'null' + description: Optional payment hash (hex string, 32 bytes) for HODL invoices. + example: "229a76310b22a588271adcabc93362239c538103ebe2a8dea1822f9cde6c6040" LNInvoiceResponse: type: object required: diff --git a/src/ldk.rs b/src/ldk.rs index 9dfd0261..94e1483d 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -743,9 +743,32 @@ async fn handle_ldk_events( } => payment_preimage, PaymentPurpose::SpontaneousPayment(preimage) => Some(preimage), }; - unlocked_state - .channel_manager - .claim_funds(payment_preimage.unwrap()); + + let preimage_to_use = match payment_preimage { + Some(preimage) => Some(preimage), + None => { + let inbound_payments = unlocked_state.inbound_payments(); + match inbound_payments.get(&payment_hash) { + Some(payment_info) => payment_info.preimage, + None => { + tracing::error!( + "EVENT: No payment info found for payment hash {}", + payment_hash + ); + return Ok(()); + } + } + } + }; + + if let Some(preimage) = preimage_to_use { + unlocked_state.channel_manager.claim_funds(preimage); + } else { + tracing::info!( + "EVENT: Holding inbound payment with hash {} (no preimage yet, waiting for claim)", + payment_hash + ); + } } Event::PaymentClaimed { payment_hash, @@ -850,6 +873,24 @@ async fn handle_ldk_events( payment_preimage ); } + + // Auto-claim pending inbound HODL payment with the same payment_hash + { + let inbound_payments = unlocked_state.inbound_payments(); + if let Some(inbound) = inbound_payments.get(&payment_hash) { + if inbound.status == HTLCStatus::Pending { + drop(inbound_payments); + tracing::info!( + "EVENT: auto-claiming inbound HODL payment with hash {} using preimage {}", + payment_hash, + payment_preimage + ); + unlocked_state + .channel_manager + .claim_funds(payment_preimage); + } + } + } } Event::OpenChannelRequest { ref temporary_channel_id, diff --git a/src/routes.rs b/src/routes.rs index fbf2c6f4..6b2acc20 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -819,6 +819,7 @@ pub(crate) struct LNInvoiceRequest { pub(crate) expiry_sec: u32, pub(crate) asset_id: Option, pub(crate) asset_amount: Option, + pub(crate) payment_hash: Option, } #[derive(Deserialize, Serialize)] @@ -2691,20 +2692,48 @@ pub(crate) async fn ln_invoice( ))); } - let invoice_params = Bolt11InvoiceParameters { - amount_msats: payload.amt_msat, - invoice_expiry_delta_secs: Some(payload.expiry_sec), - contract_id, - asset_amount: payload.asset_amount, - ..Default::default() - }; + let (invoice, preimage_opt) = if let Some(ref hash_hex) = payload.payment_hash { + let hash_bytes: [u8; 32] = hex_str_to_vec(hash_hex) + .and_then(|data| data.try_into().ok()) + .ok_or_else(|| { + APIError::InvalidPaymentHash("invalid payment hash hex".to_string()) + })?; + let payment_hash = PaymentHash(hash_bytes); - let invoice = match unlocked_state - .channel_manager - .create_bolt11_invoice(invoice_params) - { - Ok(inv) => inv, - Err(e) => return Err(APIError::FailedInvoiceCreation(e.to_string())), + let invoice_params = Bolt11InvoiceParameters { + amount_msats: payload.amt_msat, + invoice_expiry_delta_secs: Some(payload.expiry_sec), + payment_hash: Some(payment_hash), + contract_id, + asset_amount: payload.asset_amount, + ..Default::default() + }; + + let invoice = match unlocked_state + .channel_manager + .create_bolt11_invoice(invoice_params) + { + Ok(inv) => inv, + Err(e) => return Err(APIError::FailedInvoiceCreation(e.to_string())), + }; + (invoice, None) + } else { + let invoice_params = Bolt11InvoiceParameters { + amount_msats: payload.amt_msat, + invoice_expiry_delta_secs: Some(payload.expiry_sec), + contract_id, + asset_amount: payload.asset_amount, + ..Default::default() + }; + + let invoice = match unlocked_state + .channel_manager + .create_bolt11_invoice(invoice_params) + { + Ok(inv) => inv, + Err(e) => return Err(APIError::FailedInvoiceCreation(e.to_string())), + }; + (invoice, None) }; let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array()); @@ -2712,7 +2741,7 @@ pub(crate) async fn ln_invoice( unlocked_state.add_inbound_payment( payment_hash, PaymentInfo { - preimage: None, + preimage: preimage_opt, secret: Some(*invoice.payment_secret()), status: HTLCStatus::Pending, amt_msat: payload.amt_msat,