use std::fmt;
use anyhow::Context;
use bitcoin::Amount;
use lightning::util::ser::Writeable;
use lnurllib::lightning_address::LightningAddress;
use log::{info, warn};
use server_rpc::protos;
use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, PaymentHash, Preimage};
use crate::Wallet;
use crate::WalletVtxo;
use crate::actions::DriveMode;
use crate::actions::lightning::pay::ln_pay_action_id;
use crate::actions::lightning::pay::{
Htlcs, LightningSend, LightningSendState, Progress, settle_lightning_send_payment,
start_lightning_send,
};
use crate::lightning::lnaddr_invoice;
use crate::movement::PaymentMethod;
impl Wallet {
pub async fn pending_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
let mut result = Vec::new();
for cp in self.inner.db.get_all_wallet_action_checkpoints().await? {
if let Some(ls) = cp.into_lightning_send() {
result.push(ls);
}
}
Ok(result)
}
pub async fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
let mut vtxos = Vec::new();
for send in self.pending_lightning_sends().await? {
let ids: Vec<_> = match &send.progress {
Progress::Start => send.input_vtxo_ids.clone(),
Progress::HtlcReceived(h) => h.vtxo_ids.clone(),
Progress::PaymentInitiated(h) => h.vtxo_ids.clone(),
Progress::RevocableHtlcs { htlcs, .. } => htlcs.vtxo_ids.clone(),
};
for id in ids {
vtxos.push(self.get_vtxo_by_id(id).await?);
}
}
Ok(vtxos)
}
pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
let pending = self.pending_lightning_sends().await?;
if pending.is_empty() {
return Ok(());
}
info!("Syncing {} pending lightning sends", pending.len());
for send in pending {
let id = send.id();
if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
warn!("Failed to sync lightning send {}: {:#}", id, e);
}
}
Ok(())
}
pub async fn lightning_send_checkpoint(&self, hash: PaymentHash)
-> anyhow::Result<Option<LightningSend>>
{
Ok(self.inner.db.get_wallet_action_checkpoint(&ln_pay_action_id(hash)).await?
.and_then(|cp| cp.into_lightning_send()))
}
pub async fn lightning_send_state(&self, hash: PaymentHash)
-> anyhow::Result<LightningSendState>
{
if let Some(paid) = self.inner.db.get_paid_invoice(hash).await? {
return Ok(LightningSendState::Paid(paid));
}
if let Some(cp) = self.lightning_send_checkpoint(hash).await? {
return Ok(LightningSendState::InProgress(cp));
}
Ok(LightningSendState::Unknown)
}
pub async fn is_invoice_paid(&self, hash: PaymentHash) -> anyhow::Result<bool> {
Ok(self.inner.db.get_paid_invoice(hash).await?.is_some())
}
pub async fn check_lightning_payment(&self, hash: PaymentHash, wait: bool)
-> anyhow::Result<LightningSendState>
{
let send = match self.lightning_send_state(hash).await? {
LightningSendState::InProgress(s) => s,
s => return Ok(s),
};
let mode = if wait { DriveMode::UntilDone } else { DriveMode::UntilParkOrDone };
self.drive_action(send, mode).await?;
self.lightning_send_state(hash).await
}
pub(crate) async fn settle_lightning_send_with_preimage(
&self,
send: LightningSend,
htlcs: Htlcs,
preimage: Preimage,
) -> anyhow::Result<()> {
let payment_hash = send.invoice.payment_hash();
if preimage.compute_payment_hash() != payment_hash {
bail!("preimage mismatch for payment hash {}", payment_hash);
}
settle_lightning_send_payment(self, &send, &htlcs, preimage).await?;
self.inner.db.remove_wallet_action_checkpoint(&ln_pay_action_id(payment_hash)).await?;
Ok(())
}
pub async fn pay_lightning_invoice<T>(
&self,
invoice: T,
user_amount: Option<Amount>,
wait: bool,
) -> anyhow::Result<Invoice>
where
T: TryInto<Invoice>,
T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
{
let invoice = invoice.try_into().context("failed to parse invoice")?;
let amount = invoice.get_payment_amount(user_amount)?;
info!("Sending bolt11 payment of {} to invoice {}", amount, invoice);
self.make_lightning_payment(&invoice, invoice.clone().into(), user_amount, wait).await?;
Ok(invoice)
}
pub async fn pay_lightning_address(
&self,
addr: &LightningAddress,
amount: Amount,
comment: Option<impl AsRef<str>>,
wait: bool,
) -> anyhow::Result<Invoice> {
let comment = comment.as_ref();
let invoice: Invoice = lnaddr_invoice(addr, amount, comment).await
.context("lightning address error")?.into();
info!("Sending {} to lightning address {}", amount, addr);
self.make_lightning_payment(&invoice, addr.clone().into(), None, wait).await?;
info!("Paid invoice {}", invoice);
Ok(invoice)
}
pub async fn pay_lightning_offer(
&self,
offer: Offer,
user_amount: Option<Amount>,
wait: bool,
) -> anyhow::Result<Invoice> {
let (mut srv, _) = self.require_server().await?;
let offer_bytes = {
let mut bytes = Vec::new();
offer.write(&mut bytes).context("failed to serialize BOLT12 offer")?;
bytes
};
let req = protos::FetchBolt12InvoiceRequest {
offer: offer_bytes,
amount_sat: user_amount.map(|a| a.to_sat()),
};
if let Some(amt) = user_amount {
info!("Sending bolt12 payment of {} (user amount) to offer {}", amt, offer);
} else if let Some(amt) = offer.amount() {
info!("Sending bolt12 payment of {:?} (invoice amount) to offer {}", amt, offer);
} else {
warn!("Paying offer without amount nor user amount provided: {}", offer);
}
let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
let invoice = Bolt12Invoice::try_from(resp.invoice)
.map_err(|e| anyhow!("invalid invoice: {:?}", e))?;
invoice.validate_issuance(&offer)
.context("invalid BOLT12 invoice received from offer")?;
let invoice: Invoice = invoice.into();
self.make_lightning_payment(&invoice, offer.into(), None, wait).await?;
info!("Paid invoice: {}", invoice);
Ok(invoice)
}
pub async fn make_lightning_payment(
&self,
invoice: &Invoice,
original_payment_method: PaymentMethod,
user_amount: Option<Amount>,
wait: bool,
) -> anyhow::Result<()> {
if !original_payment_method.is_lightning() && !original_payment_method.is_custom() {
bail!("Invalid original payment method for lightning payment");
}
let payment_hash = invoice.payment_hash();
let mode = if wait { DriveMode::UntilDone } else { DriveMode::UntilParkOrDone };
if self.is_invoice_paid(payment_hash).await? {
bail!("Invoice has already been paid");
}
let key = ln_pay_action_id(payment_hash);
let guard = self.inner.lock_manager.try_lock(&key).await
.context("Payment operation already in progress for this invoice")?;
let action = match self.lightning_send_checkpoint(payment_hash).await? {
Some(existing) => existing,
None => {
let start = start_lightning_send(
self, invoice.clone(), user_amount, original_payment_method,
).await?;
self.inner.db.upsert_wallet_action_checkpoint(
&start.id(), &start.clone().into()
).await?;
start
},
};
self.drive_action_with_guard(action, mode, guard).await
}
}