pub mod fees;
#[cfg(feature = "onchain-bdk")]
pub mod onchain;
use std::borrow::Borrow;
use std::time::Duration;
use bitcoin::secp256k1::{schnorr, PublicKey};
use bitcoin::{Amount, Txid};
#[cfg(feature = "utoipa")]
use utoipa::ToSchema;
use ark::VtxoId;
use ark::lightning::{PaymentHash, Preimage};
use bitcoin_ext::{AmountExt, BlockDelta};
use bark::actions::lightning::pay::{LightningSendState, Progress as SendProgress};
use bark::actions::lightning::receive::{
LightningReceive, LightningReceiveState, Progress as ReceiveProgress,
};
use crate::cli::fees::FeeSchedule;
use crate::exit::error::ExitError;
use crate::exit::package::ExitTransactionPackage;
use crate::exit::ExitState;
use crate::primitives::{TransactionInfo, WalletVtxoInfo};
use crate::serde_utils;
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct ArkInfo {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub network: bitcoin::Network,
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub server_pubkey: PublicKey,
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub mailbox_pubkey: PublicKey,
#[serde(with = "serde_utils::duration")]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub round_interval: Duration,
pub nb_round_nonces: usize,
pub vtxo_exit_delta: BlockDelta,
#[serde(default)]
pub vtxo_lifetime: BlockDelta,
pub htlc_send_expiry_delta: BlockDelta,
pub htlc_expiry_delta: BlockDelta,
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub max_vtxo_amount: Option<Amount>,
pub required_board_confirmations: usize,
pub max_user_invoice_cltv_delta: u16,
#[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub min_board_amount: Amount,
pub offboard_feerate_sat_per_kvb: u64,
pub ln_receive_anti_dos_required: bool,
pub fees: FeeSchedule,
pub max_vtxo_exit_depth: u16,
pub tos_link: Option<String>,
pub max_offboard_inputs: usize,
#[deprecated(note = "renamed to `vtxo_lifetime`")]
#[serde(default)]
#[cfg_attr(feature = "utoipa", schema(required = true))]
pub vtxo_expiry_delta: BlockDelta,
}
impl<'de> serde::Deserialize<'de> for ArkInfo {
#[allow(deprecated)]
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct ArkInfoStub {
network: bitcoin::Network,
server_pubkey: PublicKey,
mailbox_pubkey: PublicKey,
#[serde(with = "serde_utils::duration")]
round_interval: Duration,
nb_round_nonces: usize,
vtxo_exit_delta: BlockDelta,
#[serde(default)]
vtxo_lifetime: BlockDelta,
htlc_send_expiry_delta: BlockDelta,
htlc_expiry_delta: BlockDelta,
max_vtxo_amount: Option<Amount>,
required_board_confirmations: usize,
max_user_invoice_cltv_delta: u16,
#[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
min_board_amount: Amount,
offboard_feerate_sat_per_kvb: u64,
ln_receive_anti_dos_required: bool,
fees: FeeSchedule,
max_vtxo_exit_depth: u16,
tos_link: Option<String>,
max_offboard_inputs: usize,
#[serde(default)]
vtxo_expiry_delta: BlockDelta,
}
let v = ArkInfoStub::deserialize(d)?;
let vtxo_lifetime = match (v.vtxo_lifetime, v.vtxo_expiry_delta) {
(0, expiry) => expiry,
(lifetime, 0) => lifetime,
(lifetime, expiry) if lifetime == expiry => lifetime,
(lifetime, expiry) => return Err(serde::de::Error::custom(format!(
"vtxo_lifetime ({}) and vtxo_expiry_delta ({}) don't match", lifetime, expiry,
))),
};
Ok(ArkInfo {
network: v.network,
server_pubkey: v.server_pubkey,
mailbox_pubkey: v.mailbox_pubkey,
round_interval: v.round_interval,
nb_round_nonces: v.nb_round_nonces,
vtxo_exit_delta: v.vtxo_exit_delta,
vtxo_lifetime: vtxo_lifetime,
vtxo_expiry_delta: vtxo_lifetime,
htlc_send_expiry_delta: v.htlc_send_expiry_delta,
htlc_expiry_delta: v.htlc_expiry_delta,
max_vtxo_amount: v.max_vtxo_amount,
required_board_confirmations: v.required_board_confirmations,
max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
min_board_amount: v.min_board_amount,
offboard_feerate_sat_per_kvb: v.offboard_feerate_sat_per_kvb,
ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
fees: v.fees,
max_vtxo_exit_depth: v.max_vtxo_exit_depth,
tos_link: v.tos_link,
max_offboard_inputs: v.max_offboard_inputs,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct NextRoundStart {
pub start_time: chrono::DateTime<chrono::Local>,
}
impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
#[allow(deprecated)] fn from(v: T) -> Self {
let v = v.borrow();
ArkInfo {
network: v.network,
server_pubkey: v.server_pubkey,
mailbox_pubkey: v.mailbox_pubkey,
round_interval: v.round_interval,
nb_round_nonces: v.nb_round_nonces,
vtxo_exit_delta: v.vtxo_exit_delta,
vtxo_lifetime: v.vtxo_lifetime,
vtxo_expiry_delta: v.vtxo_lifetime,
htlc_send_expiry_delta: v.htlc_send_expiry_delta,
htlc_expiry_delta: v.htlc_expiry_delta,
max_vtxo_amount: v.max_vtxo_amount,
required_board_confirmations: v.required_board_confirmations,
max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
min_board_amount: v.min_board_amount,
offboard_feerate_sat_per_kvb: v.offboard_feerate.to_sat_per_kwu() * 4,
ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
fees: v.fees.clone().into(),
max_vtxo_exit_depth: v.max_vtxo_exit_depth,
max_offboard_inputs: v.max_offboard_inputs,
tos_link: v.tos_link.clone(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct SignedMessage {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub signature: schnorr::Signature,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct MessageVerification {
pub valid: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct Balance {
#[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub spendable: Amount,
#[serde(rename = "pending_lightning_send_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub pending_lightning_send: Amount,
#[serde(rename = "claimable_lightning_receive_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub claimable_lightning_receive: Amount,
#[serde(rename = "pending_in_round_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub pending_in_round: Amount,
#[serde(rename = "pending_board_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub pending_board: Amount,
#[serde(
default,
rename = "pending_exit_sat",
with = "bitcoin::amount::serde::as_sat::opt",
skip_serializing_if = "Option::is_none",
)]
#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
pub pending_exit: Option<Amount>,
}
impl From<bark::Balance> for Balance {
fn from(v: bark::Balance) -> Self {
Balance {
spendable: v.spendable,
pending_in_round: v.pending_in_round,
pending_lightning_send: v.pending_lightning_send,
claimable_lightning_receive: v.claimable_lightning_receive,
pending_exit: v.pending_exit,
pending_board: v.pending_board,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct ExitProgressResponse {
pub exits: Vec<ExitProgressStatus>,
pub done: bool,
pub claimable_height: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<ExitError>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct ExitProgressStatus {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub vtxo_id: VtxoId,
pub state: ExitState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<ExitError>,
}
impl From<bark::exit::ExitProgressStatus> for ExitProgressStatus {
fn from(v: bark::exit::ExitProgressStatus) -> Self {
ExitProgressStatus {
vtxo_id: v.vtxo_id,
state: v.state.into(),
error: v.error.map(ExitError::from),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct ExitTransactionStatus {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub vtxo_id: VtxoId,
pub state: ExitState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub history: Option<Vec<ExitState>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub transactions: Vec<ExitTransactionPackage>,
}
impl From<bark::exit::ExitTransactionStatus> for ExitTransactionStatus {
fn from(v: bark::exit::ExitTransactionStatus) -> Self {
ExitTransactionStatus {
vtxo_id: v.vtxo_id,
state: v.state.into(),
history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct PendingBoardInfo {
pub funding_tx: TransactionInfo,
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
pub vtxos: Vec<VtxoId>,
#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub amount: Amount,
pub movement_id: u32,
}
impl From<bark::persist::models::PendingBoard> for PendingBoardInfo {
fn from(v: bark::persist::models::PendingBoard) -> Self {
PendingBoardInfo {
funding_tx: v.funding_tx.into(),
vtxos: v.vtxos,
amount: v.amount,
movement_id: v.movement_id.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub enum RoundStatus {
SyncError {
error: String,
},
Confirmed {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
funding_txid: Txid,
},
Unconfirmed {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
funding_txid: Txid,
},
Pending,
Failed {
error: String,
},
Canceled,
}
impl RoundStatus {
pub fn is_final(&self) -> bool {
match self {
Self::SyncError { .. } => false,
Self::Confirmed { .. } => true,
Self::Unconfirmed { .. } => false,
Self::Pending { .. } => false,
Self::Failed { .. } => true,
Self::Canceled => true,
}
}
pub fn is_success(&self) -> bool {
match self {
Self::SyncError { .. } => false,
Self::Confirmed { .. } => true,
Self::Unconfirmed { .. } => true,
Self::Pending { .. } => false,
Self::Failed { .. } => false,
Self::Canceled => false,
}
}
}
impl From<bark::round::RoundStatus> for RoundStatus {
fn from(s: bark::round::RoundStatus) -> Self {
match s {
bark::round::RoundStatus::Confirmed { funding_txid } => {
Self::Confirmed { funding_txid }
},
bark::round::RoundStatus::Unconfirmed { funding_txid } => {
Self::Unconfirmed { funding_txid }
},
bark::round::RoundStatus::Pending => Self::Pending,
bark::round::RoundStatus::Failed { error } => Self::Failed { error },
bark::round::RoundStatus::Canceled => Self::Canceled,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct RoundStateInfo {
pub round_state_id: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct InvoiceInfo {
pub invoice: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct OffboardResult {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub offboard_txid: Txid,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct LightningReceiveInfo {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub payment_hash: PaymentHash,
pub state: String,
pub invoice: String,
#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
pub payment_preimage: Option<Preimage>,
#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub amount: Amount,
#[serde(default, deserialize_with = "serde_utils::null_as_default")]
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>, required = true))]
pub htlc_vtxo_ids: Vec<VtxoId>,
pub settled_at: Option<chrono::DateTime<chrono::Local>>,
#[deprecated(note = "no longer tracked; use `state` and `settled_at`")]
#[serde(default)]
pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
#[deprecated(note = "renamed to `settled_at`")]
#[serde(default)]
pub finished_at: Option<chrono::DateTime<chrono::Local>>,
#[deprecated(note = "replaced by `htlc_vtxo_ids`")]
#[serde(default, deserialize_with = "serde_utils::null_as_default")]
#[cfg_attr(feature = "utoipa", schema(required = true))]
pub htlc_vtxos: Vec<WalletVtxoInfo>,
}
impl LightningReceiveInfo {
#[allow(deprecated)] pub fn from_state(state: &LightningReceiveState) -> Self {
match state {
LightningReceiveState::InProgress(recv) => LightningReceiveInfo::from(recv),
LightningReceiveState::Settled(s) => LightningReceiveInfo {
payment_hash: s.payment_hash,
state: "settled".to_string(),
invoice: s.invoice.to_string(),
payment_preimage: Some(s.preimage),
amount: s.amount,
htlc_vtxo_ids: vec![],
settled_at: Some(s.settled_at),
preimage_revealed_at: None,
finished_at: Some(s.settled_at),
htlc_vtxos: vec![],
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct LightningSendInfo {
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub payment_hash: PaymentHash,
pub state: String,
pub invoice: Option<String>,
#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
pub preimage: Option<Preimage>,
}
impl LightningSendInfo {
pub fn from_state(hash: PaymentHash, state: &LightningSendState) -> Self {
match state {
LightningSendState::Unknown => LightningSendInfo {
payment_hash: hash,
state: "unknown".to_string(),
invoice: None,
preimage: None,
},
LightningSendState::Paid(paid) => LightningSendInfo {
payment_hash: paid.payment_hash,
state: "paid".to_string(),
invoice: None,
preimage: Some(paid.preimage),
},
LightningSendState::InProgress(send) => {
let phase = match send.progress {
SendProgress::Start => "start",
SendProgress::HtlcReceived(_) => "htlc-received",
SendProgress::PaymentInitiated(_) => "payment-initiated",
SendProgress::RevocableHtlcs { .. } => "revocable-htlcs",
SendProgress::RevocationStuck { .. } => "revocation-stuck",
};
LightningSendInfo {
payment_hash: send.invoice.payment_hash(),
state: phase.to_string(),
invoice: Some(send.invoice.to_string()),
preimage: None,
}
},
}
}
}
impl From<&LightningReceive> for LightningReceiveInfo {
#[allow(deprecated)] fn from(recv: &LightningReceive) -> Self {
let (state, htlc_vtxo_ids) = match &recv.progress {
ReceiveProgress::AwaitingPayment => ("awaiting-payment", vec![]),
ReceiveProgress::HtlcsReady(htlcs) => ("htlcs-ready", htlcs.vtxo_ids.clone()),
ReceiveProgress::PreimageRevealed(htlcs) => ("preimage-revealed", htlcs.vtxo_ids.clone()),
ReceiveProgress::Delivering(_) => ("delivering", vec![]),
};
LightningReceiveInfo {
payment_hash: recv.payment_hash,
state: state.to_string(),
invoice: recv.invoice.to_string(),
payment_preimage: Some(recv.payment_preimage),
amount: recv.invoice.amount_milli_satoshis()
.map(Amount::from_msat_floor)
.expect("generated invoice with no amount"),
htlc_vtxo_ids,
settled_at: None,
preimage_revealed_at: None,
finished_at: None,
htlc_vtxos: vec![],
}
}
}
#[cfg(test)]
mod test {
use bitcoin::FeeRate;
use super::*;
fn lightning_receive_base_json() -> serde_json::Value {
serde_json::json!({
"amount_sat": 1000,
"payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
"state": "awaiting-payment",
"settled_at": null,
"invoice": "lnbc1",
})
}
#[test]
fn deserialize_lightning_receive_htlc_vtxo_ids_missing() {
let json = lightning_receive_base_json();
serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
}
#[test]
fn deserialize_lightning_receive_htlc_vtxo_ids_null() {
let mut json = lightning_receive_base_json();
json["htlc_vtxo_ids"] = serde_json::json!(null);
serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
}
#[test]
fn deserialize_lightning_receive_htlc_vtxo_ids_empty() {
let mut json = lightning_receive_base_json();
json["htlc_vtxo_ids"] = serde_json::json!([]);
serde_json::from_value::<LightningReceiveInfo>(json).unwrap();
}
#[allow(deprecated)]
fn ark_info_base() -> ArkInfo {
let pubkey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
.parse::<PublicKey>().unwrap();
ArkInfo {
network: bitcoin::Network::Regtest,
server_pubkey: pubkey,
mailbox_pubkey: pubkey,
round_interval: Duration::from_secs(60),
nb_round_nonces: 1,
vtxo_exit_delta: 12,
vtxo_lifetime: 100,
vtxo_expiry_delta: 100,
htlc_send_expiry_delta: 100,
htlc_expiry_delta: 6,
max_vtxo_amount: None,
required_board_confirmations: 3,
max_user_invoice_cltv_delta: 100,
min_board_amount: Amount::from_sat(1000),
offboard_feerate_sat_per_kvb: 1000,
ln_receive_anti_dos_required: false,
fees: ark::fees::FeeSchedule::default().into(),
max_vtxo_exit_depth: 10,
tos_link: None,
max_offboard_inputs: 4,
}
}
#[test]
#[allow(deprecated)]
fn ark_info_vtxo_lifetime_falls_back_to_deprecated_field() {
let mut json = serde_json::to_value(ark_info_base()).unwrap();
json.as_object_mut().unwrap().remove("vtxo_lifetime");
json["vtxo_expiry_delta"] = serde_json::json!(42);
let info = serde_json::from_value::<ArkInfo>(json).unwrap();
assert_eq!(info.vtxo_lifetime, 42);
assert_eq!(info.vtxo_expiry_delta, 42);
}
#[test]
#[allow(deprecated)]
fn ark_info_vtxo_lifetime_kept_in_sync() {
let mut json = serde_json::to_value(ark_info_base()).unwrap();
json["vtxo_lifetime"] = serde_json::json!(42);
json["vtxo_expiry_delta"] = serde_json::json!(42);
let info = serde_json::from_value::<ArkInfo>(json).unwrap();
assert_eq!(info.vtxo_lifetime, 42);
assert_eq!(info.vtxo_expiry_delta, 42);
let json = serde_json::to_value(&info).unwrap();
assert_eq!(json["vtxo_lifetime"], 42);
assert_eq!(json["vtxo_expiry_delta"], 42);
}
#[test]
fn ark_info_vtxo_lifetime_rejects_diverging_fields() {
let mut json = serde_json::to_value(ark_info_base()).unwrap();
json["vtxo_lifetime"] = serde_json::json!(42);
json["vtxo_expiry_delta"] = serde_json::json!(100);
assert!(serde_json::from_value::<ArkInfo>(json).is_err());
}
#[test]
fn ark_info_fields() {
#[allow(unused, deprecated)]
fn convert(j: ArkInfo) -> ark::ArkInfo {
ark::ArkInfo {
network: j.network,
server_pubkey: j.server_pubkey,
mailbox_pubkey: j.mailbox_pubkey,
round_interval: j.round_interval,
nb_round_nonces: j.nb_round_nonces,
vtxo_exit_delta: j.vtxo_exit_delta,
vtxo_lifetime: j.vtxo_lifetime,
vtxo_expiry_delta: j.vtxo_expiry_delta,
htlc_send_expiry_delta: j.htlc_send_expiry_delta,
htlc_expiry_delta: j.htlc_expiry_delta,
max_vtxo_amount: j.max_vtxo_amount,
required_board_confirmations: j.required_board_confirmations,
max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
min_board_amount: j.min_board_amount,
offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
fees: j.fees.into(),
max_vtxo_exit_depth: j.max_vtxo_exit_depth,
max_offboard_inputs: j.max_offboard_inputs,
tos_link: j.tos_link,
}
}
}
}