|
|
@@ -11,7 +11,6 @@ use std::sync::Arc;
|
|
|
|
|
|
use anyhow::anyhow;
|
|
|
use async_trait::async_trait;
|
|
|
-use axum::Router;
|
|
|
use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
|
|
|
use cdk_common::common::FeeReserve;
|
|
|
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
|
|
@@ -36,7 +35,6 @@ pub mod error;
|
|
|
pub struct LNbits {
|
|
|
lnbits_api: LNBitsClient,
|
|
|
fee_reserve: FeeReserve,
|
|
|
- webhook_url: Option<String>,
|
|
|
wait_invoice_cancel_token: CancellationToken,
|
|
|
wait_invoice_is_active: Arc<AtomicBool>,
|
|
|
settings: Bolt11Settings,
|
|
|
@@ -50,14 +48,12 @@ impl LNbits {
|
|
|
invoice_api_key: String,
|
|
|
api_url: String,
|
|
|
fee_reserve: FeeReserve,
|
|
|
- webhook_url: Option<String>,
|
|
|
) -> Result<Self, Error> {
|
|
|
let lnbits_api = LNBitsClient::new("", &admin_api_key, &invoice_api_key, &api_url, None)?;
|
|
|
|
|
|
Ok(Self {
|
|
|
lnbits_api,
|
|
|
fee_reserve,
|
|
|
- webhook_url,
|
|
|
wait_invoice_cancel_token: CancellationToken::new(),
|
|
|
wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
|
|
|
settings: Bolt11Settings {
|
|
|
@@ -83,6 +79,64 @@ impl LNbits {
|
|
|
Error::Anyhow(err)
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
+ /// Process an incoming message from the websocket receiver
|
|
|
+ async fn process_message(
|
|
|
+ msg_option: Option<String>,
|
|
|
+ api: &LNBitsClient,
|
|
|
+ _is_active: &Arc<AtomicBool>,
|
|
|
+ ) -> Option<WaitPaymentResponse> {
|
|
|
+ let msg = msg_option?;
|
|
|
+
|
|
|
+ let payment = match api.get_payment_info(&msg).await {
|
|
|
+ Ok(payment) => payment,
|
|
|
+ Err(_) => return None,
|
|
|
+ };
|
|
|
+
|
|
|
+ if !payment.paid {
|
|
|
+ tracing::warn!(
|
|
|
+ "Received payment notification but payment not paid for {}",
|
|
|
+ msg
|
|
|
+ );
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+
|
|
|
+ Self::create_payment_response(&msg, &payment).unwrap_or_else(|e| {
|
|
|
+ tracing::error!("Failed to create payment response: {}", e);
|
|
|
+ None
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Create a payment response from payment info
|
|
|
+ fn create_payment_response(
|
|
|
+ msg: &str,
|
|
|
+ payment: &lnbits_rs::api::payment::Payment,
|
|
|
+ ) -> Result<Option<WaitPaymentResponse>, Error> {
|
|
|
+ let amount = payment.details.amount;
|
|
|
+
|
|
|
+ if amount == i64::MIN {
|
|
|
+ return Ok(None);
|
|
|
+ }
|
|
|
+
|
|
|
+ let hash = Self::decode_payment_hash(msg)?;
|
|
|
+
|
|
|
+ Ok(Some(WaitPaymentResponse {
|
|
|
+ payment_identifier: PaymentIdentifier::PaymentHash(hash),
|
|
|
+ payment_amount: Amount::from(amount.unsigned_abs()),
|
|
|
+ unit: CurrencyUnit::Msat,
|
|
|
+ payment_id: msg.to_string(),
|
|
|
+ }))
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Decode a hex payment hash string into a byte array
|
|
|
+ fn decode_payment_hash(hash_str: &str) -> Result<[u8; 32], Error> {
|
|
|
+ let decoded = hex::decode(hash_str)
|
|
|
+ .map_err(|e| Error::Anyhow(anyhow!("Failed to decode payment hash: {}", e)))?;
|
|
|
+
|
|
|
+ decoded
|
|
|
+ .try_into()
|
|
|
+ .map_err(|_| Error::Anyhow(anyhow!("Invalid payment hash length")))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
#[async_trait]
|
|
|
@@ -118,54 +172,14 @@ impl MintPayment for LNbits {
|
|
|
|
|
|
tokio::select! {
|
|
|
_ = cancel_token.cancelled() => {
|
|
|
- // Stream is cancelled
|
|
|
is_active.store(false, Ordering::SeqCst);
|
|
|
tracing::info!("Waiting for lnbits invoice ending");
|
|
|
None
|
|
|
}
|
|
|
msg_option = receiver.recv() => {
|
|
|
- match msg_option {
|
|
|
- Some(msg) => {
|
|
|
- let check = api.get_payment_info(&msg).await;
|
|
|
- match check {
|
|
|
- Ok(payment) => {
|
|
|
- if payment.paid {
|
|
|
- match hex::decode(msg.clone()) {
|
|
|
- Ok(decoded) => {
|
|
|
- match decoded.try_into() {
|
|
|
- Ok(hash) => {
|
|
|
- let response = WaitPaymentResponse {
|
|
|
- payment_identifier: PaymentIdentifier::PaymentHash(hash),
|
|
|
- payment_amount: Amount::from(payment.details.amount as u64),
|
|
|
- unit: CurrencyUnit::Msat,
|
|
|
- payment_id: msg.clone()
|
|
|
- };
|
|
|
- Some((response, (api, cancel_token, is_active)))
|
|
|
- },
|
|
|
- Err(e) => {
|
|
|
- tracing::error!("Failed to convert payment hash bytes to array: {:?}", e);
|
|
|
- None
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- Err(e) => {
|
|
|
- tracing::error!("Failed to decode payment hash hex string: {}", e);
|
|
|
- None
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- tracing::warn!("Received payment notification but could not check payment for {}", msg);
|
|
|
- None
|
|
|
- }
|
|
|
- },
|
|
|
- Err(_) => None
|
|
|
- }
|
|
|
- },
|
|
|
- None => {
|
|
|
- is_active.store(false, Ordering::SeqCst);
|
|
|
- None
|
|
|
- }
|
|
|
- }
|
|
|
+ Self::process_message(msg_option, &api, &is_active)
|
|
|
+ .await
|
|
|
+ .map(|response| (response, (api, cancel_token, is_active)))
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
@@ -306,7 +320,6 @@ impl MintPayment for LNbits {
|
|
|
memo: Some(description),
|
|
|
unit: unit.to_string(),
|
|
|
expiry,
|
|
|
- webhook: self.webhook_url.clone(),
|
|
|
internal: None,
|
|
|
out: false,
|
|
|
};
|
|
|
@@ -321,10 +334,8 @@ impl MintPayment for LNbits {
|
|
|
Self::Err::Anyhow(anyhow!("Could not create invoice"))
|
|
|
})?;
|
|
|
|
|
|
- let request: Bolt11Invoice = create_invoice_response
|
|
|
- .bolt11()
|
|
|
- .ok_or_else(|| Self::Err::Anyhow(anyhow!("Missing bolt11 invoice")))?
|
|
|
- .parse()?;
|
|
|
+ let request: Bolt11Invoice = create_invoice_response.bolt11().parse()?;
|
|
|
+
|
|
|
let expiry = request.expires_at().map(|t| t.as_secs());
|
|
|
|
|
|
Ok(CreateIncomingPaymentResponse {
|
|
|
@@ -389,7 +400,7 @@ impl MintPayment for LNbits {
|
|
|
let pay_response = MakePaymentResponse {
|
|
|
payment_lookup_id: payment_identifier.clone(),
|
|
|
payment_proof: payment.preimage,
|
|
|
- status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
|
|
|
+ status: lnbits_to_melt_status(&payment.details.status),
|
|
|
total_spent: Amount::from(
|
|
|
payment.details.amount.unsigned_abs() + payment.details.fee.unsigned_abs(),
|
|
|
),
|
|
|
@@ -400,11 +411,7 @@ impl MintPayment for LNbits {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-fn lnbits_to_melt_status(status: &str, pending: Option<bool>) -> MeltQuoteState {
|
|
|
- if pending.unwrap_or_default() {
|
|
|
- return MeltQuoteState::Pending;
|
|
|
- }
|
|
|
-
|
|
|
+fn lnbits_to_melt_status(status: &str) -> MeltQuoteState {
|
|
|
match status {
|
|
|
"success" => MeltQuoteState::Paid,
|
|
|
"failed" => MeltQuoteState::Unpaid,
|
|
|
@@ -412,15 +419,3 @@ fn lnbits_to_melt_status(status: &str, pending: Option<bool>) -> MeltQuoteState
|
|
|
_ => MeltQuoteState::Unknown,
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-impl LNbits {
|
|
|
- /// Create invoice webhook
|
|
|
- pub async fn create_invoice_webhook_router(
|
|
|
- &self,
|
|
|
- webhook_endpoint: &str,
|
|
|
- ) -> anyhow::Result<Router> {
|
|
|
- self.lnbits_api
|
|
|
- .create_invoice_webhook_router(webhook_endpoint)
|
|
|
- .await
|
|
|
- }
|
|
|
-}
|