1#[cfg(feature = "onchain_bdk")]
2pub mod onchain;
3
4use std::borrow::Borrow;
5use std::collections::HashMap;
6use std::str::FromStr;
7use std::time::Duration;
8
9use anyhow::anyhow;
10use bitcoin::secp256k1::PublicKey;
11use bitcoin::{Amount, Txid, SignedAmount, ScriptBuf};
12use chrono::DateTime;
13#[cfg(feature = "utoipa")]
14use utoipa::ToSchema;
15
16use ark::VtxoId;
17use ark::lightning::{Invoice, Offer, PaymentHash, Preimage};
18use bark::lnurllib::lightning_address::LightningAddress;
19use bark::movement::MovementId;
20use bitcoin_ext::{AmountExt, BlockDelta};
21
22use crate::exit::error::ExitError;
23use crate::exit::package::ExitTransactionPackage;
24use crate::exit::ExitState;
25use crate::primitives::{TransactionInfo, WalletVtxoInfo};
26use crate::serde_utils;
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
29#[cfg_attr(feature = "utoipa", derive(ToSchema))]
30pub struct ArkInfo {
31 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
33 pub network: bitcoin::Network,
34 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
36 pub server_pubkey: PublicKey,
37 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
39 pub mailbox_pubkey: PublicKey,
40 #[serde(with = "serde_utils::duration")]
42 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
43 pub round_interval: Duration,
44 pub nb_round_nonces: usize,
46 pub vtxo_exit_delta: BlockDelta,
48 pub vtxo_expiry_delta: BlockDelta,
50 pub htlc_send_expiry_delta: BlockDelta,
52 pub htlc_expiry_delta: BlockDelta,
54 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
56 pub max_vtxo_amount: Option<Amount>,
57 pub required_board_confirmations: usize,
59 pub max_user_invoice_cltv_delta: u16,
62 #[serde(rename = "min_board_amount_sat", with = "bitcoin::amount::serde::as_sat")]
64 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
65 pub min_board_amount: Amount,
66 pub offboard_feerate_sat_per_kvb: u64,
68 pub offboard_fixed_fee_vb: u64,
71 pub ln_receive_anti_dos_required: bool,
75}
76
77impl<T: Borrow<ark::ArkInfo>> From<T> for ArkInfo {
78 fn from(v: T) -> Self {
79 let v = v.borrow();
80 ArkInfo {
81 network: v.network,
82 server_pubkey: v.server_pubkey,
83 mailbox_pubkey: v.mailbox_pubkey,
84 round_interval: v.round_interval,
85 nb_round_nonces: v.nb_round_nonces,
86 vtxo_exit_delta: v.vtxo_exit_delta,
87 vtxo_expiry_delta: v.vtxo_expiry_delta,
88 htlc_send_expiry_delta: v.htlc_send_expiry_delta,
89 htlc_expiry_delta: v.htlc_expiry_delta,
90 max_vtxo_amount: v.max_vtxo_amount,
91 required_board_confirmations: v.required_board_confirmations,
92 max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta,
93 min_board_amount: v.min_board_amount,
94 offboard_feerate_sat_per_kvb: v.offboard_feerate.to_sat_per_kwu() * 4,
95 offboard_fixed_fee_vb: v.offboard_fixed_fee_vb,
96 ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
102#[cfg_attr(feature = "utoipa", derive(ToSchema))]
103pub struct Balance {
104 #[serde(rename = "spendable_sat", with = "bitcoin::amount::serde::as_sat")]
105 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
106 pub spendable: Amount,
107 #[serde(rename = "pending_lightning_send_sat", with = "bitcoin::amount::serde::as_sat")]
108 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
109 pub pending_lightning_send: Amount,
110 #[serde(rename = "claimable_lightning_receive_sat", with = "bitcoin::amount::serde::as_sat")]
111 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
112 pub claimable_lightning_receive: Amount,
113 #[serde(rename = "pending_in_round_sat", with = "bitcoin::amount::serde::as_sat")]
114 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
115 pub pending_in_round: Amount,
116 #[serde(rename = "pending_board_sat", with = "bitcoin::amount::serde::as_sat")]
117 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
118 pub pending_board: Amount,
119 #[serde(
120 default,
121 rename = "pending_exit_sat",
122 with = "bitcoin::amount::serde::as_sat::opt",
123 skip_serializing_if = "Option::is_none",
124 )]
125 #[cfg_attr(feature = "utoipa", schema(value_type = u64, nullable=true))]
126 pub pending_exit: Option<Amount>,
127}
128
129impl From<bark::Balance> for Balance {
130 fn from(v: bark::Balance) -> Self {
131 Balance {
132 spendable: v.spendable,
133 pending_in_round: v.pending_in_round,
134 pending_lightning_send: v.pending_lightning_send,
135 claimable_lightning_receive: v.claimable_lightning_receive,
136 pending_exit: v.pending_exit,
137 pending_board: v.pending_board,
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
143#[cfg_attr(feature = "utoipa", derive(ToSchema))]
144pub struct ExitProgressResponse {
145 pub exits: Vec<ExitProgressStatus>,
147 pub done: bool,
149 pub claimable_height: Option<u32>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
154#[cfg_attr(feature = "utoipa", derive(ToSchema))]
155pub struct ExitProgressStatus {
156 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
158 pub vtxo_id: VtxoId,
159 pub state: ExitState,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub error: Option<ExitError>,
164}
165
166impl From<bark::exit::ExitProgressStatus> for ExitProgressStatus {
167 fn from(v: bark::exit::ExitProgressStatus) -> Self {
168 ExitProgressStatus {
169 vtxo_id: v.vtxo_id,
170 state: v.state.into(),
171 error: v.error.map(ExitError::from),
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
177#[cfg_attr(feature = "utoipa", derive(ToSchema))]
178pub struct ExitTransactionStatus {
179 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
181 pub vtxo_id: VtxoId,
182 pub state: ExitState,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub history: Option<Vec<ExitState>>,
187 #[serde(default, skip_serializing_if = "Vec::is_empty")]
189 pub transactions: Vec<ExitTransactionPackage>,
190}
191
192impl From<bark::exit::ExitTransactionStatus> for ExitTransactionStatus {
193 fn from(v: bark::exit::ExitTransactionStatus) -> Self {
194 ExitTransactionStatus {
195 vtxo_id: v.vtxo_id,
196 state: v.state.into(),
197 history: v.history.map(|h| h.into_iter().map(ExitState::from).collect()),
198 transactions: v.transactions.into_iter().map(ExitTransactionPackage::from).collect(),
199 }
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
205#[cfg_attr(feature = "utoipa", derive(ToSchema))]
206pub struct PendingBoardInfo {
207 pub funding_tx: TransactionInfo,
211 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
216 pub vtxos: Vec<VtxoId>,
217 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
219 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
220 pub amount: Amount,
221 pub movement_id: u32,
223}
224
225impl From<bark::persist::models::PendingBoard> for PendingBoardInfo {
226 fn from(v: bark::persist::models::PendingBoard) -> Self {
227 PendingBoardInfo {
228 funding_tx: v.funding_tx.into(),
229 vtxos: v.vtxos,
230 amount: v.amount,
231 movement_id: v.movement_id.0,
232 }
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
237#[serde(rename_all = "kebab-case")]
238#[cfg_attr(feature = "utoipa", derive(ToSchema))]
239pub enum MovementStatus {
240 Pending,
242 Successful,
244 Failed,
247 Canceled,
250}
251
252impl From<bark::movement::MovementStatus> for MovementStatus {
253 fn from(v: bark::movement::MovementStatus) -> Self {
254 match v {
255 bark::movement::MovementStatus::Pending => Self::Pending,
256 bark::movement::MovementStatus::Successful => Self::Successful,
257 bark::movement::MovementStatus::Failed => Self::Failed,
258 bark::movement::MovementStatus::Canceled => Self::Canceled,
259 }
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
265#[cfg_attr(feature = "utoipa", derive(ToSchema))]
266pub struct Movement {
267 #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
269 pub id: MovementId,
270 pub status: MovementStatus,
272 pub subsystem: MovementSubsystem,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub metadata: Option<HashMap<String, serde_json::Value>>,
279 #[serde(rename="intended_balance_sat", with="bitcoin::amount::serde::as_sat")]
282 #[cfg_attr(feature = "utoipa", schema(value_type = i64))]
283 pub intended_balance: SignedAmount,
284 #[serde(rename="effective_balance_sat", with="bitcoin::amount::serde::as_sat")]
288 #[cfg_attr(feature = "utoipa", schema(value_type = i64))]
289 pub effective_balance: SignedAmount,
290 #[serde(rename="offchain_fee_sat", with="bitcoin::amount::serde::as_sat")]
294 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
295 pub offchain_fee: Amount,
296 pub sent_to: Vec<MovementDestination>,
298 pub received_on: Vec<MovementDestination>,
301 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
304 pub input_vtxos: Vec<VtxoId>,
305 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
308 pub output_vtxos: Vec<VtxoId>,
309 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
314 pub exited_vtxos: Vec<VtxoId>,
315 pub time: MovementTimestamp,
317}
318
319impl From<bark::movement::Movement> for Movement {
320 fn from(m: bark::movement::Movement) -> Self {
321 Movement {
322 id: m.id,
323 status: m.status.into(),
324 subsystem: MovementSubsystem::from(m.subsystem),
325 metadata: if m.metadata.is_empty() { None } else { Some(m.metadata) },
326 intended_balance: m.intended_balance,
327 effective_balance: m.effective_balance,
328 offchain_fee: m.offchain_fee,
329 sent_to: m.sent_to.into_iter().map(MovementDestination::from).collect(),
330 received_on: m.received_on.into_iter().map(MovementDestination::from).collect(),
331 input_vtxos: m.input_vtxos,
332 output_vtxos: m.output_vtxos,
333 exited_vtxos: m.exited_vtxos,
334 time: MovementTimestamp::from(m.time),
335 }
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
342#[cfg_attr(feature = "utoipa", derive(ToSchema))]
343pub struct MovementDestination {
344 pub destination: PaymentMethod,
346 #[serde(rename="amount_sat", with="bitcoin::amount::serde::as_sat")]
348 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
349 pub amount: Amount,
350}
351
352impl From<bark::movement::MovementDestination> for MovementDestination {
353 fn from(d: bark::movement::MovementDestination) -> Self {
354 MovementDestination {
355 destination: PaymentMethod::from(d.destination),
356 amount: d.amount,
357 }
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
365#[serde(tag = "type", content = "value", rename_all = "kebab-case")]
366pub enum PaymentMethod {
367 Ark(String),
369 Bitcoin(String),
371 OutputScript(String),
374 Invoice(String),
376 Offer(String),
378 LightningAddress(String),
380 Custom(String),
382}
383
384#[cfg(feature = "utoipa")]
385impl utoipa::PartialSchema for PaymentMethod {
386 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
387 use utoipa::openapi::schema;
388
389 schema::ObjectBuilder::new()
390 .title(Some("PaymentMethod"))
391 .description(Some("A payment method with a type discriminator and string value"))
392 .property(
393 "type",
394 schema::ObjectBuilder::new()
395 .schema_type(schema::SchemaType::Type(schema::Type::String))
396 .enum_values(Some([
397 "ark",
398 "bitcoin",
399 "output-script",
400 "invoice",
401 "offer",
402 "lightning-address",
403 "custom",
404 ]))
405 .description(Some("The type of payment method"))
406 )
407 .required("type")
408 .property(
409 "value",
410 schema::ObjectBuilder::new()
411 .schema_type(schema::SchemaType::Type(schema::Type::String))
412 .description(Some("The payment method value (address, invoice, etc.)"))
413 )
414 .required("value")
415 .into()
416 }
417}
418
419#[cfg(feature = "utoipa")]
420impl utoipa::ToSchema for PaymentMethod {
421 fn name() -> std::borrow::Cow<'static, str> {
422 std::borrow::Cow::Borrowed("PaymentMethod")
423 }
424}
425
426impl From<bark::movement::PaymentMethod> for PaymentMethod {
427 fn from(p: bark::movement::PaymentMethod) -> Self {
428 match p {
429 bark::movement::PaymentMethod::Ark(a) => Self::Ark(a.to_string()),
430 bark::movement::PaymentMethod::Bitcoin(b) => Self::Bitcoin(b.assume_checked().to_string()),
431 bark::movement::PaymentMethod::OutputScript(s) => Self::OutputScript(s.to_hex_string()),
432 bark::movement::PaymentMethod::Invoice(i) => Self::Invoice(i.to_string()),
433 bark::movement::PaymentMethod::Offer(o) => Self::Offer(o.to_string()),
434 bark::movement::PaymentMethod::LightningAddress(l) => Self::LightningAddress(l.to_string()),
435 bark::movement::PaymentMethod::Custom(c) => Self::Custom(c),
436 }
437 }
438}
439
440impl TryFrom<PaymentMethod> for bark::movement::PaymentMethod {
441 type Error = anyhow::Error;
442
443 fn try_from(p: PaymentMethod) -> Result<Self, Self::Error> {
444 match p {
445 PaymentMethod::Ark(a) => Ok(bark::movement::PaymentMethod::Ark(
446 ark::Address::from_str(&a)?,
447 )),
448 PaymentMethod::Bitcoin(b) => Ok(bark::movement::PaymentMethod::Bitcoin(
449 bitcoin::Address::from_str(&b)?,
450 )),
451 PaymentMethod::OutputScript(s) => Ok(bark::movement::PaymentMethod::OutputScript(
452 ScriptBuf::from_hex(&s)?,
453 )),
454 PaymentMethod::Invoice(i) => Ok(bark::movement::PaymentMethod::Invoice(
455 Invoice::from_str(&i)?,
456 )),
457 PaymentMethod::Offer(o) => Ok(bark::movement::PaymentMethod::Offer(
458 Offer::from_str(&o).map_err(|e| anyhow!("Failed to parse offer: {:?}", e))?,
459 )),
460 PaymentMethod::LightningAddress(l) => Ok(bark::movement::PaymentMethod::LightningAddress(
461 LightningAddress::from_str(&l)?,
462 )),
463 PaymentMethod::Custom(c) => Ok(bark::movement::PaymentMethod::Custom(c)),
464 }
465 }
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
471#[cfg_attr(feature = "utoipa", derive(ToSchema))]
472pub struct MovementSubsystem {
473 pub name: String,
475 pub kind: String,
477}
478
479impl From<bark::movement::MovementSubsystem> for MovementSubsystem {
480 fn from(s: bark::movement::MovementSubsystem) -> Self {
481 MovementSubsystem {
482 name: s.name,
483 kind: s.kind,
484 }
485 }
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
490#[cfg_attr(feature = "utoipa", derive(ToSchema))]
491pub struct MovementTimestamp {
492 pub created_at: DateTime<chrono::Local>,
494 pub updated_at: DateTime<chrono::Local>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub completed_at: Option<DateTime<chrono::Local>>,
499}
500
501impl From<bark::movement::MovementTimestamp> for MovementTimestamp {
502 fn from(t: bark::movement::MovementTimestamp) -> Self {
503 MovementTimestamp {
504 created_at: t.created_at,
505 updated_at: t.updated_at,
506 completed_at: t.completed_at,
507 }
508 }
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[serde(tag = "status", rename_all = "kebab-case")]
513#[cfg_attr(feature = "utoipa", derive(ToSchema))]
514pub enum RoundStatus {
515 SyncError {
517 error: String,
518 },
519 Confirmed {
521 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
522 funding_txid: Txid,
523 },
524 Unconfirmed {
526 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
527 funding_txid: Txid,
528 },
529 Pending,
531 Failed {
533 error: String,
534 },
535 Canceled,
537}
538
539impl RoundStatus {
540 pub fn is_final(&self) -> bool {
542 match self {
543 Self::SyncError { .. } => false,
544 Self::Confirmed { .. } => true,
545 Self::Unconfirmed { .. } => false,
546 Self::Pending { .. } => false,
547 Self::Failed { .. } => true,
548 Self::Canceled => true,
549 }
550 }
551
552 pub fn is_success(&self) -> bool {
554 match self {
555 Self::SyncError { .. } => false,
556 Self::Confirmed { .. } => true,
557 Self::Unconfirmed { .. } => true,
558 Self::Pending { .. } => false,
559 Self::Failed { .. } => false,
560 Self::Canceled => false,
561 }
562 }
563}
564
565impl From<bark::round::RoundStatus> for RoundStatus {
566 fn from(s: bark::round::RoundStatus) -> Self {
567 match s {
568 bark::round::RoundStatus::Confirmed { funding_txid } => {
569 Self::Confirmed { funding_txid }
570 },
571 bark::round::RoundStatus::Unconfirmed { funding_txid } => {
572 Self::Unconfirmed { funding_txid }
573 },
574 bark::round::RoundStatus::Pending => Self::Pending,
575 bark::round::RoundStatus::Failed { error } => Self::Failed { error },
576 bark::round::RoundStatus::Canceled => Self::Canceled,
577 }
578 }
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
582#[cfg_attr(feature = "utoipa", derive(ToSchema))]
583pub struct InvoiceInfo {
584 pub invoice: String,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589#[cfg_attr(feature = "utoipa", derive(ToSchema))]
590pub struct OffboardResult {
591 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
593 pub offboard_txid: Txid,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
597#[cfg_attr(feature = "utoipa", derive(ToSchema))]
598pub struct LightningReceiveInfo {
599 #[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
601 #[cfg_attr(feature = "utoipa", schema(value_type = u64))]
602 pub amount: Amount,
603 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
605 pub payment_hash: PaymentHash,
606 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
608 pub payment_preimage: Preimage,
609 pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
611 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
613 pub invoice: String,
615 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<WalletVtxoInfo>, nullable = true))]
619 pub htlc_vtxos: Option<Vec<WalletVtxoInfo>>,
620}
621
622impl From<bark::persist::models::LightningReceive> for LightningReceiveInfo {
623 fn from(v: bark::persist::models::LightningReceive) -> Self {
624 LightningReceiveInfo {
625 payment_hash: v.payment_hash,
626 payment_preimage: v.payment_preimage,
627 preimage_revealed_at: v.preimage_revealed_at,
628 invoice: v.invoice.to_string(),
629 htlc_vtxos: v.htlc_vtxos.map(|vtxos| vtxos.into_iter()
630 .map(crate::primitives::WalletVtxoInfo::from).collect()),
631 amount: v.invoice.amount_milli_satoshis().map(Amount::from_msat_floor)
632 .unwrap_or(Amount::ZERO),
633 finished_at: v.finished_at,
634 }
635 }
636}
637
638#[cfg(test)]
639mod test {
640 use bitcoin::FeeRate;
641 use super::*;
642
643 #[test]
644 fn ark_info_fields() {
645 #[allow(unused)]
649 fn convert(j: ArkInfo) -> ark::ArkInfo {
650 ark::ArkInfo {
651 network: j.network,
652 server_pubkey: j.server_pubkey,
653 mailbox_pubkey: j.mailbox_pubkey,
654 round_interval: j.round_interval,
655 nb_round_nonces: j.nb_round_nonces,
656 vtxo_exit_delta: j.vtxo_exit_delta,
657 vtxo_expiry_delta: j.vtxo_expiry_delta,
658 htlc_send_expiry_delta: j.htlc_send_expiry_delta,
659 htlc_expiry_delta: j.htlc_expiry_delta,
660 max_vtxo_amount: j.max_vtxo_amount,
661 required_board_confirmations: j.required_board_confirmations,
662 max_user_invoice_cltv_delta: j.max_user_invoice_cltv_delta,
663 min_board_amount: j.min_board_amount,
664 offboard_feerate: FeeRate::from_sat_per_kwu(j.offboard_feerate_sat_per_kvb / 4),
665 offboard_fixed_fee_vb: j.offboard_fixed_fee_vb,
666 ln_receive_anti_dos_required: j.ln_receive_anti_dos_required,
667 }
668 }
669 }
670}
671