1pub mod models;
112
113pub(crate) mod progress;
114pub(crate) mod transaction_manager;
115
116pub use vtxo::ExitVtxo;
117
118mod vtxo;
119
120use std::borrow::Borrow;
121use std::cmp;
122use std::collections::{HashMap, HashSet};
123use std::sync::Arc;
124
125use anyhow::Context;
126use bitcoin::{
127 sighash, Address, Amount, FeeRate, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
128};
129use bitcoin::consensus::Params;
130use log::{error, info, warn};
131
132use ark::{Vtxo, VtxoId, SECP};
133use bitcoin_ext::{BlockHeight, P2TR_DUST};
134
135use crate::Wallet;
136use crate::exit::models::{ExitError, ExitProgressStatus, ExitState, ExitTransactionStatus};
137use crate::exit::transaction_manager::ExitTransactionManager;
138use crate::movement::{MovementDestination, MovementStatus};
139use crate::movement::manager::MovementManager;
140use crate::movement::update::MovementUpdate;
141use crate::onchain::{ChainSource, ExitUnilaterally};
142use crate::persist::BarkPersister;
143use crate::persist::models::StoredExit;
144use crate::psbtext::PsbtInputExt;
145use crate::subsystem::{BarkSubsystem, ExitMovement, SubsystemId};
146use crate::vtxo::state::{VtxoState, UNSPENT_STATES};
147
148pub struct Exit {
150 tx_manager: ExitTransactionManager,
151 persister: Arc<dyn BarkPersister>,
152 chain_source: Arc<ChainSource>,
153 movement_manager: Arc<MovementManager>,
154
155 subsystem_id: SubsystemId,
156 vtxos_to_exit: HashSet<VtxoId>,
157 exit_vtxos: Vec<ExitVtxo>,
158}
159
160impl Exit {
161 pub (crate) async fn new(
162 persister: Arc<dyn BarkPersister>,
163 chain_source: Arc<ChainSource>,
164 movement_manager: Arc<MovementManager>,
165 ) -> anyhow::Result<Exit> {
166 let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
167
168 let exit_vtxo_entries = persister.get_exit_vtxo_entries()?;
170
171 let subsystem_id = movement_manager.register_subsystem(
172 BarkSubsystem::Exit.as_str().into(),
173 ).await?;
174 Ok(Exit {
175 vtxos_to_exit: HashSet::new(),
176 exit_vtxos: Vec::with_capacity(exit_vtxo_entries.len()),
177 subsystem_id,
178 tx_manager,
179 persister,
180 chain_source,
181 movement_manager,
182 })
183 }
184
185 pub (crate) async fn load(
186 &mut self,
187 onchain: &dyn ExitUnilaterally,
188 ) -> anyhow::Result<()> {
189 let exit_vtxo_entries = self.persister.get_exit_vtxo_entries()?;
190 for entry in exit_vtxo_entries {
191 if let Some(vtxo) = self.persister.get_wallet_vtxo(entry.vtxo_id)? {
192 let txids = self.tx_manager.track_vtxo_exits(&vtxo.vtxo, onchain).await?;
193 self.exit_vtxos.push(ExitVtxo::from_parts(vtxo.vtxo, txids, entry.state, entry.history));
194 } else {
195 error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
196 }
197 }
198
199 Ok(())
200 }
201
202 pub async fn get_exit_status(
209 &self,
210 vtxo_id: VtxoId,
211 include_history: bool,
212 include_transactions: bool,
213 ) -> Result<Option<ExitTransactionStatus>, ExitError> {
214 match self.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id) {
215 None => Ok(None),
216 Some(exit) => {
217 let transactions = if include_transactions {
218 let mut vec = Vec::with_capacity(exit.txids().len());
219 for txid in exit.txids() {
220 vec.push(self.tx_manager.get_package(*txid)?.read().await.clone());
221 }
222 vec
223 } else {
224 vec![]
225 };
226 Ok(Some(ExitTransactionStatus {
227 vtxo_id: exit.id(),
228 state: exit.state().clone(),
229 history: if include_history { Some(exit.history().clone()) } else { None },
230 transactions,
231 }))
232 },
233 }
234 }
235
236 pub fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<&ExitVtxo> {
238 self.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id)
239 }
240
241 pub fn get_exit_vtxos(&self) -> &Vec<ExitVtxo> {
243 &self.exit_vtxos
244 }
245
246 pub fn has_pending_exits(&self) -> bool {
248 self.exit_vtxos.iter().any(|ev| ev.state().is_pending())
249 }
250
251 pub fn pending_total(&self) -> Amount {
253 self.exit_vtxos
254 .iter()
255 .filter_map(|ev| {
256 if ev.state().is_pending() {
257 Some(ev.vtxo().spec().amount)
258 } else {
259 None
260 }
261 }).sum()
262 }
263
264 pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
266 let mut highest_claimable_height = None;
267 for exit in &self.exit_vtxos {
268 if matches!(exit.state(), ExitState::Claimed(..)) {
269 continue;
270 }
271 match exit.state().claimable_height() {
272 Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
273 None => return None,
274 }
275 }
276 highest_claimable_height
277 }
278
279 pub async fn start_exit_for_entire_wallet(
286 &mut self,
287 onchain: &dyn ExitUnilaterally,
288 ) -> anyhow::Result<()> {
289 let vtxos: Vec<Vtxo> = self.persister.get_vtxos_by_state(&UNSPENT_STATES)?.into_iter()
290 .map(|v| v.vtxo).collect();
291 self.start_exit_for_vtxos(&vtxos, onchain).await?;
292
293 Ok(())
294 }
295
296 pub async fn start_exit_for_vtxos<V: Borrow<Vtxo>>(
303 &mut self,
304 vtxos: &[V],
305 onchain: &dyn ExitUnilaterally,
306 ) -> anyhow::Result<()> {
307 self.mark_vtxos_for_exit(&vtxos).await?;
308 self.start_vtxo_exits(onchain).await?;
309 Ok(())
310 }
311
312 pub fn list_vtxos_to_exit(&self) -> Vec<VtxoId> {
314 self.vtxos_to_exit.iter().cloned().collect()
315 }
316
317 pub async fn mark_vtxos_for_exit<'a>(
322 &mut self,
323 vtxos: &[impl Borrow<Vtxo>],
324 ) -> anyhow::Result<()> {
325 for vtxo in vtxos {
326 let vtxo = vtxo.borrow();
327 let vtxo_id = vtxo.id();
328 if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
329 warn!("VTXO {} is already in the exit process", vtxo_id);
330 continue;
331 }
332 self.vtxos_to_exit.insert(vtxo.id());
333
334 self.persister.update_vtxo_state_checked(vtxo_id, VtxoState::Spent, &UNSPENT_STATES)?;
336 let params = Params::new(self.chain_source.network());
337 let balance = -vtxo.amount().to_signed()?;
338 let destination = MovementDestination::new(
339 Address::from_script(&vtxo.output_script_pubkey(), ¶ms)?.to_string(),
340 vtxo.amount(),
341 );
342
343 self.movement_manager.new_finished_movement(
347 self.subsystem_id,
348 ExitMovement::Exit.to_string(),
349 MovementStatus::Finished,
350 MovementUpdate::new()
351 .intended_and_effective_balance(balance)
352 .consumed_vtxo(vtxo.id())
353 .sent_to([destination]),
354 ).await.context("Failed to register exit movement")?;
355 }
356 Ok(())
357 }
358
359 pub async fn start_vtxo_exits(&mut self, onchain: &dyn ExitUnilaterally) -> anyhow::Result<()> {
363 if self.vtxos_to_exit.is_empty() {
364 return Ok(());
365 }
366
367 let tip = self.chain_source.tip().await?;
368
369 let cloned = self.vtxos_to_exit.iter().cloned().collect::<Vec<_>>();
370 for vtxo_id in cloned {
371 let vtxo = match self.persister.get_wallet_vtxo(vtxo_id)? {
372 Some(vtxo) => vtxo.vtxo,
373 None => {
374 error!("Could not find vtxo to exit {}", vtxo_id);
375 continue;
376 }
377 };
378
379 if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo.id()) {
380 warn!("VTXO {} is already in the exit process", vtxo.id());
381 continue;
382 } else {
383 let txids = self.tx_manager.track_vtxo_exits(&vtxo, &*onchain).await?;
386 let exit = ExitVtxo::new(vtxo.clone(), txids, tip);
387 self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit))?;
388 self.exit_vtxos.push(exit);
389 }
390
391 self.vtxos_to_exit.remove(&vtxo_id);
392 }
393
394 Ok(())
395 }
396
397 pub (crate) fn clear_exit(&mut self) -> anyhow::Result<()> {
401 for exit in &self.exit_vtxos {
402 self.persister.remove_exit_vtxo_entry(&exit.id())?;
403 }
404 self.exit_vtxos.clear();
405 Ok(())
406 }
407
408 pub async fn progress_exits(
424 &mut self,
425 onchain: &mut dyn ExitUnilaterally,
426 fee_rate_override: Option<FeeRate>,
427 ) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
428 self.tx_manager.sync().await?;
429 let mut exit_statuses = Vec::with_capacity(self.exit_vtxos.len());
430 for ev in self.exit_vtxos.iter_mut() {
431 info!("Progressing exit for VTXO {}", ev.id());
432 let error = match ev.progress(
433 &self.chain_source,
434 &mut self.tx_manager,
435 &*self.persister,
436 onchain,
437 fee_rate_override,
438 ).await {
439 Ok(_) => None,
440 Err(e) => {
441 match &e {
442 ExitError::InsufficientConfirmedFunds { .. } => {
443 warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
444 },
445 _ => {
446 error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
447 }
448 }
449 Some(e)
450 }
451 };
452 if !matches!(ev.state(), ExitState::Claimed(..)) {
453 exit_statuses.push(ExitProgressStatus {
454 vtxo_id: ev.id(),
455 state: ev.state().clone(),
456 error,
457 });
458 }
459 }
460 Ok(Some(exit_statuses))
461 }
462
463 pub async fn sync_exit(
466 &mut self,
467 onchain: &mut dyn ExitUnilaterally,
468 ) -> anyhow::Result<()> {
469 self.tx_manager.sync().await?;
470 self.start_vtxo_exits(onchain).await?;
471 for exit in &mut self.exit_vtxos {
472 if exit.state().requires_network_update() {
474 if let Err(e) = exit.progress(
475 &self.chain_source, &mut self.tx_manager, &*self.persister, onchain, None,
476 ).await {
477 error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
478 }
479 }
480 }
481 Ok(())
482 }
483
484 pub fn list_claimable(&self) -> Vec<&ExitVtxo> {
486 self.exit_vtxos.iter().filter(|ev| ev.is_claimable()).collect()
487 }
488
489 pub fn sign_exit_claim_inputs(&self, psbt: &mut Psbt, wallet: &Wallet) -> anyhow::Result<()> {
497 let prevouts = psbt.inputs.iter()
498 .map(|i| i.witness_utxo.clone().unwrap())
499 .collect::<Vec<_>>();
500
501 let prevouts = sighash::Prevouts::All(&prevouts);
502 let mut shc = sighash::SighashCache::new(&psbt.unsigned_tx);
503
504 let claimable = self.list_claimable()
505 .into_iter()
506 .map(|v| (v.vtxo().id(), v))
507 .collect::<HashMap<_, _>>();
508
509 let mut spent = Vec::new();
510 for (i, input) in psbt.inputs.iter_mut().enumerate() {
511 let vtxo = input.get_exit_claim_input();
512
513 if let Some(vtxo) = vtxo {
514 let exit_vtxo = *claimable.get(&vtxo.id()).context("vtxo is not exited yet")?;
515
516 let keypair = wallet.get_vtxo_key(&vtxo)?;
517
518 input.maybe_sign_exit_claim_input(
519 &SECP,
520 &mut shc,
521 &prevouts,
522 i,
523 &keypair
524 )?;
525
526 spent.push(exit_vtxo);
527 }
528 }
529
530 Ok(())
531 }
532
533 pub async fn drain_exits<'a>(
542 &self,
543 inputs: &[impl Borrow<ExitVtxo>],
544 wallet: &Wallet,
545 address: Address,
546 fee_rate_override: Option<FeeRate>,
547 ) -> anyhow::Result<Psbt, ExitError> {
548 if inputs.is_empty() {
549 return Err(ExitError::ClaimMissingInputs);
550 }
551 let mut tx = {
552 let mut output_amount = Amount::ZERO;
553 let mut tx_ins = Vec::with_capacity(inputs.len());
554 for input in inputs {
555 let input = input.borrow();
556 if !matches!(input.state(), ExitState::Claimable(..)) {
557 return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
558 }
559 output_amount += input.vtxo().amount();
560 tx_ins.push(TxIn {
561 previous_output: input.vtxo().point(),
562 script_sig: ScriptBuf::default(),
563 sequence: Sequence::from_height(input.vtxo().exit_delta()),
564 witness: Witness::new(),
565 });
566 }
567 Transaction {
568 version: bitcoin::transaction::Version(3),
569 lock_time: bitcoin::absolute::LockTime::ZERO,
570 input: tx_ins,
571 output: vec![
572 TxOut {
573 script_pubkey: address.script_pubkey(),
574 value: output_amount,
575 },
576 ],
577 }
578 };
579
580 let create_psbt = |tx: Transaction| {
582 let mut psbt = Psbt::from_unsigned_tx(tx)
583 .map_err(|e| ExitError::InternalError {
584 error: format!("Failed to create exit claim PSBT: {}", e),
585 })?;
586 psbt.inputs.iter_mut().zip(inputs).for_each(|(i, v)| {
587 i.set_exit_claim_input(&v.borrow().vtxo());
588 i.witness_utxo = Some(v.borrow().vtxo().txout())
589 });
590 self.sign_exit_claim_inputs(&mut psbt, wallet)
591 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
592 Ok(psbt)
593 };
594 let fee_amount = {
595 let fee_rate = fee_rate_override
596 .unwrap_or(self.chain_source.fee_rates().await.regular);
597 fee_rate * create_psbt(tx.clone())?
598 .extract_tx()
599 .map_err(|e| ExitError::InternalError {
600 error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
601 })?
602 .weight()
603 };
604
605 let needed = fee_amount + P2TR_DUST;
607 if needed > tx.output[0].value {
608 return Err(ExitError::ClaimFeeExceedsOutput {
609 needed, output: tx.output[0].value,
610 });
611 }
612 tx.output[0].value -= fee_amount;
613
614 create_psbt(tx)
616 }
617}