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::policy::signing::VtxoSigner;
141use bitcoin_ext::{BlockHeight, P2TR_DUST};
142
143use crate::Wallet;
144use crate::chain::ChainSource;
145use crate::exit::transaction_manager::ExitTransactionManager;
146use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
147use crate::movement::manager::MovementManager;
148use crate::movement::update::MovementUpdate;
149use crate::onchain::ExitUnilaterally;
150use crate::persist::BarkPersister;
151use crate::persist::models::StoredExit;
152use crate::psbtext::PsbtInputExt;
153use crate::subsystem::{ExitMovement, Subsystem};
154use crate::vtxo::{VtxoState, VtxoStateKind};
155
156pub struct Exit {
158 tx_manager: ExitTransactionManager,
159 persister: Arc<dyn BarkPersister>,
160 chain_source: Arc<ChainSource>,
161 movement_manager: Arc<MovementManager>,
162
163 exit_vtxos: Vec<ExitVtxo>,
164}
165
166impl Exit {
167 pub (crate) async fn new(
168 persister: Arc<dyn BarkPersister>,
169 chain_source: Arc<ChainSource>,
170 movement_manager: Arc<MovementManager>,
171 ) -> anyhow::Result<Exit> {
172 let tx_manager = ExitTransactionManager::new(persister.clone(), chain_source.clone())?;
173
174 Ok(Exit {
175 exit_vtxos: Vec::new(),
176 tx_manager,
177 persister,
178 chain_source,
179 movement_manager,
180 })
181 }
182
183 pub (crate) async fn load(
184 &mut self,
185 onchain: &dyn ExitUnilaterally,
186 ) -> anyhow::Result<()> {
187 let exit_vtxo_entries = self.persister.get_exit_vtxo_entries().await?;
188 self.exit_vtxos.reserve(exit_vtxo_entries.len());
189
190 for entry in exit_vtxo_entries {
191 if let Some(vtxo) = self.persister.get_wallet_vtxo(entry.vtxo_id).await? {
192 let mut exit = ExitVtxo::from_entry(entry, &vtxo);
193 exit.initialize(&mut self.tx_manager, &*self.persister, onchain).await?;
194 self.exit_vtxos.push(exit);
195 } else {
196 error!("VTXO {} is marked for exit but it's missing from the database", entry.vtxo_id);
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 mut txs = Vec::new();
218 if include_transactions {
219 if let Some(txids) = exit.txids() {
220 txs.reserve(txids.len());
221 for txid in txids {
222 txs.push(self.tx_manager.get_package(*txid)?.read().await.clone());
223 }
224 } else {
225 let exit_vtxo = exit.get_vtxo(&*self.persister).await?;
226 for tx in exit_vtxo.vtxo.transactions() {
231 txs.push(ExitTransactionPackage {
232 exit: TransactionInfo {
233 txid: tx.tx.compute_txid(),
234 tx: tx.tx,
235 },
236 child: None,
237 })
238 }
239 }
240 }
241 Ok(Some(ExitTransactionStatus {
242 vtxo_id: exit.id(),
243 state: exit.state().clone(),
244 history: if include_history { Some(exit.history().clone()) } else { None },
245 transactions: txs,
246 }))
247 },
248 }
249 }
250
251 pub fn get_exit_vtxo(&self, vtxo_id: VtxoId) -> Option<&ExitVtxo> {
253 self.exit_vtxos.iter().find(|ev| ev.id() == vtxo_id)
254 }
255
256 pub fn get_exit_vtxos(&self) -> &Vec<ExitVtxo> {
258 &self.exit_vtxos
259 }
260
261 pub fn has_pending_exits(&self) -> bool {
263 self.exit_vtxos.iter().any(|ev| ev.state().is_pending())
264 }
265
266 pub fn pending_total(&self) -> Amount {
268 self.exit_vtxos
269 .iter()
270 .filter_map(|ev| {
271 if ev.state().is_pending() {
272 Some(ev.amount())
273 } else {
274 None
275 }
276 }).sum()
277 }
278
279 pub async fn all_claimable_at_height(&self) -> Option<BlockHeight> {
281 let mut highest_claimable_height = None;
282 for exit in &self.exit_vtxos {
283 if matches!(exit.state(), ExitState::Claimed(..)) {
284 continue;
285 }
286 match exit.state().claimable_height() {
287 Some(h) => highest_claimable_height = cmp::max(highest_claimable_height, Some(h)),
288 None => return None,
289 }
290 }
291 highest_claimable_height
292 }
293
294 pub async fn start_exit_for_entire_wallet(&mut self) -> anyhow::Result<()> {
301 let vtxos = self.persister.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?.into_iter()
302 .map(|v| v.vtxo)
303 .collect::<Vec<_>>();
304 self.start_exit_for_vtxos(&vtxos).await?;
305
306 Ok(())
307 }
308
309 pub async fn start_exit_for_vtxos<'a>(
316 &mut self,
317 vtxos: &[impl Borrow<Vtxo>],
318 ) -> anyhow::Result<()> {
319 if vtxos.is_empty() {
320 return Ok(());
321 }
322 let tip = self.chain_source.tip().await?;
323 let params = Params::new(self.chain_source.network());
324 for vtxo in vtxos {
325 let vtxo = vtxo.borrow();
326 let vtxo_id = vtxo.id();
327 if self.exit_vtxos.iter().any(|ev| ev.id() == vtxo_id) {
328 warn!("VTXO {} is already in the exit process", vtxo_id);
329 continue;
330 }
331
332 trace!("Starting exit for VTXO: {}", vtxo_id);
335 let exit = ExitVtxo::new(vtxo, tip);
336 self.persister.store_exit_vtxo_entry(&StoredExit::new(&exit)).await?;
337 self.persister.update_vtxo_state_checked(
338 vtxo_id, VtxoState::Spent, &VtxoStateKind::UNSPENT_STATES,
339 ).await?;
340 self.exit_vtxos.push(exit);
341 trace!("Exit for VTXO started successfully: {}", vtxo_id);
342
343 let balance = -vtxo.amount().to_signed()?;
345 let script_pubkey = vtxo.output_script_pubkey();
346 let payment_method = match Address::from_script(&script_pubkey, ¶ms) {
347 Ok(addr) => PaymentMethod::Bitcoin(addr.into_unchecked()),
348 Err(e) => {
349 warn!("Unable to convert script pubkey to address: {:#}", e);
350 PaymentMethod::OutputScript(script_pubkey)
351 }
352 };
353
354 self.movement_manager.new_finished_movement(
358 Subsystem::EXIT,
359 ExitMovement::Exit.to_string(),
360 MovementStatus::Successful,
361 MovementUpdate::new()
362 .intended_and_effective_balance(balance)
363 .consumed_vtxo(vtxo_id)
364 .sent_to([MovementDestination::new(payment_method, vtxo.amount())]),
365 ).await.context("Failed to register exit movement")?;
366 }
367 Ok(())
368 }
369
370 pub (crate) async fn dangerous_clear_exit(&mut self) -> anyhow::Result<()> {
374 for exit in &self.exit_vtxos {
375 self.persister.remove_exit_vtxo_entry(&exit.id()).await?;
376 }
377 self.exit_vtxos.clear();
378 Ok(())
379 }
380
381 pub async fn progress_exits(
396 &mut self,
397 wallet: &Wallet,
398 onchain: &mut dyn ExitUnilaterally,
399 fee_rate_override: Option<FeeRate>,
400 ) -> anyhow::Result<Option<Vec<ExitProgressStatus>>> {
401 let mut exit_statuses = Vec::with_capacity(self.exit_vtxos.len());
402 for ev in self.exit_vtxos.iter_mut() {
403 if !ev.is_initialized() {
404 warn!("Skipping progress of uninitialized unilateral exit {}", ev.id());
405 continue;
406 }
407
408 info!("Progressing exit for VTXO {}", ev.id());
409 let error = match ev.progress(
410 wallet,
411 &mut self.tx_manager,
412 onchain,
413 fee_rate_override,
414 true,
415 ).await {
416 Ok(_) => None,
417 Err(e) => {
418 match &e {
419 ExitError::InsufficientConfirmedFunds { .. } => {
420 warn!("Can't progress exit for VTXO {} at this time: {}", ev.id(), e);
421 },
422 _ => {
423 error!("Error progressing exit for VTXO {}: {}", ev.id(), e);
424 }
425 }
426 Some(e)
427 }
428 };
429 if !matches!(ev.state(), ExitState::Claimed(..)) {
430 exit_statuses.push(ExitProgressStatus {
431 vtxo_id: ev.id(),
432 state: ev.state().clone(),
433 error,
434 });
435 }
436 }
437 Ok(Some(exit_statuses))
438 }
439
440 pub async fn sync(
444 &mut self,
445 wallet: &Wallet,
446 onchain: &mut dyn ExitUnilaterally,
447 ) -> anyhow::Result<()> {
448 self.sync_no_progress(onchain).await?;
449 for exit in &mut self.exit_vtxos {
450 if exit.state().requires_network_update() {
452 if let Err(e) = exit.progress(
453 wallet, &mut self.tx_manager, onchain, None, false,
454 ).await {
455 error!("Error syncing exit for VTXO {}: {}", exit.id(), e);
456 }
457 }
458 }
459 Ok(())
460 }
461
462 pub async fn sync_no_progress(
467 &mut self,
468 onchain: &dyn ExitUnilaterally,
469 ) -> anyhow::Result<()> {
470 for exit in &mut self.exit_vtxos {
471 if !exit.is_initialized() {
472 match exit.initialize(&mut self.tx_manager, &*self.persister, onchain).await {
473 Ok(()) => continue,
474 Err(e) => {
475 error!("Error initializing exit for VTXO {}: {:#}", exit.id(), e);
476 }
477 }
478 }
479 }
480 self.tx_manager.sync().await?;
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 async 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(|e| (e.id(), e))
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 claimable yet")?;
515
516 let witness = wallet.sign_input(&vtxo, i, &mut shc, &prevouts).await
517 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
518
519 input.final_script_witness = Some(witness);
520 spent.push(exit_vtxo);
521 }
522 }
523
524 Ok(())
525 }
526
527 pub async fn drain_exits<'a>(
536 &self,
537 inputs: &[impl Borrow<ExitVtxo>],
538 wallet: &Wallet,
539 address: Address,
540 fee_rate_override: Option<FeeRate>,
541 ) -> anyhow::Result<Psbt, ExitError> {
542 let tip = self.chain_source.tip().await
543 .map_err(|e| ExitError::TipRetrievalFailure { error: e.to_string() })?;
544
545 if inputs.is_empty() {
546 return Err(ExitError::ClaimMissingInputs);
547 }
548 let mut vtxos = HashMap::with_capacity(inputs.len());
549 for input in inputs {
550 let i = input.borrow();
551 let vtxo = i.get_vtxo(&*self.persister).await?;
552 vtxos.insert(i.id(), vtxo);
553 }
554
555 let mut tx = {
556 let mut output_amount = Amount::ZERO;
557 let mut tx_ins = Vec::with_capacity(inputs.len());
558 for input in inputs {
559 let input = input.borrow();
560 let vtxo = &vtxos[&input.id()];
561 if !matches!(input.state(), ExitState::Claimable(..)) {
562 return Err(ExitError::VtxoNotClaimable { vtxo: input.id() });
563 }
564
565 output_amount += vtxo.amount();
566
567 let clause = wallet.find_signable_clause(vtxo).await
568 .ok_or(ExitError::ClaimMissingSignableClause { vtxo: vtxo.id() })?;
569
570 tx_ins.push(TxIn {
571 previous_output: vtxo.point(),
572 script_sig: ScriptBuf::default(),
573 sequence: clause.sequence().unwrap_or(Sequence::ZERO),
574 witness: Witness::new(),
575 });
576 }
577
578 let locktime = bitcoin::absolute::LockTime::from_height(tip)
579 .map_err(|e| ExitError::InvalidLocktime { tip, error: e.to_string() })?;
580
581 Transaction {
582 version: bitcoin::transaction::Version(3),
583 lock_time: locktime,
584 input: tx_ins,
585 output: vec![
586 TxOut {
587 script_pubkey: address.script_pubkey(),
588 value: output_amount,
589 },
590 ],
591 }
592 };
593
594 let create_psbt = |tx: Transaction| async {
596 let mut psbt = Psbt::from_unsigned_tx(tx)
597 .map_err(|e| ExitError::InternalError {
598 error: format!("Failed to create exit claim PSBT: {}", e),
599 })?;
600 psbt.inputs.iter_mut().zip(inputs).for_each(|(i, e)| {
601 let v = &vtxos[&e.borrow().id()];
602 i.set_exit_claim_input(&v.vtxo);
603 i.witness_utxo = Some(v.vtxo.txout())
604 });
605 self.sign_exit_claim_inputs(&mut psbt, wallet).await
606 .map_err(|e| ExitError::ClaimSigningError { error: e.to_string() })?;
607 Ok(psbt)
608 };
609 let fee_amount = {
610 let fee_rate = fee_rate_override
611 .unwrap_or(self.chain_source.fee_rates().await.regular);
612 fee_rate * create_psbt(tx.clone()).await?
613 .extract_tx()
614 .map_err(|e| ExitError::InternalError {
615 error: format!("Failed to get tx from signed exit claim PSBT: {}", e),
616 })?
617 .weight()
618 };
619
620 let needed = fee_amount + P2TR_DUST;
622 if needed > tx.output[0].value {
623 return Err(ExitError::ClaimFeeExceedsOutput {
624 needed, output: tx.output[0].value,
625 });
626 }
627 tx.output[0].value -= fee_amount;
628
629 create_psbt(tx).await
631 }
632}