#[cfg(feature = "onchain_bdk")]
pub mod onchain;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
use anyhow::anyhow;
use bitcoin::secp256k1::PublicKey;
use bitcoin::{Amount, FeeRate, Txid, SignedAmount, ScriptBuf};
use chrono::DateTime;
#[cfg(feature = "utoipa")]
use utoipa::ToSchema;
use ark::VtxoId;
use ark::lightning::{Invoice, Offer, PaymentHash, Preimage};
use bark::lnurllib::lightning_address::LightningAddress;
use bark::movement::MovementId;
use bitcoin_ext::{AmountExt, BlockDelta, BlockHeight};
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, Deserialize, 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,
pub vtxo_expiry_delta: 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,
}
impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
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_expiry_delta: v.vtxo_expiry_delta,
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,
}
}
}
#[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, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct Config {
pub ark: String,
pub bitcoind: Option<String>,
pub bitcoind_cookie: Option<String>,
pub bitcoind_user: Option<String>,
pub bitcoind_pass: Option<String>,
pub esplora: Option<String>,
pub vtxo_refresh_expiry_threshold: BlockHeight,
#[serde(rename = "fallback_fee_rate_kvb", with = "serde_utils::fee_rate_sats_per_kvb")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable = true))]
pub fallback_fee_rate: Option<FeeRate>,
}
#[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>,
}
#[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::models::ExitProgressStatus> for ExitProgressStatus {
fn from(v: bark::exit::models::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::models::ExitTransactionStatus> for ExitTransactionStatus {
fn from(v: bark::exit::models::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, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub enum MovementStatus {
Pending,
Successful,
Failed,
Canceled,
}
impl From<bark::movement::MovementStatus> for MovementStatus {
fn from(v: bark::movement::MovementStatus) -> Self {
match v {
bark::movement::MovementStatus::Pending => Self::Pending,
bark::movement::MovementStatus::Successful => Self::Successful,
bark::movement::MovementStatus::Failed => Self::Failed,
bark::movement::MovementStatus::Canceled => Self::Canceled,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct Movement {
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
pub id: MovementId,
pub status: MovementStatus,
pub subsystem: MovementSubsystem,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(rename="intended_balance_sat", with="bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = i64))]
pub intended_balance: SignedAmount,
#[serde(rename="effective_balance_sat", with="bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = i64))]
pub effective_balance: SignedAmount,
#[serde(rename="offchain_fee_sat", with="bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub offchain_fee: Amount,
pub sent_to: Vec<MovementDestination>,
pub received_on: Vec<MovementDestination>,
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
pub input_vtxos: Vec<VtxoId>,
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
pub output_vtxos: Vec<VtxoId>,
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
pub exited_vtxos: Vec<VtxoId>,
pub time: MovementTimestamp,
}
impl From<bark::movement::Movement> for Movement {
fn from(m: bark::movement::Movement) -> Self {
Movement {
id: m.id,
status: m.status.into(),
subsystem: MovementSubsystem::from(m.subsystem),
metadata: if m.metadata.is_empty() { None } else { Some(m.metadata) },
intended_balance: m.intended_balance,
effective_balance: m.effective_balance,
offchain_fee: m.offchain_fee,
sent_to: m.sent_to.into_iter().map(MovementDestination::from).collect(),
received_on: m.received_on.into_iter().map(MovementDestination::from).collect(),
input_vtxos: m.input_vtxos,
output_vtxos: m.output_vtxos,
exited_vtxos: m.exited_vtxos,
time: MovementTimestamp::from(m.time),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct MovementDestination {
pub destination: PaymentMethod,
#[serde(rename="amount_sat", with="bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub amount: Amount,
}
impl From<bark::movement::MovementDestination> for MovementDestination {
fn from(d: bark::movement::MovementDestination) -> Self {
MovementDestination {
destination: PaymentMethod::from(d.destination),
amount: d.amount,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(tag = "type", content = "value", rename_all = "kebab-case")]
pub enum PaymentMethod {
Ark(String),
Bitcoin(String),
OutputScript(String),
Invoice(String),
Offer(String),
LightningAddress(String),
Custom(String),
}
#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for PaymentMethod {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema;
schema::ObjectBuilder::new()
.title(Some("PaymentMethod"))
.description(Some("A payment method with a type discriminator and string value"))
.property(
"type",
schema::ObjectBuilder::new()
.schema_type(schema::SchemaType::Type(schema::Type::String))
.enum_values(Some([
"ark",
"bitcoin",
"output-script",
"invoice",
"offer",
"lightning-address",
"custom",
]))
.description(Some("The type of payment method"))
)
.required("type")
.property(
"value",
schema::ObjectBuilder::new()
.schema_type(schema::SchemaType::Type(schema::Type::String))
.description(Some("The payment method value (address, invoice, etc.)"))
)
.required("value")
.into()
}
}
#[cfg(feature = "utoipa")]
impl utoipa::ToSchema for PaymentMethod {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PaymentMethod")
}
}
impl From<bark::payment_method::PaymentMethod> for PaymentMethod {
fn from(p: bark::payment_method::PaymentMethod) -> Self {
match p {
bark::payment_method::PaymentMethod::Ark(a) => Self::Ark(a.to_string()),
bark::payment_method::PaymentMethod::Bitcoin(b) => Self::Bitcoin(b.assume_checked().to_string()),
bark::payment_method::PaymentMethod::OutputScript(s) => Self::OutputScript(s.to_hex_string()),
bark::payment_method::PaymentMethod::Invoice(i) => Self::Invoice(i.to_string()),
bark::payment_method::PaymentMethod::Offer(o) => Self::Offer(o.to_string()),
bark::payment_method::PaymentMethod::LightningAddress(l) => Self::LightningAddress(l.to_string()),
bark::payment_method::PaymentMethod::Custom(c) => Self::Custom(c),
}
}
}
impl TryFrom<PaymentMethod> for bark::payment_method::PaymentMethod {
type Error = anyhow::Error;
fn try_from(p: PaymentMethod) -> Result<Self, Self::Error> {
match p {
PaymentMethod::Ark(a) => Ok(bark::payment_method::PaymentMethod::Ark(ark::Address::from_str(&a)?)),
PaymentMethod::Bitcoin(b) => Ok(bark::payment_method::PaymentMethod::Bitcoin(bitcoin::Address::from_str(&b)?)),
PaymentMethod::OutputScript(s) => Ok(bark::payment_method::PaymentMethod::OutputScript(ScriptBuf::from_hex(&s)?)),
PaymentMethod::Invoice(i) => Ok(bark::payment_method::PaymentMethod::Invoice(Invoice::from_str(&i)?)),
PaymentMethod::Offer(o) => Ok(bark::payment_method::PaymentMethod::Offer(
Offer::from_str(&o).map_err(|e| anyhow!("Failed to parse offer: {:?}", e))?,
)),
PaymentMethod::LightningAddress(l) => Ok(bark::payment_method::PaymentMethod::LightningAddress(LightningAddress::from_str(&l)?)),
PaymentMethod::Custom(c) => Ok(bark::payment_method::PaymentMethod::Custom(c)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct MovementSubsystem {
pub name: String,
pub kind: String,
}
impl From<bark::movement::MovementSubsystem> for MovementSubsystem {
fn from(s: bark::movement::MovementSubsystem) -> Self {
MovementSubsystem {
name: s.name,
kind: s.kind,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct MovementTimestamp {
pub created_at: DateTime<chrono::Local>,
pub updated_at: DateTime<chrono::Local>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<chrono::Local>>,
}
impl From<bark::movement::MovementTimestamp> for MovementTimestamp {
fn from(t: bark::movement::MovementTimestamp) -> Self {
MovementTimestamp {
created_at: t.created_at,
updated_at: t.updated_at,
completed_at: t.completed_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "result", rename_all = "lowercase")]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub enum RoundStatus {
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 {
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
unsigned_funding_txids: Vec<Txid>,
},
Failed {
error: String,
},
Canceled,
}
impl RoundStatus {
pub fn is_final(&self) -> bool {
match self {
Self::Confirmed { .. } => true,
Self::Unconfirmed { .. } => false,
Self::Pending { .. } => false,
Self::Failed { .. } => true,
Self::Canceled => true,
}
}
pub fn is_success(&self) -> bool {
match self {
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 { unsigned_funding_txids } => Self::Pending { unsigned_funding_txids },
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 InvoiceInfo {
pub invoice: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct LightningReceiveInfo {
#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
#[cfg_attr(feature = "utoipa", schema(value_type = u64))]
pub amount: Amount,
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub payment_hash: PaymentHash,
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub payment_preimage: Preimage,
pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
pub finished_at: Option<chrono::DateTime<chrono::Local>>,
pub invoice: String,
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<WalletVtxoInfo>, nullable = true))]
pub htlc_vtxos: Option<Vec<WalletVtxoInfo>>,
}
impl From<bark::persist::models::LightningReceive> for LightningReceiveInfo {
fn from(v: bark::persist::models::LightningReceive) -> Self {
LightningReceiveInfo {
payment_hash: v.payment_hash,
payment_preimage: v.payment_preimage,
preimage_revealed_at: v.preimage_revealed_at,
invoice: v.invoice.to_string(),
htlc_vtxos: v.htlc_vtxos.map(|vtxos| vtxos.into_iter()
.map(crate::primitives::WalletVtxoInfo::from).collect()),
amount: v.invoice.amount_milli_satoshis().map(Amount::from_msat_floor)
.unwrap_or(Amount::ZERO),
finished_at: v.finished_at,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn ark_info_fields() {
#[allow(unused)]
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_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,
}
}
}
}