use anyhow::Context;
use bitcoin::Amount;
use futures::StreamExt;
use lightning_invoice::Bolt11Invoice;
use log::{error, info, warn};
use server_rpc::protos;
use ark::lightning::{Bolt11InvoiceExt, Preimage, PaymentHash};
use crate::Wallet;
use crate::actions::DriveMode;
use crate::actions::lightning::receive::{
Htlcs, LightningReceive, LightningReceiveState, Progress,
ln_recv_action_id, start_lightning_receive
};
use crate::movement::MovementStatus;
use crate::movement::update::MovementUpdate;
impl Wallet {
pub async fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
let mut result = Vec::new();
for cp in self.inner.db.get_all_wallet_action_checkpoints().await? {
if let Some(recv) = cp.into_lightning_receive() {
result.push(recv);
}
}
Ok(result)
}
pub async fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
let mut total = Amount::ZERO;
for recv in self.pending_lightning_receives().await? {
let vtxo_ids = match &recv.progress {
Progress::AwaitingPayment => continue,
Progress::HtlcsReady(htlcs) => &htlcs.vtxo_ids,
Progress::PreimageRevealed(htlcs) => &htlcs.vtxo_ids,
Progress::Delivering(_) => continue,
};
for id in vtxo_ids {
total += self.get_vtxo_by_id(*id).await?.vtxo.amount();
}
}
Ok(total)
}
pub async fn sync_pending_lightning_receives(&self) -> anyhow::Result<()> {
let pending = self.pending_lightning_receives().await?;
if pending.is_empty() {
return Ok(());
}
info!("Syncing {} pending lightning receives", pending.len());
for recv in pending {
let id = recv.id();
if let Err(e) = self.drive_action(recv, DriveMode::UntilParkOrDone).await {
warn!("Failed to sync lightning receive {}: {:#}", id, e);
}
}
Ok(())
}
pub async fn lightning_receive_checkpoint(&self, hash: PaymentHash)
-> anyhow::Result<Option<LightningReceive>>
{
Ok(self.inner.db.get_wallet_action_checkpoint(&ln_recv_action_id(hash)).await?
.and_then(|cp| cp.into_lightning_receive()))
}
pub(crate) async fn lightning_receive_preimage(&self, hash: PaymentHash)
-> anyhow::Result<Option<Preimage>>
{
if let Some(settled) = self.inner.db.get_settled_lightning_receive(hash).await? {
return Ok(Some(settled.preimage));
}
if let Some(cp) = self.lightning_receive_checkpoint(hash).await? {
return Ok(Some(cp.payment_preimage));
}
Ok(None)
}
pub async fn lightning_receive_state(&self, hash: PaymentHash)
-> anyhow::Result<LightningReceiveState>
{
if let Some(settled) = self.inner.db.get_settled_lightning_receive(hash).await? {
return Ok(LightningReceiveState::Settled(settled));
}
if let Some(cp) = self.lightning_receive_checkpoint(hash).await? {
return Ok(LightningReceiveState::InProgress(cp));
}
bail!("no pending lightning receive found for this payment hash");
}
pub async fn bolt11_invoice(
&self,
amount: Amount,
description: Option<String>,
token: Option<String>,
) -> anyhow::Result<Bolt11Invoice> {
let start = start_lightning_receive(self, amount, description, token, None).await?;
self.inner.db.upsert_wallet_action_checkpoint(&start.id(), &start.clone().into()).await?;
Ok(start.invoice.clone())
}
pub async fn bolt11_invoice_for_address(
&self,
amount: Amount,
claim_destination: ark::Address,
description: Option<String>,
token: Option<String>,
) -> anyhow::Result<Bolt11Invoice> {
let start = start_lightning_receive(
self, amount, description, token, Some(claim_destination),
).await?;
self.inner.db.upsert_wallet_action_checkpoint(&start.id(), &start.clone().into()).await?;
Ok(start.invoice.clone())
}
pub async fn cancel_lightning_receive(&self, hash: PaymentHash) -> anyhow::Result<()> {
let key = ln_recv_action_id(hash);
let _guard = self.inner.lock_manager.try_lock(&key).await
.context("receive operation already in progress for this payment")?;
let recv = self.lightning_receive_checkpoint(hash).await?
.context("no pending lightning receive found for this payment hash")?;
match recv.progress {
Progress::AwaitingPayment => {
if let Ok((mut srv, _)) = self.require_server().await {
if let Err(e) = srv.client.cancel_lightning_receive(
protos::CancelLightningReceiveRequest { payment_hash: hash.to_vec() },
).await {
warn!("server did not cancel lightning receive {}: {}", hash, e);
}
}
self.stop_wallet_action(&key).await?;
Ok(())
},
Progress::HtlcsReady(_) => {
bail!("cannot cancel: HTLCs already granted; the receive will complete \
or be abandoned near expiry");
},
Progress::PreimageRevealed(_) | Progress::Delivering(_) => {
bail!("cannot cancel: preimage has already been revealed");
},
}
}
pub async fn attempt_lightning_receive_exit(
&self,
payment: impl Into<PaymentHash>,
) -> anyhow::Result<()> {
let hash = payment.into();
let key = ln_recv_action_id(hash);
let _guard = self.inner.lock_manager.try_lock(&key).await
.context("receive operation already in progress for this payment")?;
let recv = self.lightning_receive_checkpoint(hash).await?
.context("no pending lightning receive found for this payment hash")?;
let htlcs = match &recv.progress {
Progress::PreimageRevealed(htlcs) => htlcs,
_ => bail!("preimage must be revealed before attempting to exit"),
};
self.exit_lightning_receive_htlcs(&recv, htlcs).await?;
self.stop_wallet_action(&key).await?;
Ok(())
}
pub async fn exit_lightning_receive_htlcs(
&self,
recv: &LightningReceive,
htlcs: &Htlcs,
) -> anyhow::Result<()> {
warn!("Exiting HTLC VTXOs for lightning receive {}", recv.payment_hash);
let mut vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
for id in htlcs.vtxo_ids.iter() {
vtxos.push(self.get_vtxo_by_id(*id).await?.vtxo);
}
let vtxo_refs = vtxos.iter().collect::<Vec<_>>();
self.inner.exit.start_exit_for_vtxos(&vtxo_refs).await?;
self.inner.movements.finish_movement_with_update(
htlcs.movement_id,
MovementStatus::Failed,
MovementUpdate::new().exited_vtxos(vtxo_refs),
).await?;
let amount = recv.invoice.get_payment_amount(None).unwrap_or(Amount::ZERO);
self.inner.db.record_settled_lightning_receive(
recv.payment_hash, recv.payment_preimage, &recv.invoice, amount,
).await?;
Ok(())
}
pub async fn try_claim_lightning_receive(&self, hash: PaymentHash, wait: bool)
-> anyhow::Result<LightningReceiveState>
{
if let Some(recv) = self.lightning_receive_checkpoint(hash).await? {
let mode = if wait { DriveMode::UntilDone } else { DriveMode::UntilParkOrDone };
self.drive_action(recv, mode).await?;
}
self.lightning_receive_state(hash).await
}
pub async fn try_claim_all_lightning_receives(&self, wait: bool)
-> anyhow::Result<Vec<LightningReceiveState>>
{
let pending = self.pending_lightning_receives().await?;
let total = pending.len();
if total == 0 {
return Ok(vec![]);
}
let results: Vec<_> = tokio_stream::iter(pending)
.map(|rcv| async move {
self.try_claim_lightning_receive(rcv.invoice.into(), wait).await
})
.buffer_unordered(3)
.collect()
.await;
let mut claimed = vec![];
let mut failed = 0;
for result in results {
match result {
Ok(receive) => claimed.push(receive),
Err(e) => {
error!("Error claiming lightning receive: {:#}", e);
failed += 1;
}
}
}
if failed > 0 {
info!(
"Lightning receive claims: {} succeeded, {} failed out of {} pending",
claimed.len(), failed, total
);
}
if claimed.is_empty() {
anyhow::bail!("All {} lightning receive claim(s) failed", failed);
}
Ok(claimed)
}
}