use std::fmt;
use anyhow::Context;
use bitcoin::{Amount, SignedAmount};
use bitcoin::hex::DisplayHex;
use lightning::util::ser::Writeable;
use lnurllib::lightning_address::LightningAddress;
use log::{debug, error, info, trace, warn};
use server_rpc::protos::{self, lightning_payment_status::PaymentStatus};
use ark::{musig, VtxoPolicy};
use ark::arkoor::ArkoorDestination;
use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, PaymentHash, Preimage};
use ark::util::IteratorExt;
use bitcoin_ext::BlockHeight;
use crate::{Wallet, WalletVtxo};
use crate::lightning::lnaddr_invoice;
use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
use crate::movement::update::MovementUpdate;
use crate::persist::models::LightningSend;
use crate::subsystem::{LightningMovement, LightningSendMovement, Subsystem};
impl Wallet {
pub async fn pending_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
Ok(self.db.get_all_pending_lightning_send().await?)
}
pub async fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
let vtxos = self.db.get_all_pending_lightning_send().await?.into_iter()
.flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
.collect::<Vec<_>>();
Ok(vtxos)
}
pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
let pending_payments = self.pending_lightning_sends().await?;
if pending_payments.is_empty() {
return Ok(());
}
info!("Syncing {} pending lightning sends", pending_payments.len());
for payment in pending_payments {
let payment_hash = payment.invoice.payment_hash();
self.check_lightning_payment(payment_hash, false).await?;
}
Ok(())
}
async fn process_lightning_revocation(&self, payment: &LightningSend) -> anyhow::Result<()> {
let (mut srv, _) = self.require_server().await?;
let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
.map(|v| v.vtxo).collect::<Vec<_>>();
debug!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
let mut secs = Vec::with_capacity(htlc_vtxos.len());
let mut pubs = Vec::with_capacity(htlc_vtxos.len());
let mut htlc_keypairs = Vec::with_capacity(htlc_vtxos.len());
for input in htlc_vtxos.iter() {
let keypair = self.get_vtxo_key(input).await?;
let (s, p) = musig::nonce_pair(&keypair);
secs.push(s);
pubs.push(p);
htlc_keypairs.push(keypair);
}
let (revocation_keypair, _) = self.derive_store_next_keypair().await?;
let revocation_claim_policy = VtxoPolicy::new_pubkey(revocation_keypair.public_key());
let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
htlc_vtxos.iter().cloned(),
revocation_claim_policy,
)
.context("Failed to construct arkoor package")?
.generate_user_nonces(&htlc_keypairs)?;
let cosign_request = protos::ArkoorPackageCosignRequest::from(
builder.cosign_request(),
);
let response = srv.client
.request_lightning_pay_htlc_revocation(cosign_request).await
.context("server failed to cosign arkoor")?.into_inner();
let cosign_resp = ArkoorPackageCosignResponse::try_from(response)
.context("Failed to parse cosign response from server")?;
let vtxos = builder
.user_cosign(&htlc_keypairs, cosign_resp)
.context("Failed to cosign vtxos")?
.build_signed_vtxos();
let mut revoked = Amount::ZERO;
for vtxo in &vtxos {
debug!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
revoked += vtxo.amount();
}
let count = vtxos.len();
let effective = -payment.amount.to_signed()? - payment.fee.to_signed()? + revoked.to_signed()?;
if effective != SignedAmount::ZERO {
warn!("Movement {} should have fee of zero, but got {}: amount = {}, fee = {}, revoked = {}",
payment.movement_id, effective, payment.amount, payment.fee, revoked,
);
}
self.movements.finish_movement_with_update(
payment.movement_id,
MovementStatus::Failed,
MovementUpdate::new()
.effective_balance(effective)
.fee(effective.unsigned_abs())
.produced_vtxos(&vtxos)
).await?;
self.store_spendable_vtxos(&vtxos).await?;
self.mark_vtxos_as_spent(&htlc_vtxos).await?;
self.db.remove_lightning_send(payment.invoice.payment_hash()).await?;
debug!("Revoked {} HTLC VTXOs", count);
Ok(())
}
async fn process_lightning_send_server_preimage(
&self,
preimage: Option<Vec<u8>>,
payment: &LightningSend,
) -> anyhow::Result<Option<Preimage>> {
let payment_hash = payment.invoice.payment_hash();
let preimage_res = preimage
.context("preimage is missing")
.map(|p| Ok(Preimage::try_from(p)?))
.flatten();
match preimage_res {
Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
info!("Lightning payment succeeded! Preimage: {}. Payment hash: {}",
preimage.as_hex(), payment.invoice.payment_hash().as_hex());
self.db.finish_lightning_send(payment_hash, Some(preimage)).await?;
self.mark_vtxos_as_spent(&payment.htlc_vtxos).await?;
self.movements.finish_movement_with_update(
payment.movement_id,
MovementStatus::Successful,
MovementUpdate::new().metadata([(
"payment_preimage".into(),
serde_json::to_value(preimage).expect("payment preimage can serde"),
)])
).await?;
Ok(Some(preimage))
},
_ => {
error!("Server failed to provide a valid preimage. \
Payment hash: {}. Preimage result: {:#?}", payment_hash, preimage_res
);
Ok(None)
}
}
}
pub async fn check_lightning_payment(&self, payment_hash: PaymentHash, wait: bool)
-> anyhow::Result<Option<LightningSend>>
{
trace!("Checking lightning payment status for payment hash: {}", payment_hash);
{
let mut inflight = self.inflight_lightning_payments.lock().await;
if !inflight.insert(payment_hash) {
bail!("Payment operation already in progress for this invoice");
}
}
let result = self.check_lightning_payment_inner(payment_hash, wait).await;
{
let mut inflight = self.inflight_lightning_payments.lock().await;
inflight.remove(&payment_hash);
}
result
}
async fn check_lightning_payment_inner(&self, payment_hash: PaymentHash, wait: bool)
-> anyhow::Result<Option<LightningSend>>
{
let (mut srv, _) = self.require_server().await?;
let payment = self.db.get_lightning_send(payment_hash).await?
.context("no lightning send found for payment hash")?;
if payment.preimage.is_some() {
trace!("Payment already completed with preimage");
return Ok(Some(payment));
}
if payment.htlc_vtxos.is_empty() {
bail!("No HTLC VTXOs found for payment");
}
let policy = payment.htlc_vtxos.iter()
.all_same(|v| v.vtxo.policy())
.ok_or(anyhow::anyhow!("All lightning htlc should have the same policy"))?;
let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
if policy.payment_hash != payment_hash {
bail!("Payment hash mismatch");
}
let req = protos::CheckLightningPaymentRequest {
hash: payment_hash.to_vec(),
wait,
};
let response = srv.client.check_lightning_payment(req).await
.map(|r| r.into_inner().payment_status);
let tip = self.chain.tip().await?;
let min_vtxo_expiry = payment.htlc_vtxos.iter()
.map(|v| v.vtxo.expiry_height())
.min().context("no HTLC VTXOs for expiry check")?;
let expired = tip > policy.htlc_expiry
|| tip > min_vtxo_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold);
let should_revoke = match response {
Ok(Some(PaymentStatus::Success(status))) => {
let preimage_opt = self.process_lightning_send_server_preimage(
Some(status.preimage), &payment,
).await?;
if preimage_opt.is_some() {
let updated_payment = self.db.get_lightning_send(payment_hash).await?
.context("payment disappeared from database")?;
return Ok(Some(updated_payment));
} else {
trace!("Server said payment is complete, but has no valid preimage: {:?}", preimage_opt);
expired
}
},
Ok(Some(PaymentStatus::Failed(_))) => {
info!("Payment failed, revoking VTXO");
true
},
Ok(Some(PaymentStatus::Pending(_))) => {
trace!("Payment is still pending");
expired
},
Ok(None) | Err(_) => expired,
};
if should_revoke {
debug!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
payment_hash, tip, policy.htlc_expiry);
if let Err(e) = self.process_lightning_revocation(&payment).await {
warn!("Failed to revoke VTXO: {}", e);
if tip > min_vtxo_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
warn!("HTLC VTXOs for payment {} are near VTXO expiry, marking to exit", payment_hash);
let vtxos = payment.htlc_vtxos
.iter()
.map(|v| v.vtxo.clone())
.collect::<Vec<_>>();
self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
let effective = -payment.amount.to_signed()? - payment.fee.to_signed()? + exited.to_signed()?;
if effective != SignedAmount::ZERO {
warn!("Movement {} should have fee of zero, but got {}: amount = {}, fee = {}, exited = {}",
payment.movement_id, effective, payment.amount, payment.fee, exited,
);
}
self.movements.finish_movement_with_update(
payment.movement_id,
MovementStatus::Failed,
MovementUpdate::new()
.effective_balance(effective)
.fee(effective.unsigned_abs())
.exited_vtxos(&vtxos)
).await?;
self.db.finish_lightning_send(payment.invoice.payment_hash(), None).await?;
}
return Err(e)
}
}
Ok(self.db.get_lightning_send(payment_hash).await?)
}
pub async fn pay_lightning_invoice<T>(
&self,
invoice: T,
user_amount: Option<Amount>,
) -> anyhow::Result<LightningSend>
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).await
}
pub async fn pay_lightning_address(
&self,
addr: &LightningAddress,
amount: Amount,
comment: Option<impl AsRef<str>>,
) -> anyhow::Result<LightningSend> {
let comment = comment.as_ref();
let invoice = lnaddr_invoice(addr, amount, comment).await
.context("lightning address error")?;
info!("Sending {} to lightning address {}", amount, addr);
let ret = self.make_lightning_payment(&invoice.into(), addr.clone().into(), None).await
.context("bolt11 payment error")?;
info!("Paid invoice {}", ret.invoice);
Ok(ret)
}
pub async fn pay_lightning_offer(
&self,
offer: Offer,
user_amount: Option<Amount>,
) -> anyhow::Result<LightningSend> {
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 ret = self.make_lightning_payment(&invoice.into(), offer.into(), None).await
.context("bolt12 payment error")?;
info!("Paid invoice: {}", ret.invoice.to_string());
Ok(ret)
}
pub async fn make_lightning_payment(
&self,
invoice: &Invoice,
original_payment_method: PaymentMethod,
user_amount: Option<Amount>,
) -> anyhow::Result<LightningSend> {
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 mut inflight = self.inflight_lightning_payments.lock().await;
if !inflight.insert(payment_hash) {
bail!("Payment already in progress for this invoice");
}
}
let result = self.make_lightning_payment_inner(
invoice, original_payment_method, user_amount, payment_hash
).await;
{
let mut inflight = self.inflight_lightning_payments.lock().await;
inflight.remove(&payment_hash);
}
result
}
async fn make_lightning_payment_inner(
&self,
invoice: &Invoice,
original_payment_method: PaymentMethod,
user_amount: Option<Amount>,
payment_hash: PaymentHash,
) -> anyhow::Result<LightningSend> {
let (mut srv, ark_info) = self.require_server().await?;
let tip = self.chain.tip().await?;
let properties = self.db.read_properties().await?.context("Missing config")?;
if invoice.network() != properties.network {
bail!("Invoice is for wrong network: {}", invoice.network());
}
let lightning_send = self.db.get_lightning_send(payment_hash).await?;
if lightning_send.is_some() {
bail!("Invoice has already been paid");
}
invoice.check_signature()?;
let payment_amount = invoice.get_payment_amount(user_amount)?;
if payment_amount == Amount::ZERO {
bail!("Cannot pay invoice for 0 sats (0 sat invoices are not any-amount invoices)");
}
let (change_keypair, _) = self.derive_store_next_keypair().await?;
let (inputs, fee) = self.select_vtxos_to_cover_with_fee(
payment_amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
).await.context("Could not find enough suitable VTXOs to cover lightning payment")?;
let total_amount = payment_amount + fee;
let mut secs = Vec::with_capacity(inputs.len());
let mut pubs = Vec::with_capacity(inputs.len());
let mut input_keypairs = Vec::with_capacity(inputs.len());
let mut input_ids = Vec::with_capacity(inputs.len());
for input in inputs.iter() {
let keypair = self.get_vtxo_key(input).await?;
let (s, p) = musig::nonce_pair(&keypair);
secs.push(s);
pubs.push(p);
input_keypairs.push(keypair);
input_ids.push(input.id());
}
let expiry = tip + ark_info.htlc_send_expiry_delta as BlockHeight;
let policy = VtxoPolicy::new_server_htlc_send(
change_keypair.public_key(), invoice.payment_hash(), expiry,
);
let input_amount = inputs.iter().map(|v| v.amount()).sum::<Amount>();
let pay_dest = ArkoorDestination { total_amount, policy };
let outputs = if input_amount == total_amount {
vec![pay_dest]
} else {
let change_dest = ArkoorDestination {
total_amount: input_amount - total_amount,
policy: VtxoPolicy::new_pubkey(change_keypair.public_key()),
};
vec![pay_dest, change_dest]
};
let builder = ArkoorPackageBuilder::new_with_checkpoints(
inputs.iter().map(|v| &v.vtxo).cloned(),
outputs,
)
.context("Failed to construct arkoor package")?
.generate_user_nonces(&input_keypairs)
.context("invalid nb of keypairs")?;
let package_cosign_request = protos::ArkoorPackageCosignRequest::from(
builder.cosign_request(),
);
let cosign_request = protos::LightningPayHtlcCosignRequest {
parts: package_cosign_request.parts,
};
let response = srv.client.request_lightning_pay_htlc_cosign(cosign_request).await
.context("htlc request failed")?.into_inner();
let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
.context("Failed to parse cosign response from server")?;
let vtxos = builder
.user_cosign(&input_keypairs, cosign_responses)
.context("Failed to cosign vtxos")?
.build_signed_vtxos();
let (htlc_vtxos, change_vtxos) = vtxos.into_iter()
.partition::<Vec<_>, _>(|v| matches!(v.policy(), VtxoPolicy::ServerHtlcSend(_)));
let mut effective_balance = Amount::ZERO;
for vtxo in &htlc_vtxos {
self.validate_vtxo(vtxo).await?;
effective_balance += vtxo.amount();
}
let movement_id = self.movements.new_movement_with_update(
Subsystem::LIGHTNING_SEND,
LightningSendMovement::Send.to_string(),
MovementUpdate::new()
.intended_balance(-payment_amount.to_signed()?)
.effective_balance(-effective_balance.to_signed()?)
.fee(fee)
.consumed_vtxos(&inputs)
.sent_to([MovementDestination::new(original_payment_method, payment_amount)])
.metadata(LightningMovement::metadata(invoice.payment_hash(), &htlc_vtxos, None))
).await?;
self.store_locked_vtxos(&htlc_vtxos, Some(movement_id)).await?;
self.mark_vtxos_as_spent(&input_ids).await?;
for change in &change_vtxos {
let last_input = inputs.last().context("no inputs provided")?;
let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
let tx = tx.with_context(|| {
format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
})?;
change.validate(&tx).context("invalid lightning change vtxo")?;
self.store_spendable_vtxos([change]).await?;
}
self.movements.update_movement(
movement_id,
MovementUpdate::new()
.produced_vtxos(change_vtxos)
.metadata(LightningMovement::metadata(invoice.payment_hash(), &htlc_vtxos, None))
).await?;
let lightning_send = self.db.store_new_pending_lightning_send(
&invoice,
payment_amount,
fee,
&htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(),
movement_id,
).await?;
self.register_vtxos_with_server(&htlc_vtxos).await?;
let req = protos::InitiateLightningPaymentRequest {
invoice: invoice.to_string(),
htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
payment_amount_sat: payment_amount.to_sat(),
};
srv.client.initiate_lightning_payment(req).await?;
Ok(lightning_send)
}
}