1
2mod error;
3mod package;
4mod states;
5
6pub use self::package::{
7 ChildTransactionInfo, ExitCpfpRequest, ExitTransactionPackage, FeeInfo, RbfRequirement,
8 TransactionInfo,
9};
10pub use self::error::ExitError;
11pub use self::states::{
12 ExitTx, ExitTxStatus, ExitTxOrigin, ExitStartState, ExitProcessingState, ExitAwaitingDeltaState,
13 ExitClaimableState, ExitClaimInProgressState, ExitClaimedState, ExitVtxoAlreadySpentState,
14 ExitCanceledState,
15};
16
17use std::fmt;
18
19use ark::VtxoId;
20use bitcoin::Txid;
21
22use bitcoin_ext::{BlockDelta, BlockHeight, BlockRef, TxStatus};
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(tag = "type", rename_all = "kebab-case")]
28pub enum ExitState {
29 Start(ExitStartState),
30 Processing(ExitProcessingState),
31 AwaitingDelta(ExitAwaitingDeltaState),
32 Claimable(ExitClaimableState),
33 ClaimInProgress(ExitClaimInProgressState),
34 Claimed(ExitClaimedState),
40 VtxoAlreadySpent(ExitVtxoAlreadySpentState),
44 Canceled(ExitCanceledState),
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "kebab-case")]
57pub enum ExitStateKind {
58 Start,
59 Processing,
60 AwaitingDelta,
61 Claimable,
62 ClaimInProgress,
63 Claimed,
64 VtxoAlreadySpent,
65 Canceled,
66}
67
68impl ExitStateKind {
69 pub const ALL: &[ExitStateKind] = &[
71 ExitStateKind::Start,
72 ExitStateKind::Processing,
73 ExitStateKind::AwaitingDelta,
74 ExitStateKind::Claimable,
75 ExitStateKind::ClaimInProgress,
76 ExitStateKind::Claimed,
77 ExitStateKind::VtxoAlreadySpent,
78 ExitStateKind::Canceled,
79 ];
80
81 pub const LIVE_STATES: &[ExitStateKind] = &[
83 ExitStateKind::Start,
84 ExitStateKind::Processing,
85 ExitStateKind::AwaitingDelta,
86 ExitStateKind::Claimable,
87 ExitStateKind::ClaimInProgress,
88 ];
89
90 pub const FINISHED_STATES: &[ExitStateKind] = &[
93 ExitStateKind::Claimed,
94 ExitStateKind::VtxoAlreadySpent,
95 ExitStateKind::Canceled,
96 ];
97
98 pub fn as_str(&self) -> &'static str {
102 match self {
103 ExitStateKind::Start => "start",
104 ExitStateKind::Processing => "processing",
105 ExitStateKind::AwaitingDelta => "awaiting-delta",
106 ExitStateKind::Claimable => "claimable",
107 ExitStateKind::ClaimInProgress => "claim-in-progress",
108 ExitStateKind::Claimed => "claimed",
109 ExitStateKind::VtxoAlreadySpent => "vtxo-already-spent",
110 ExitStateKind::Canceled => "canceled",
111 }
112 }
113}
114
115impl fmt::Display for ExitStateKind {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.write_str(self.as_str())
118 }
119}
120
121impl ExitState {
122 pub fn kind(&self) -> ExitStateKind {
124 match self {
125 ExitState::Start(_) => ExitStateKind::Start,
126 ExitState::Processing(_) => ExitStateKind::Processing,
127 ExitState::AwaitingDelta(_) => ExitStateKind::AwaitingDelta,
128 ExitState::Claimable(_) => ExitStateKind::Claimable,
129 ExitState::ClaimInProgress(_) => ExitStateKind::ClaimInProgress,
130 ExitState::Claimed(_) => ExitStateKind::Claimed,
131 ExitState::VtxoAlreadySpent(_) => ExitStateKind::VtxoAlreadySpent,
132 ExitState::Canceled(_) => ExitStateKind::Canceled,
133 }
134 }
135
136 pub fn new_start(tip: BlockHeight) -> Self {
137 ExitState::Start(ExitStartState { tip_height: tip })
138 }
139
140 pub fn new_processing<T: IntoIterator<Item = Txid>>(tip: BlockHeight, txids: T) -> Self {
141 ExitState::Processing(ExitProcessingState {
142 tip_height: tip,
143 transactions: txids.into_iter()
144 .map(|id| ExitTx {
145 txid: id,
146 status: ExitTxStatus::VerifyInputs,
147 })
148 .collect::<Vec<_>>(),
149 })
150 }
151
152 pub fn new_processing_from_transactions(tip: BlockHeight, transactions: Vec<ExitTx>) -> Self {
153 ExitState::Processing(ExitProcessingState {
154 tip_height: tip,
155 transactions,
156 })
157 }
158
159 pub fn new_awaiting_delta(
160 tip: BlockHeight,
161 confirmed_block: BlockRef,
162 wait_delta: BlockDelta
163 ) -> Self {
164 debug_assert_ne!(wait_delta, 0, "wait delta must be non-zero");
165 let claimable_height = confirmed_block.height + wait_delta as BlockHeight;
166 ExitState::AwaitingDelta(ExitAwaitingDeltaState {
167 tip_height: tip,
168 confirmed_block,
169 claimable_height,
170 })
171 }
172
173 pub fn new_claimable(
174 tip: BlockHeight,
175 claimable_since: BlockRef,
176 last_scanned_block: Option<BlockRef>
177 ) -> Self {
178 ExitState::Claimable(ExitClaimableState {
179 tip_height: tip,
180 claimable_since,
181 last_scanned_block,
182 })
183 }
184
185 pub fn new_claim_in_progress(
186 tip: BlockHeight,
187 claimable_since: BlockRef,
188 claim_txid: Txid
189 ) -> Self {
190 ExitState::ClaimInProgress(ExitClaimInProgressState {
191 tip_height: tip,
192 claimable_since,
193 claim_txid,
194 })
195 }
196
197 pub fn new_claimed(tip: BlockHeight, txid: Txid, block: BlockRef) -> Self {
198 ExitState::Claimed(ExitClaimedState {
199 tip_height: tip,
200 txid,
201 block,
202 })
203 }
204
205 pub fn new_vtxo_already_spent(tip: BlockHeight) -> Self {
206 ExitState::VtxoAlreadySpent(ExitVtxoAlreadySpentState { tip_height: tip })
207 }
208
209 pub fn new_canceled(tip: BlockHeight) -> Self {
210 ExitState::Canceled(ExitCanceledState { tip_height: tip })
211 }
212
213 pub fn is_pending(&self) -> bool {
218 match self {
219 ExitState::Start(_) => true,
220 ExitState::Processing(_) => true,
221 ExitState::AwaitingDelta(_) => true,
222 _ => false,
223 }
224 }
225
226 pub fn is_claimable(&self) -> bool {
229 match self {
230 ExitState::Claimable(_) => true,
231 _ => false,
232 }
233 }
234
235 pub fn is_cancelable(&self) -> bool {
239 match self {
240 ExitState::Start(_) => true,
241 ExitState::Processing(s) => s.transactions.last().map_or(true, |tx| matches!(
242 tx.status,
243 ExitTxStatus::VerifyInputs
244 | ExitTxStatus::AwaitingInputConfirmation { .. }
245 | ExitTxStatus::AwaitingCpfpBroadcast,
246 )),
247 _ => false,
248 }
249 }
250
251 pub fn requires_confirmations(&self) -> bool {
252 match self {
253 ExitState::Processing(s) => {
254 s.transactions.iter().any(|s| match s.status {
255 ExitTxStatus::AwaitingInputConfirmation { .. } => true,
256 ExitTxStatus::AwaitingConfirmation { .. } => true,
257 _ => false,
258 })
259 },
260 ExitState::AwaitingDelta(_) => true,
261 ExitState::ClaimInProgress(_) => true,
262 _ => false,
263 }
264 }
265
266 pub fn claimable_height(&self) -> Option<BlockHeight> {
267 match self {
268 ExitState::AwaitingDelta(s) => Some(s.claimable_height),
269 ExitState::Claimable(s) => Some(s.claimable_since.height),
270 ExitState::ClaimInProgress(s) => Some(s.claimable_since.height),
271 _ => None,
272 }
273 }
274
275 pub fn warrants_exited_vtxo(&self) -> bool {
285 match self {
286 ExitState::Start(_) => false,
287 ExitState::Processing(_) => false,
288 ExitState::AwaitingDelta(_) => true,
289 ExitState::Claimable(_) => true,
290 ExitState::ClaimInProgress(_) => true,
291 ExitState::Claimed(_) => true,
292 ExitState::VtxoAlreadySpent(_) => false,
293 ExitState::Canceled(_) => false,
294 }
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct ExitProgressStatus {
300 pub vtxo_id: VtxoId,
302 pub state: ExitState,
304 pub error: Option<ExitError>,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct ExitTransactionStatus {
310 pub vtxo_id: VtxoId,
312 pub state: ExitState,
314 pub history: Option<Vec<ExitState>>,
316 pub transactions: Vec<ExitTransactionPackage>,
318}
319
320#[derive(Clone, Copy, Debug, Eq, PartialEq)]
321pub struct ExitChildStatus {
322 pub txid: Txid,
323 pub status: TxStatus,
324 pub origin: ExitTxOrigin,
325 pub fee_info: Option<FeeInfo>,
326}
327
328#[cfg(test)]
329mod test {
330 use super::*;
331
332 use bitcoin::hashes::Hash;
333
334 fn txid(n: u8) -> Txid {
335 Txid::from_byte_array([n; 32])
336 }
337
338 fn tx(n: u8, status: ExitTxStatus) -> ExitTx {
339 ExitTx { txid: txid(n), status }
340 }
341
342 fn broadcast() -> ExitTxStatus {
344 ExitTxStatus::AwaitingConfirmation { child_txid: txid(99), origin: ExitTxOrigin::Mempool }
345 }
346
347 fn all_states() -> [ExitState; 8] {
349 let block_ref = BlockRef { height: 1, hash: bitcoin::BlockHash::all_zeros() };
350 [
351 ExitState::new_start(1),
352 ExitState::new_processing(1, [txid(1)]),
353 ExitState::new_awaiting_delta(1, block_ref, 10),
354 ExitState::new_claimable(1, block_ref, Some(block_ref)),
355 ExitState::new_claim_in_progress(1, block_ref, txid(1)),
356 ExitState::new_claimed(1, txid(1), block_ref),
357 ExitState::new_vtxo_already_spent(1),
358 ExitState::new_canceled(1),
359 ]
360 }
361
362 #[test]
363 fn is_cancelable_only_checks_the_final_tx() {
364 assert!(ExitState::new_start(100).is_cancelable());
366
367 assert!(ExitState::new_processing_from_transactions(100, vec![
369 tx(1, ExitTxStatus::VerifyInputs),
370 tx(2, ExitTxStatus::AwaitingCpfpBroadcast),
371 ]).is_cancelable());
372
373 assert!(ExitState::new_processing_from_transactions(100, vec![
376 tx(1, broadcast()),
377 tx(2, ExitTxStatus::AwaitingCpfpBroadcast),
378 ]).is_cancelable());
379
380 assert!(!ExitState::new_processing_from_transactions(100, vec![
382 tx(1, broadcast()),
383 tx(2, broadcast()),
384 ]).is_cancelable());
385
386 assert!(!ExitState::new_canceled(100).is_cancelable());
388 assert!(!ExitState::new_vtxo_already_spent(100).is_cancelable());
389 }
390
391 #[test]
392 fn exit_state_kind_tag_matches_serde() {
393 for state in all_states() {
396 let state_tag = serde_json::to_value(&state).unwrap()
397 .get("type").unwrap().as_str().unwrap().to_string();
398 let kind_tag = serde_json::to_value(state.kind()).unwrap()
399 .as_str().unwrap().to_string();
400 assert_eq!(state_tag, kind_tag, "tag mismatch for {:?}", state.kind());
401 }
402
403 for &kind in ExitStateKind::ALL {
406 let serde_tag = serde_json::to_value(kind).unwrap().as_str().unwrap().to_string();
407 assert_eq!(kind.as_str(), serde_tag, "as_str mismatch for {:?}", kind);
408 }
409 }
410
411 #[test]
412 fn exit_state_kind_all_is_exhaustive() {
413 match ExitStateKind::Start {
416 ExitStateKind::Start => {},
417 ExitStateKind::Processing => {},
418 ExitStateKind::AwaitingDelta => {},
419 ExitStateKind::Claimable => {},
420 ExitStateKind::ClaimInProgress => {},
421 ExitStateKind::Claimed => {},
422 ExitStateKind::VtxoAlreadySpent => {},
423 ExitStateKind::Canceled => {},
424 }
425
426 assert_eq!(ExitStateKind::ALL.len(), all_states().len());
428 for state in all_states() {
429 let count = ExitStateKind::ALL.iter().filter(|&&k| k == state.kind()).count();
430 assert_eq!(count, 1, "{:?} should appear exactly once in ALL", state.kind());
431 }
432 }
433
434 #[test]
435 fn exit_state_kind_live_and_finished_states_partition_all() {
436 for &kind in ExitStateKind::ALL {
438 let live = !matches!(
439 kind,
440 ExitStateKind::Claimed
441 | ExitStateKind::VtxoAlreadySpent
442 | ExitStateKind::Canceled,
443 );
444 assert_eq!(
445 ExitStateKind::LIVE_STATES.contains(&kind), live,
446 "LIVE_STATES membership wrong for {:?}", kind,
447 );
448 assert_eq!(
449 ExitStateKind::FINISHED_STATES.contains(&kind), !live,
450 "FINISHED_STATES membership wrong for {:?}", kind,
451 );
452 }
453
454 for state in all_states() {
457 if state.is_pending() {
458 assert!(
459 ExitStateKind::LIVE_STATES.contains(&state.kind()),
460 "{:?} is_pending() but its kind is not in LIVE_STATES", state.kind(),
461 );
462 }
463 }
464 }
465}