1mod models;
115mod vtxo;
116pub(crate) mod progress;
117pub(crate) mod transaction_manager;
118
119pub use self::models::{
120 ExitTransactionPackage, TransactionInfo, ChildTransactionInfo, ExitError, ExitState,
121 ExitTx, ExitTxStatus, ExitTxOrigin, ExitStartState, ExitProcessingState, ExitAwaitingDeltaState,
122 ExitClaimableState, ExitClaimInProgressState, ExitClaimedState, ExitProgressStatus,
123 ExitTransactionStatus,
124};
125pub use self::vtxo::ExitVtxo;
126
127use std::borrow::Borrow;
128use std::cmp;
129use std::collections::HashMap;
130use std::sync::Arc;
131
132use anyhow::Context;
133use bitcoin::{
134 Address, Amount, FeeRate, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, sighash
135};
136use bitcoin::consensus::Params;
137use log::{error, info, trace, warn};
138
139use ark::{Vtxo, VtxoId};
140use ark::vtxo::Bare;
141use ark::vtxo::policy::signing::VtxoSigner;
142use bitcoin_ext::{BlockHeight, P2TR_DUST};
143
144use crate::Wallet;
145use crate::chain::ChainSource;
146use crate::exit::transaction_manager::ExitTransactionManager;
147use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
148use crate::movement::manager::MovementManager;
149use crate::movement::update::MovementUpdate;
150use crate::onchain::ExitUnilaterally;
151use crate::persist::BarkPersister;
152use crate::persist::models::StoredExit;
153use crate::psbtext::PsbtInputExt;
154use crate::subsystem::{ExitMovement, Subsystem};
155use crate::vtxo::{VtxoState, VtxoStateKind};
156
157pub(crate) struct ExitInner {
159 tx_manager: ExitTransactionManager,
160 persister: Arc<dyn BarkPersister>,
161 chain_source: Arc<ChainSource>,
162 movement_manager: Arc<MovementManager>,
163
164 exit_vtxos: Vec<ExitVtxo>,
165}
166
167impl ExitInner {
168 async fn start_exit_for_vtxos(
171 &mut self,
172 vtxos: &[impl Borrow<Vtxo<Bare>>],
173 ) -> anyhow::Result<()> {
174 if vtxos.is_empty() {
175 return Ok(());
176 }
177 let tip = self.chain_source.tip().await?;
178 let params = Params::new(self.chain_source.network());
179 for vtxo in vtxos {
180 let vtxo = vtxo.borrow();
181 let vtxo_id = vtxo.id();
182 if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
183 warn!("VTXO {} is already in the exit process", vtxo_id);
184 continue;
185 }
186
187 if vtxo.amount() < P2TR_DUST {
189 return Err(ExitError::DustLimit {
190 vtxo: vtxo.amount(),
191 dust: P2TR_DUST,
192 }.into());
193 }
194
195 trace!("Starting exit for VTXO: {}", vtxo_id);
198 let exit = ExitVtxo::new(vtxo, tip);
199 self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit)).await?;
200 self.persister.update_vtxo_state_checked(
201 vtxo_id, VtxoState::Spent, &VtxoStateKind::UNSPENT_STATES,
202 ).await?;
203 self.exit_vtxos.push(exit);
204 trace!("Exit for VTXO started successfully: {}", vtxo_id);
205
206 let balance = -vtxo.amount().to_signed()?;
208 let script_pubkey = vtxo.output_script_pubkey();
209 let payment_method = match Address::from_script(&script_pubkey, ¶ms) {
210 Ok(addr) => PaymentMethod::Bitcoin(addr.into_unchecked()),
211 Err(e) => {
212 warn!("Unable to convert script pubkey to address: {:#}", e);
213 PaymentMethod::OutputScript(script_pubkey)
214 }
215 };
216
217 self.movement_manager.new_finished_movement(
221 Subsystem::EXIT,
222 ExitMovement::Exit.to_string(),
223 MovementStatus::Successful,
224 MovementUpdate::new()
225 .intended_and_effective_balance(balance)
226 .consumed_vtxo(vtxo_id)
227 .sent_to([MovementDestination::new(payment_method, vtxo.amount())]),
228 ).await.context("Failed to register exit movement")?;
229 }
230 Ok(())
231 }
232
233 async fn sync_no_progress(&mut self, onchain: &dyn ExitUnilaterally) -> anyhow::Result<()> {
236 let mut exit_vtxos = std::mem::take(&mut self.exit_vtxos);
237 for exit in &mut exit_vtxos {
238 if !exit.is_initialized() {
239 match exit.initialize(&mut self.tx_manager, &*self.persister, onchain).await {
240 Ok(()) => continue,
241 Err(e) => {
242 error!("Error initializing exit for VTXO {}: {:#}", exit.id(), e);
243 }
244 }
245 }
246 }
247 self.exit_vtxos = exit_vtxos;
248 self.tx_manager.sync().await?;
249 Ok(())
250 }
251
252 async fn sign_exit_claim_inputs(
255 &self,
256 psbt: &mut Psbt,
257 wallet: &Wallet,
258 ) -> anyhow::Result<()> {
259 let prevouts = psbt.inputs.iter()
260 .map(|i| i.witness_utxo.clone().unwrap())
261 .collect::<Vec<_>>();
262
263 let prevouts = sighash::Prevouts::All(&prevouts);
264 let mut shc = sighash::SighashCache::new(&psbt.unsigned_tx);
265
266 let claimable = self.exit_vtxos.iter()
267 .filter(|ev| ev.is_claimable())
268 .map(|e| (e.id(), e))
269 .collect::<HashMap<_, _>>();
270
271 for (i, input) in psbt.inputs.iter_mut().enumerate() {
272 let vtxo = input.get_exit_claim_input();
273
274 if let Some(vtxo) = vtxo {
275 let exit_vtxo = claimable.get(&vtxo.id()).context("vtxo is not claimable yet")?;
276
277 let witness = wallet.sign_input(&vtxo, i, &mut shc, &prevouts).await
278 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
279
280 input.final_script_witness = Some(witness);
281 let _ = exit_vtxo;
282 }
283 }
284
285 Ok(())
286 }
287}
288
289pub struct Exit {
292 inner: Arc<tokio::sync::RwLock<ExitInner>>,
293}
294
295impl Exit {
296 pub(crate) async fn new(
297 persister: Arc<dyn BarkPersister>,
298 chain_source: Arc<ChainSource>,
299 movement_manager: Arc<MovementManager>,
300 ) -> anyhow::Result<Exit> {
301 let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
302 let inner = ExitInner {
303 exit_vtxos: Vec::new(),
304 tx_manager,
305 persister,
306 chain_source,
307 movement_manager,
308 };
309 Ok(Exit { inner: Arc::new(tokio::sync::RwLock::new(inner)) })
310 }
311
312 pub(crate) async fn load(&self, onchain: &dyn ExitUnilaterally) -> anyhow::Result<()> {
313 let mut guard = self.inner.write().await;
314 let inner = &mut *guard;
315 let exit_vtxo_entries = inner.persister.get_exit_vtxo_entries().await?;
316 inner.exit_vtxos.reserve(exit_vtxo_entries.len());
317
318 for entry in exit_vtxo_entries {
319 if let Some(vtxo) = inner.persister.get_wallet_vtxo(entry.vtxo_id).await? {
320 let mut exit = ExitVtxo::from_entry(entry, &vtxo);
321 exit.initialize(&mut inner.tx_manager, &*inner.persister, onchain).await?;
322 inner.exit_vtxos.push(exit);
323 } else {
324 error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
325 }
326 }
327 Ok(())
328 }
329
330 pub async fn get_exit_status(
337 &self,
338 vtxo_id: VtxoId,
339 include_history: bool,
340 include_transactions: bool,
341 ) -> Result<Option<ExitTransactionStatus>, ExitError> {
342 let guard = self.inner.read().await;
343 match guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id) {
344 None => Ok(None),
345 Some(exit) => {
346 let mut txs = Vec::new();
347 if include_transactions {
348 if let Some(txids) = exit.txids() {
349 txs.reserve(txids.len());
350 for txid in txids {
351 txs.push(guard.tx_manager.get_package(*txid)?.read().await.clone());
352 }
353 } else {
354 let exit_vtxo = exit.get_full_vtxo(&*guard.persister).await?;
359 for tx in exit_vtxo.transactions() {
360 txs.push(ExitTransactionPackage {
361 exit: TransactionInfo {
362 txid: tx.tx.compute_txid(),
363 tx: tx.tx,
364 },
365 child: None,
366 })
367 }
368 }
369 }
370 Ok(Some(ExitTransactionStatus {
371 vtxo_id: exit.id(),
372 state: exit.state().clone(),
373 history: if include_history { Some(exit.history().clone()) } else { None },
374 transactions: txs,
375 }))
376 },
377 }
378 }
379
380 pub async fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<ExitVtxo> {
382 let guard = self.inner.read().await;
383 guard.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id).cloned()
384 }
385
386 pub async fn get_exit_vtxos(&self) -> Vec<ExitVtxo> {
388 let guard = self.inner.read().await;
389 guard.exit_vtxos.clone()
390 }
391
392 pub async fn has_pending_exits(&self) -> bool {
394 let guard = self.inner.read().await;
395 guard.exit_vtxos.iter().any(|ev| ev.state().is_pending())
396 }
397
398 pub fn try_pending_total(&self) -> Option<Amount> {
400 self.inner.try_read().ok().map(|guard| {
401 guard.exit_vtxos.iter()
402 .filter_map(|ev| if ev.state().is_pending() { Some(ev.amount()) } else { None })
403 .sum()
404 })
405 }
406
407 pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
409 let guard = self.inner.read().await;
410 let mut highest_claimable_height = None;
411 for exit in &guard.exit_vtxos {
412 if matches!(exit.state(), ExitState::Claimed(..)) {
413 continue;
414 }
415 match exit.state().claimable_height() {
416 Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
417 None => return None,
418 }
419 }
420 highest_claimable_height
421 }
422
423 pub async fn start_exit_for_entire_wallet(&self) -> anyhow::Result<()> {
430 let mut guard = self.inner.write().await;
431 let all_vtxos = guard.persister.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?
432 .into_iter().map(|v| v.vtxo);
433
434 let (eligible, dust) = all_vtxos.partition::<Vec<_>, _>(|v| v.amount() >= P2TR_DUST);
436
437 for vtxo in &dust {
439 warn!(
440 "Skipping dust VTXO {}: {} sats is below the dust limit ({} sats).",
441 vtxo.id(), vtxo.amount().to_sat(), P2TR_DUST.to_sat()
442 );
443 }
444
445 if eligible.is_empty() && !dust.is_empty() {
447 warn!(
448 "Exit not started: all {} VTXOs (total {}) are below the dust limit. \
449 To exit and consolidate dust, you need to refresh your VTXOs first \
450 (requires total balance >= {})",
451 dust.len(),
452 dust.iter().map(|v| v.amount()).sum::<Amount>(),
453 P2TR_DUST,
454 );
455 return Ok(());
456 }
457
458 guard.start_exit_for_vtxos(&eligible).await
459 }
460
461 pub async fn start_exit_for_vtxos(
468 &self,
469 vtxos: &[impl Borrow<Vtxo<Bare>>],
470 ) -> anyhow::Result<()> {
471 let mut guard = self.inner.write().await;
472 guard.start_exit_for_vtxos(vtxos).await
473 }
474
475 pub(crate) async fn dangerous_clear_exit(&self) -> anyhow::Result<()> {
479 let mut guard = self.inner.write().await;
480 for exit in &guard.exit_vtxos {
481 guard.persister.remove_exit_vtxo_entry(&exit.id()).await?;
482 }
483 guard.exit_vtxos.clear();
484 Ok(())
485 }
486
487 pub async fn progress_exits(
502 &self,
503 wallet: &Wallet,
504 onchain: &mut dyn ExitUnilaterally,
505 fee_rate_override: Option<FeeRate>,
506 ) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
507 let mut guard = self.inner.write().await;
508 let mut exit_vtxos = std::mem::take(&mut guard.exit_vtxos);
509 let mut exit_statuses = Vec::with_capacity(exit_vtxos.len());
510
511 for ev in exit_vtxos.iter_mut() {
512 if !ev.is_initialized() {
513 warn!("Skipping progress of uninitialized unilateral exit {}", ev.id());
514 continue;
515 }
516
517 info!("Progressing exit for VTXO {}", ev.id());
518 let error = match ev.progress(
519 wallet,
520 &mut guard.tx_manager,
521 onchain,
522 fee_rate_override,
523 true,
524 ).await {
525 Ok(_) => None,
526 Err(e) => {
527 match &e {
528 ExitError::InsufficientConfirmedFunds { .. } => {
529 warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
530 },
531 _ => {
532 error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
533 }
534 }
535 Some(e)
536 }
537 };
538 if !matches!(ev.state(), ExitState::Claimed(..)) {
539 exit_statuses.push(ExitProgressStatus {
540 vtxo_id: ev.id(),
541 state: ev.state().clone(),
542 error,
543 });
544 }
545 }
546
547 guard.exit_vtxos = exit_vtxos;
548 Ok(Some(exit_statuses))
549 }
550
551 pub async fn sync(
555 &self,
556 wallet: &Wallet,
557 onchain: &mut dyn ExitUnilaterally,
558 ) -> anyhow::Result<()> {
559 let mut guard = self.inner.write().await;
560 guard.sync_no_progress(onchain).await?;
561 let mut exit_vtxos = std::mem::take(&mut guard.exit_vtxos);
562 for exit in &mut exit_vtxos {
563 if exit.state().requires_network_update() {
565 if let Err(e) = exit.progress(
566 wallet, &mut guard.tx_manager, onchain, None, false,
567 ).await {
568 error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
569 }
570 }
571 }
572 guard.exit_vtxos = exit_vtxos;
573 Ok(())
574 }
575
576 pub async fn sync_no_progress(&self, onchain: &dyn ExitUnilaterally) -> anyhow::Result<()> {
581 let mut guard = self.inner.write().await;
582 guard.sync_no_progress(onchain).await
583 }
584
585 pub async fn list_claimable(&self) -> Vec<ExitVtxo> {
587 let guard = self.inner.read().await;
588 guard.exit_vtxos.iter().filter(|ev| ev.is_claimable()).cloned().collect()
589 }
590
591 pub async fn sign_exit_claim_inputs(&self, psbt: &mut Psbt, wallet: &Wallet) -> anyhow::Result<()> {
599 let guard = self.inner.read().await;
600 guard.sign_exit_claim_inputs(psbt, wallet).await
601 }
602
603 pub async fn drain_exits(
612 &self,
613 inputs: &[impl Borrow<ExitVtxo>],
614 wallet: &Wallet,
615 address: Address,
616 fee_rate_override: Option<FeeRate>,
617 ) -> anyhow::Result<Psbt, ExitError> {
618 let guard = self.inner.read().await;
619
620 let tip = guard.chain_source.tip().await
621 .map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
622
623 if inputs.is_empty() {
624 return Err(ExitError::ClaimMissingInputs);
625 }
626 let mut vtxos = HashMap::with_capacity(inputs.len());
627 for input in inputs {
628 let i = input.borrow();
629 let vtxo = i.get_full_vtxo(&*guard.persister).await?;
630 vtxos.insert(i.id(), vtxo);
631 }
632
633 let mut tx = {
634 let mut output_amount = Amount::ZERO;
635 let mut tx_ins = Vec::with_capacity(inputs.len());
636 for input in inputs {
637 let input = input.borrow();
638 let vtxo = &vtxos[&input.id()];
639 if !matches!(input.state(), ExitState::Claimable(..)) {
640 return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
641 }
642
643 output_amount += vtxo.amount();
644
645 let clause = wallet.find_signable_clause(vtxo).await
646 .ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
647
648 tx_ins.push(TxIn {
649 previous_output: vtxo.point(),
650 script_sig: ScriptBuf::default(),
651 sequence: clause.sequence().unwrap_or(Sequence::ZERO),
652 witness: Witness::new(),
653 });
654 }
655
656 let locktime = bitcoin::absolute::LockTime::from_height(tip)
657 .map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
658
659 Transaction {
660 version: bitcoin::transaction::Version(3),
661 lock_time: locktime,
662 input: tx_ins,
663 output: vec![
664 TxOut {
665 script_pubkey: address.script_pubkey(),
666 value: output_amount,
667 },
668 ],
669 }
670 };
671
672 let create_psbt = |tx: Transaction| async {
674 let mut psbt = Psbt::from_unsigned_tx(tx)
675 .map_err(|e| ExitError::InternalError {
676 error: format!("Failed to create exit claim PSBT: {}", e),
677 })?;
678 psbt.inputs.iter_mut().zip(inputs).for_each(|(i, e)| {
679 let v = &vtxos[&e.borrow().id()];
680 i.set_exit_claim_input(v);
681 i.witness_utxo = Some(v.txout())
682 });
683 guard.sign_exit_claim_inputs(&mut psbt, wallet).await
684 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
685 Ok(psbt)
686 };
687 let fee_amount = {
688 let fee_rate = fee_rate_override
689 .unwrap_or(guard.chain_source.fee_rates().await.regular);
690 fee_rate * create_psbt(tx.clone()).await?
691 .extract_tx()
692 .map_err(|e| ExitError::InternalError {
693 error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
694 })?
695 .weight()
696 };
697
698 let needed = fee_amount + P2TR_DUST;
700 if needed > tx.output[0].value {
701 return Err(ExitError::ClaimFeeExceedsOutput {
702 needed, output: tx.output[0].value,
703 });
704 }
705 tx.output[0].value -= fee_amount;
706
707 create_psbt(tx).await
709 }
710}