1use std::borrow::Cow;
12use std::fmt;
13
14use bitcoin::{Amount, Transaction};
15use bitcoin::secp256k1::{Keypair, PublicKey};
16use lightning_invoice::Bolt11Invoice;
17
18use ark::{Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
19use ark::vtxo::Full;
20use ark::mailbox::MailboxIdentifier;
21use ark::musig::DangerousSecretNonce;
22use ark::tree::signed::{UnlockHash, VtxoTreeSpec};
23use ark::lightning::{Invoice, PaymentHash, Preimage};
24use ark::rounds::RoundSeq;
25use bitcoin_ext::BlockDelta;
26
27use crate::WalletVtxo;
28use crate::exit::{ExitState, ExitTxOrigin, ExitVtxo};
29use crate::movement::MovementId;
30use crate::lock_manager::LockGuard;
31use crate::round::{AttemptState, RoundFlowState, RoundParticipation, RoundState};
32use crate::vtxo::VtxoState;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct SerdeVtxo {
44 #[serde(with = "ark::encode::serde")]
45 pub vtxo: Vtxo<Full>,
46 pub states: Vec<VtxoState>,
48}
49
50#[derive(Debug, thiserror::Error)]
51#[error("vtxo has no state")]
52pub struct MissingStateError;
53
54impl SerdeVtxo {
55 pub fn current_state(&self) -> Option<&VtxoState> {
56 self.states.last()
57 }
58
59 pub fn to_wallet_vtxo(&self) -> Result<WalletVtxo, MissingStateError> {
60 let state = self.current_state().cloned().ok_or(MissingStateError)?;
61 Ok(wallet_vtxo_from_full(&self.vtxo, state))
62 }
63}
64
65pub(crate) fn wallet_vtxo_from_full(
75 vtxo: &Vtxo<Full>,
76 state: VtxoState,
77) -> WalletVtxo {
78 WalletVtxo {
79 vtxo: vtxo.to_bare(),
80 state,
81 exit_depth: vtxo.exit_depth(),
82 exit_tx_weight: vtxo.transactions().map(|t| t.tx.weight()).sum(),
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SerdeVtxoKey {
89 pub index: u32,
90 pub public_key: PublicKey,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub struct RoundStateId(pub u32);
96
97impl RoundStateId {
98 pub fn to_bytes(&self) -> [u8; 4] {
99 self.0.to_be_bytes()
100 }
101}
102
103impl fmt::Display for RoundStateId {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 fmt::Display::fmt(&self.0, f)
106 }
107}
108
109#[allow(unused)]
110pub struct Locked(Box<dyn LockGuard>);
111
112pub struct Unlocked;
113
114pub struct StoredRoundState<G = Locked> {
115 id: RoundStateId,
116 state: RoundState,
117 _guard: G
118}
119
120impl<G> StoredRoundState<G> {
121 pub fn id(&self) -> RoundStateId {
122 self.id
123 }
124
125 pub fn state(&self) -> &RoundState {
126 &self.state
127 }
128}
129
130impl StoredRoundState<Unlocked> {
131 pub fn new(id: RoundStateId, state: RoundState) -> Self {
132 Self { id, state, _guard: Unlocked }
133 }
134
135 pub fn lock(self, guard: Box<dyn LockGuard>) -> StoredRoundState {
136 StoredRoundState { id: self.id, state: self.state, _guard: Locked(guard) }
137 }
138}
139
140impl StoredRoundState {
141 pub fn state_mut(&mut self) -> &mut RoundState {
142 &mut self.state
143 }
144
145 pub fn unlock(self) -> StoredRoundState<Unlocked> {
146 StoredRoundState { id: self.id, state: self.state, _guard: Unlocked }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct PendingBoard {
153 #[serde(with = "bitcoin_ext::serde::encodable")]
156 pub funding_tx: Transaction,
157 pub vtxos: Vec<VtxoId>,
161 #[serde(with = "bitcoin::amount::serde::as_sat")]
163 pub amount: Amount,
164 pub movement_id: MovementId,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct PendingOffboard {
174 pub movement_id: MovementId,
176 pub offboard_txid: bitcoin::Txid,
178 pub offboard_tx: Transaction,
180 pub vtxo_ids: Vec<VtxoId>,
182 pub destination: String,
184 pub created_at: chrono::DateTime<chrono::Local>,
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
192pub struct LightningSend {
193 pub invoice: Invoice,
195 #[serde(with = "bitcoin::amount::serde::as_sat")]
197 pub amount: Amount,
198 pub fee: Amount,
200 pub htlc_vtxos: Vec<WalletVtxo>,
202 pub movement_id: MovementId,
204 pub preimage: Option<Preimage>,
211 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct LightningReceive {
223 pub payment_hash: PaymentHash,
224 pub payment_preimage: Preimage,
225 pub invoice: Bolt11Invoice,
226 pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
227 pub htlc_vtxos: Vec<WalletVtxo>,
228 pub htlc_recv_cltv_delta: BlockDelta,
229 pub movement_id: Option<MovementId>,
230 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
231}
232
233#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
238pub struct StoredExit {
239 pub vtxo_id: VtxoId,
241 pub state: ExitState,
243 pub history: Vec<ExitState>,
245}
246
247impl StoredExit {
248 pub fn new(exit: &ExitVtxo) -> Self {
250 Self {
251 vtxo_id: exit.id(),
252 state: exit.state().clone(),
253 history: exit.history().clone(),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct SerdeExitChildTx {
261 #[serde(with = "bitcoin_ext::serde::encodable")]
262 pub child_tx: Transaction,
263 pub origin: ExitTxOrigin,
264}
265
266#[derive(Debug, Clone, Deserialize, Serialize)]
267struct SerdeVtxoRequest<'a> {
268 #[serde(with = "bitcoin::amount::serde::as_sat")]
269 amount: Amount,
270 #[serde(with = "ark::encode::serde")]
271 policy: Cow<'a, VtxoPolicy>,
272}
273
274impl<'a> From<&'a VtxoRequest> for SerdeVtxoRequest<'a> {
275 fn from(v: &'a VtxoRequest) -> Self {
276 Self {
277 amount: v.amount,
278 policy: Cow::Borrowed(&v.policy),
279 }
280 }
281}
282
283impl<'a> From<SerdeVtxoRequest<'a>> for VtxoRequest {
284 fn from(v: SerdeVtxoRequest<'a>) -> Self {
285 VtxoRequest {
286 amount: v.amount,
287 policy: v.policy.into_owned(),
288 }
289 }
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294struct SerdeRoundParticipation<'a> {
295 #[serde(with = "ark::encode::serde::cow::vec")]
296 inputs: Cow<'a, [Vtxo<Full>]>,
297 outputs: Vec<SerdeVtxoRequest<'a>>,
298 #[serde(default, skip_serializing_if = "Option::is_none", with = "ark::encode::serde::opt")]
299 unblinded_mailbox_id: Option<MailboxIdentifier>,
300}
301
302impl<'a> From<&'a RoundParticipation> for SerdeRoundParticipation<'a> {
303 fn from(v: &'a RoundParticipation) -> Self {
304 Self {
305 inputs: Cow::Borrowed(&v.inputs),
306 outputs: v.outputs.iter().map(|v| v.into()).collect(),
307 unblinded_mailbox_id: v.unblinded_mailbox_id,
308 }
309 }
310}
311
312impl<'a> From<SerdeRoundParticipation<'a>> for RoundParticipation {
313 fn from(v: SerdeRoundParticipation<'a>) -> Self {
314 Self {
315 inputs: v.inputs.into_owned(),
316 outputs: v.outputs.into_iter().map(|v| v.into()).collect(),
317 unblinded_mailbox_id: v.unblinded_mailbox_id,
318 }
319 }
320}
321
322#[derive(Debug, Serialize, Deserialize)]
324enum SerdeAttemptState<'a> {
325 AwaitingAttempt,
326 AwaitingUnsignedVtxoTree {
327 cosign_keys: Cow<'a, [Keypair]>,
328 secret_nonces: Cow<'a, [Vec<DangerousSecretNonce>]>,
329 unlock_hash: UnlockHash,
330 },
331 AwaitingFinishedRound {
332 #[serde(with = "bitcoin_ext::serde::encodable::cow")]
333 unsigned_round_tx: Cow<'a, Transaction>,
334 #[serde(with = "ark::encode::serde")]
335 vtxos_spec: Cow<'a, VtxoTreeSpec>,
336 unlock_hash: UnlockHash,
337 },
338}
339
340impl<'a> From<&'a AttemptState> for SerdeAttemptState<'a> {
341 fn from(state: &'a AttemptState) -> Self {
342 match state {
343 AttemptState::AwaitingAttempt => SerdeAttemptState::AwaitingAttempt,
344 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
345 SerdeAttemptState::AwaitingUnsignedVtxoTree {
346 cosign_keys: Cow::Borrowed(cosign_keys),
347 secret_nonces: Cow::Borrowed(secret_nonces),
348 unlock_hash: *unlock_hash,
349 }
350 },
351 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
352 SerdeAttemptState::AwaitingFinishedRound {
353 unsigned_round_tx: Cow::Borrowed(unsigned_round_tx),
354 vtxos_spec: Cow::Borrowed(vtxos_spec),
355 unlock_hash: *unlock_hash,
356 }
357 },
358 }
359 }
360}
361
362impl<'a> From<SerdeAttemptState<'a>> for AttemptState {
363 fn from(state: SerdeAttemptState<'a>) -> Self {
364 match state {
365 SerdeAttemptState::AwaitingAttempt => AttemptState::AwaitingAttempt,
366 SerdeAttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
367 AttemptState::AwaitingUnsignedVtxoTree {
368 cosign_keys: cosign_keys.into_owned(),
369 secret_nonces: secret_nonces.into_owned(),
370 unlock_hash: unlock_hash,
371 }
372 },
373 SerdeAttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
374 AttemptState::AwaitingFinishedRound {
375 unsigned_round_tx: unsigned_round_tx.into_owned(),
376 vtxos_spec: vtxos_spec.into_owned(),
377 unlock_hash: unlock_hash,
378 }
379 },
380 }
381 }
382}
383
384#[derive(Debug, Serialize, Deserialize)]
386enum SerdeRoundFlowState<'a> {
387 NonInteractivePending {
389 unlock_hash: UnlockHash,
390 },
391
392 InteractivePending,
394 InteractiveOngoing {
396 round_seq: RoundSeq,
397 attempt_seq: usize,
398 state: SerdeAttemptState<'a>,
399 },
400
401 Finished {
403 funding_tx: Cow<'a, Transaction>,
404 unlock_hash: UnlockHash,
405 },
406
407 Failed {
409 error: Cow<'a, str>,
410 },
411
412 Canceled,
414}
415
416impl<'a> From<&'a RoundFlowState> for SerdeRoundFlowState<'a> {
417 fn from(state: &'a RoundFlowState) -> Self {
418 match state {
419 RoundFlowState::NonInteractivePending { unlock_hash } => {
420 SerdeRoundFlowState::NonInteractivePending {
421 unlock_hash: *unlock_hash,
422 }
423 },
424 RoundFlowState::InteractivePending => SerdeRoundFlowState::InteractivePending,
425 RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
426 SerdeRoundFlowState::InteractiveOngoing {
427 round_seq: *round_seq,
428 attempt_seq: *attempt_seq,
429 state: state.into(),
430 }
431 },
432 RoundFlowState::Finished { funding_tx, unlock_hash } => {
433 SerdeRoundFlowState::Finished {
434 funding_tx: Cow::Borrowed(funding_tx),
435 unlock_hash: *unlock_hash,
436 }
437 },
438 RoundFlowState::Failed { error } => {
439 SerdeRoundFlowState::Failed {
440 error: Cow::Borrowed(error),
441 }
442 },
443 RoundFlowState::Canceled => SerdeRoundFlowState::Canceled,
444 }
445 }
446}
447
448impl<'a> From<SerdeRoundFlowState<'a>> for RoundFlowState {
449 fn from(state: SerdeRoundFlowState<'a>) -> Self {
450 match state {
451 SerdeRoundFlowState::NonInteractivePending { unlock_hash } => {
452 RoundFlowState::NonInteractivePending { unlock_hash }
453 },
454 SerdeRoundFlowState::InteractivePending => RoundFlowState::InteractivePending,
455 SerdeRoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
456 RoundFlowState::InteractiveOngoing {
457 round_seq: round_seq,
458 attempt_seq: attempt_seq,
459 state: state.into(),
460 }
461 },
462 SerdeRoundFlowState::Finished { funding_tx, unlock_hash } => {
463 RoundFlowState::Finished {
464 funding_tx: funding_tx.into_owned(),
465 unlock_hash,
466 }
467 },
468 SerdeRoundFlowState::Failed { error } => {
469 RoundFlowState::Failed {
470 error: error.into_owned(),
471 }
472 },
473 SerdeRoundFlowState::Canceled => RoundFlowState::Canceled,
474 }
475 }
476}
477
478#[derive(Debug, Serialize, Deserialize)]
480pub struct SerdeRoundState<'a> {
481 done: bool,
482 participation: SerdeRoundParticipation<'a>,
483 movement_id: Option<MovementId>,
484 flow: SerdeRoundFlowState<'a>,
485 #[serde(with = "ark::encode::serde::cow::vec")]
486 new_vtxos: Cow<'a, [Vtxo<Full>]>,
487 sent_forfeit_sigs: bool,
488}
489
490impl<'a> From<&'a RoundState> for SerdeRoundState<'a> {
491 fn from(state: &'a RoundState) -> Self {
492 Self {
493 done: state.done,
494 participation: (&state.participation).into(),
495 movement_id: state.movement_id,
496 flow: (&state.flow).into(),
497 new_vtxos: Cow::Borrowed(&state.new_vtxos),
498 sent_forfeit_sigs: state.sent_forfeit_sigs,
499 }
500 }
501}
502
503impl<'a> From<SerdeRoundState<'a>> for RoundState {
504 fn from(state: SerdeRoundState<'a>) -> Self {
505 Self {
506 done: state.done,
507 participation: state.participation.into(),
508 movement_id: state.movement_id,
509 flow: state.flow.into(),
510 new_vtxos: state.new_vtxos.into_owned(),
511 sent_forfeit_sigs: state.sent_forfeit_sigs,
512 }
513 }
514}
515
516#[cfg(test)]
517mod test {
518 use crate::exit::{ExitState, ExitTxOrigin};
519 use crate::vtxo::VtxoState;
520
521 #[test]
522 fn test_serialized_structs() {
525 let serialised = r#"{"type":"start","tip_height":119}"#;
527 serde_json::from_str::<ExitState>(serialised).unwrap();
528 let serialised = r#"{"type":"awaiting-delta","tip_height":122,"confirmed_block":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f","claimable_height":134}"#;
529 serde_json::from_str::<ExitState>(serialised).unwrap();
530 let serialised = r#"{"type":"claimable","tip_height":134,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block":null}"#;
531 serde_json::from_str::<ExitState>(serialised).unwrap();
532 let serialised = r#"{"type":"claimable","tip_height":140,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block": "139:c6e9eb8c8b4d9620bbe87b94d7fb0fbb8eef1c4a8c1e60f7b3a5d80fe26b0d3e"}"#;
533 serde_json::from_str::<ExitState>(serialised).unwrap();
534 let serialised = r#"{"type":"claim-in-progress","tip_height":134, "claimable_since": "134:6585896bdda6f08d924bf45cc2b16418af56703b3c50930e4dccbc1728d3800a","claim_txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c"}"#;
535 serde_json::from_str::<ExitState>(serialised).unwrap();
536 let serialised = r#"{"type":"claimed","tip_height":134,"txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c","block": "122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f"}"#;
537 serde_json::from_str::<ExitState>(serialised).unwrap();
538
539 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"verify-inputs"}}]}"#;
543 serde_json::from_str::<ExitState>(serialised).unwrap();
544 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-input-confirmation","txids":["ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de"]}}]}"#;
545 serde_json::from_str::<ExitState>(serialised).unwrap();
546 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-cpfp-broadcast"}}]}"#;
547 serde_json::from_str::<ExitState>(serialised).unwrap();
548 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-confirmation","child_txid":"ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de","origin":{"type":"wallet","confirmed_in":null}}}]}"#;
549 serde_json::from_str::<ExitState>(serialised).unwrap();
550 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-confirmation","child_txid":"ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de","origin":{"type":"mempool","fee_rate_kwu":25000,"total_fee":27625}}}]}"#;
551 serde_json::from_str::<ExitState>(serialised).unwrap();
552 let serialised = r#"{"type":"processing","tip_height":134,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"confirmed","child_txid":"ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de","block":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f","origin":{"type":"block","confirmed_in":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f"}}}]}"#;
553 serde_json::from_str::<ExitState>(serialised).unwrap();
554
555 let serialized = r#"{"type":"wallet","confirmed_in":null}"#;
557 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
558 let serialized = r#"{"type":"wallet","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
559 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
560 let serialized = r#"{"type":"mempool"}"#;
562 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
563 let serialized = r#"{"type":"mempool","fee_rate_kwu":25000,"total_fee":27625}"#;
565 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
566 let serialized = r#"{"type":"block","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
567 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
568
569 let serialised = r#"{"type": "spendable"}"#;
571 serde_json::from_str::<VtxoState>(serialised).unwrap();
572 let serialised = r#"{"type": "spent"}"#;
573 serde_json::from_str::<VtxoState>(serialised).unwrap();
574 let serialised = r#"{"type": "locked", "movement_id": null}"#;
575 serde_json::from_str::<VtxoState>(serialised).unwrap();
576 let serialised = r#"{"type": "locked", "movement_id": 42}"#;
577 serde_json::from_str::<VtxoState>(serialised).unwrap();
578 }
579}