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::{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<Locked> {
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)]
194pub struct PaidInvoice {
195 pub payment_hash: PaymentHash,
196 pub preimage: Preimage,
197 pub paid_at: chrono::DateTime<chrono::Local>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct LightningReceive {
208 pub payment_hash: PaymentHash,
209 pub payment_preimage: Preimage,
210 pub invoice: Bolt11Invoice,
211 pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
212 pub htlc_vtxos: Vec<WalletVtxo>,
213 pub htlc_recv_cltv_delta: BlockDelta,
214 pub movement_id: Option<MovementId>,
215 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
216}
217
218#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub struct StoredExit {
224 pub vtxo_id: VtxoId,
226 pub state: ExitState,
228 pub history: Vec<ExitState>,
230}
231
232impl StoredExit {
233 pub fn new(exit: &ExitVtxo) -> Self {
235 Self {
236 vtxo_id: exit.id(),
237 state: exit.state().clone(),
238 history: exit.history().clone(),
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct SerdeExitChildTx {
246 #[serde(with = "bitcoin_ext::serde::encodable")]
247 pub child_tx: Transaction,
248 pub origin: ExitTxOrigin,
249}
250
251#[derive(Debug, Clone, Deserialize, Serialize)]
252struct SerdeVtxoRequest<'a> {
253 #[serde(with = "bitcoin::amount::serde::as_sat")]
254 amount: Amount,
255 #[serde(with = "ark::encode::serde")]
256 policy: Cow<'a, VtxoPolicy>,
257}
258
259impl<'a> From<&'a VtxoRequest> for SerdeVtxoRequest<'a> {
260 fn from(v: &'a VtxoRequest) -> Self {
261 Self {
262 amount: v.amount,
263 policy: Cow::Borrowed(&v.policy),
264 }
265 }
266}
267
268impl<'a> From<SerdeVtxoRequest<'a>> for VtxoRequest {
269 fn from(v: SerdeVtxoRequest<'a>) -> Self {
270 VtxoRequest {
271 amount: v.amount,
272 policy: v.policy.into_owned(),
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279struct SerdeRoundParticipation<'a> {
280 #[serde(with = "ark::encode::serde::cow::vec")]
281 inputs: Cow<'a, [Vtxo<Full>]>,
282 outputs: Vec<SerdeVtxoRequest<'a>>,
283 #[serde(default, skip_serializing_if = "Option::is_none", with = "ark::encode::serde::opt")]
284 unblinded_mailbox_id: Option<MailboxIdentifier>,
285}
286
287impl<'a> From<&'a RoundParticipation> for SerdeRoundParticipation<'a> {
288 fn from(v: &'a RoundParticipation) -> Self {
289 Self {
290 inputs: Cow::Borrowed(&v.inputs),
291 outputs: v.outputs.iter().map(|v| v.into()).collect(),
292 unblinded_mailbox_id: v.unblinded_mailbox_id,
293 }
294 }
295}
296
297impl<'a> From<SerdeRoundParticipation<'a>> for RoundParticipation {
298 fn from(v: SerdeRoundParticipation<'a>) -> Self {
299 Self {
300 inputs: v.inputs.into_owned(),
301 outputs: v.outputs.into_iter().map(|v| v.into()).collect(),
302 unblinded_mailbox_id: v.unblinded_mailbox_id,
303 }
304 }
305}
306
307#[derive(Debug, Serialize, Deserialize)]
309enum SerdeAttemptState<'a> {
310 AwaitingAttempt,
311 AwaitingUnsignedVtxoTree {
312 cosign_keys: Cow<'a, [Keypair]>,
313 secret_nonces: Cow<'a, [Vec<DangerousSecretNonce>]>,
314 unlock_hash: UnlockHash,
315 },
316 AwaitingFinishedRound {
317 #[serde(with = "bitcoin_ext::serde::encodable::cow")]
318 unsigned_round_tx: Cow<'a, Transaction>,
319 #[serde(with = "ark::encode::serde")]
320 vtxos_spec: Cow<'a, VtxoTreeSpec>,
321 unlock_hash: UnlockHash,
322 },
323}
324
325impl<'a> From<&'a AttemptState> for SerdeAttemptState<'a> {
326 fn from(state: &'a AttemptState) -> Self {
327 match state {
328 AttemptState::AwaitingAttempt => SerdeAttemptState::AwaitingAttempt,
329 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
330 SerdeAttemptState::AwaitingUnsignedVtxoTree {
331 cosign_keys: Cow::Borrowed(cosign_keys),
332 secret_nonces: Cow::Borrowed(secret_nonces),
333 unlock_hash: *unlock_hash,
334 }
335 },
336 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
337 SerdeAttemptState::AwaitingFinishedRound {
338 unsigned_round_tx: Cow::Borrowed(unsigned_round_tx),
339 vtxos_spec: Cow::Borrowed(vtxos_spec),
340 unlock_hash: *unlock_hash,
341 }
342 },
343 }
344 }
345}
346
347impl<'a> From<SerdeAttemptState<'a>> for AttemptState {
348 fn from(state: SerdeAttemptState<'a>) -> Self {
349 match state {
350 SerdeAttemptState::AwaitingAttempt => AttemptState::AwaitingAttempt,
351 SerdeAttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
352 AttemptState::AwaitingUnsignedVtxoTree {
353 cosign_keys: cosign_keys.into_owned(),
354 secret_nonces: secret_nonces.into_owned(),
355 unlock_hash: unlock_hash,
356 }
357 },
358 SerdeAttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
359 AttemptState::AwaitingFinishedRound {
360 unsigned_round_tx: unsigned_round_tx.into_owned(),
361 vtxos_spec: vtxos_spec.into_owned(),
362 unlock_hash: unlock_hash,
363 }
364 },
365 }
366 }
367}
368
369#[derive(Debug, Serialize, Deserialize)]
371enum SerdeRoundFlowState<'a> {
372 NonInteractivePending {
374 unlock_hash: UnlockHash,
375 },
376
377 InteractivePending,
379 InteractiveOngoing {
381 round_seq: RoundSeq,
382 attempt_seq: usize,
383 state: SerdeAttemptState<'a>,
384 },
385
386 Finished {
388 funding_tx: Cow<'a, Transaction>,
389 unlock_hash: UnlockHash,
390 },
391
392 Failed {
394 error: Cow<'a, str>,
395 },
396
397 Canceled,
399}
400
401impl<'a> From<&'a RoundFlowState> for SerdeRoundFlowState<'a> {
402 fn from(state: &'a RoundFlowState) -> Self {
403 match state {
404 RoundFlowState::NonInteractivePending { unlock_hash } => {
405 SerdeRoundFlowState::NonInteractivePending {
406 unlock_hash: *unlock_hash,
407 }
408 },
409 RoundFlowState::InteractivePending => SerdeRoundFlowState::InteractivePending,
410 RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
411 SerdeRoundFlowState::InteractiveOngoing {
412 round_seq: *round_seq,
413 attempt_seq: *attempt_seq,
414 state: state.into(),
415 }
416 },
417 RoundFlowState::Finished { funding_tx, unlock_hash } => {
418 SerdeRoundFlowState::Finished {
419 funding_tx: Cow::Borrowed(funding_tx),
420 unlock_hash: *unlock_hash,
421 }
422 },
423 RoundFlowState::Failed { error } => {
424 SerdeRoundFlowState::Failed {
425 error: Cow::Borrowed(error),
426 }
427 },
428 RoundFlowState::Canceled => SerdeRoundFlowState::Canceled,
429 }
430 }
431}
432
433impl<'a> From<SerdeRoundFlowState<'a>> for RoundFlowState {
434 fn from(state: SerdeRoundFlowState<'a>) -> Self {
435 match state {
436 SerdeRoundFlowState::NonInteractivePending { unlock_hash } => {
437 RoundFlowState::NonInteractivePending { unlock_hash }
438 },
439 SerdeRoundFlowState::InteractivePending => RoundFlowState::InteractivePending,
440 SerdeRoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
441 RoundFlowState::InteractiveOngoing {
442 round_seq: round_seq,
443 attempt_seq: attempt_seq,
444 state: state.into(),
445 }
446 },
447 SerdeRoundFlowState::Finished { funding_tx, unlock_hash } => {
448 RoundFlowState::Finished {
449 funding_tx: funding_tx.into_owned(),
450 unlock_hash,
451 }
452 },
453 SerdeRoundFlowState::Failed { error } => {
454 RoundFlowState::Failed {
455 error: error.into_owned(),
456 }
457 },
458 SerdeRoundFlowState::Canceled => RoundFlowState::Canceled,
459 }
460 }
461}
462
463#[derive(Debug, Serialize, Deserialize)]
465pub struct SerdeRoundState<'a> {
466 done: bool,
467 participation: SerdeRoundParticipation<'a>,
468 movement_id: Option<MovementId>,
469 flow: SerdeRoundFlowState<'a>,
470 #[serde(with = "ark::encode::serde::cow::vec")]
471 new_vtxos: Cow<'a, [Vtxo<Full>]>,
472 sent_forfeit_sigs: bool,
473}
474
475impl<'a> From<&'a RoundState> for SerdeRoundState<'a> {
476 fn from(state: &'a RoundState) -> Self {
477 Self {
478 done: state.done,
479 participation: (&state.participation).into(),
480 movement_id: state.movement_id,
481 flow: (&state.flow).into(),
482 new_vtxos: Cow::Borrowed(&state.new_vtxos),
483 sent_forfeit_sigs: state.sent_forfeit_sigs,
484 }
485 }
486}
487
488impl<'a> From<SerdeRoundState<'a>> for RoundState {
489 fn from(state: SerdeRoundState<'a>) -> Self {
490 Self {
491 done: state.done,
492 participation: state.participation.into(),
493 movement_id: state.movement_id,
494 flow: state.flow.into(),
495 new_vtxos: state.new_vtxos.into_owned(),
496 sent_forfeit_sigs: state.sent_forfeit_sigs,
497 }
498 }
499}
500
501#[cfg(test)]
502mod test {
503 use crate::exit::{ExitState, ExitTxOrigin};
504 use crate::vtxo::VtxoState;
505
506 #[test]
507 fn test_serialized_structs() {
510 let serialised = r#"{"type":"start","tip_height":119}"#;
512 serde_json::from_str::<ExitState>(serialised).unwrap();
513 let serialised = r#"{"type":"awaiting-delta","tip_height":122,"confirmed_block":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f","claimable_height":134}"#;
514 serde_json::from_str::<ExitState>(serialised).unwrap();
515 let serialised = r#"{"type":"claimable","tip_height":134,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block":null}"#;
516 serde_json::from_str::<ExitState>(serialised).unwrap();
517 let serialised = r#"{"type":"claimable","tip_height":140,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block": "139:c6e9eb8c8b4d9620bbe87b94d7fb0fbb8eef1c4a8c1e60f7b3a5d80fe26b0d3e"}"#;
518 serde_json::from_str::<ExitState>(serialised).unwrap();
519 let serialised = r#"{"type":"claim-in-progress","tip_height":134, "claimable_since": "134:6585896bdda6f08d924bf45cc2b16418af56703b3c50930e4dccbc1728d3800a","claim_txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c"}"#;
520 serde_json::from_str::<ExitState>(serialised).unwrap();
521 let serialised = r#"{"type":"claimed","tip_height":134,"txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c","block": "122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f"}"#;
522 serde_json::from_str::<ExitState>(serialised).unwrap();
523
524 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"verify-inputs"}}]}"#;
528 serde_json::from_str::<ExitState>(serialised).unwrap();
529 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-input-confirmation","txids":["ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de"]}}]}"#;
530 serde_json::from_str::<ExitState>(serialised).unwrap();
531 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-cpfp-broadcast"}}]}"#;
532 serde_json::from_str::<ExitState>(serialised).unwrap();
533 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-confirmation","child_txid":"ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de","origin":{"type":"wallet","confirmed_in":null}}}]}"#;
534 serde_json::from_str::<ExitState>(serialised).unwrap();
535 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}}}]}"#;
536 serde_json::from_str::<ExitState>(serialised).unwrap();
537 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"}}}]}"#;
538 serde_json::from_str::<ExitState>(serialised).unwrap();
539
540 let serialized = r#"{"type":"wallet","confirmed_in":null}"#;
542 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
543 let serialized = r#"{"type":"wallet","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
544 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
545 let serialized = r#"{"type":"mempool"}"#;
547 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
548 let serialized = r#"{"type":"mempool","fee_rate_kwu":25000,"total_fee":27625}"#;
550 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
551 let serialized = r#"{"type":"block","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
552 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
553
554 let serialised = r#"{"type": "spendable"}"#;
556 serde_json::from_str::<VtxoState>(serialised).unwrap();
557 let serialised = r#"{"type": "spent"}"#;
558 serde_json::from_str::<VtxoState>(serialised).unwrap();
559 let serialised = r#"{"type": "locked", "movement_id": null}"#;
560 serde_json::from_str::<VtxoState>(serialised).unwrap();
561 let serialised = r#"{"type": "locked", "movement_id": 42}"#;
562 serde_json::from_str::<VtxoState>(serialised).unwrap();
563 }
564}