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::round::{AttemptState, RoundFlowState, RoundParticipation, RoundState, RoundStateGuard};
31use crate::vtxo::VtxoState;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SerdeVtxo {
36 #[serde(with = "ark::encode::serde")]
37 pub vtxo: Vtxo<Full>,
38 pub states: Vec<VtxoState>,
40}
41
42#[derive(Debug, thiserror::Error)]
43#[error("vtxo has no state")]
44pub struct MissingStateError;
45
46impl SerdeVtxo {
47 pub fn current_state(&self) -> Option<&VtxoState> {
48 self.states.last()
49 }
50
51 pub fn to_wallet_vtxo(&self) -> Result<WalletVtxo, MissingStateError> {
52 Ok(WalletVtxo {
53 vtxo: self.vtxo.clone(),
54 state: self.current_state().cloned().ok_or(MissingStateError)?,
55 })
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SerdeVtxoKey {
62 pub index: u32,
63 pub public_key: PublicKey,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct RoundStateId(pub u32);
69
70impl RoundStateId {
71 pub fn to_bytes(&self) -> [u8; 4] {
72 self.0.to_be_bytes()
73 }
74}
75
76impl fmt::Display for RoundStateId {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 fmt::Display::fmt(&self.0, f)
79 }
80}
81
82#[allow(unused)]
83pub struct Locked(RoundStateGuard);
84
85pub struct Unlocked;
86
87pub struct StoredRoundState<G = Locked> {
88 id: RoundStateId,
89 state: RoundState,
90 _guard: G
91}
92
93impl<G> StoredRoundState<G> {
94 pub fn id(&self) -> RoundStateId {
95 self.id
96 }
97
98 pub fn state(&self) -> &RoundState {
99 &self.state
100 }
101}
102
103impl StoredRoundState<Unlocked> {
104 pub fn new(id: RoundStateId, state: RoundState) -> Self {
105 Self { id, state, _guard: Unlocked }
106 }
107
108 pub fn lock(self, guard: RoundStateGuard) -> StoredRoundState {
109 StoredRoundState { id: self.id, state: self.state, _guard: Locked(guard) }
110 }
111}
112
113impl StoredRoundState {
114 pub fn state_mut(&mut self) -> &mut RoundState {
115 &mut self.state
116 }
117
118 pub fn unlock(self) -> StoredRoundState<Unlocked> {
119 StoredRoundState { id: self.id, state: self.state, _guard: Unlocked }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct PendingBoard {
126 #[serde(with = "bitcoin_ext::serde::encodable")]
129 pub funding_tx: Transaction,
130 pub vtxos: Vec<VtxoId>,
134 #[serde(with = "bitcoin::amount::serde::as_sat")]
136 pub amount: Amount,
137 pub movement_id: MovementId,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct PendingOffboard {
147 pub movement_id: MovementId,
149 pub offboard_txid: bitcoin::Txid,
151 pub offboard_tx: Transaction,
153 pub vtxo_ids: Vec<VtxoId>,
155 pub destination: String,
157 pub created_at: chrono::DateTime<chrono::Local>,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
165pub struct LightningSend {
166 pub invoice: Invoice,
168 #[serde(with = "bitcoin::amount::serde::as_sat")]
170 pub amount: Amount,
171 pub fee: Amount,
173 pub htlc_vtxos: Vec<WalletVtxo>,
175 pub movement_id: MovementId,
177 pub preimage: Option<Preimage>,
184 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct LightningReceive {
196 pub payment_hash: PaymentHash,
197 pub payment_preimage: Preimage,
198 pub invoice: Bolt11Invoice,
199 pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
200 pub htlc_vtxos: Vec<WalletVtxo>,
201 pub htlc_recv_cltv_delta: BlockDelta,
202 pub movement_id: Option<MovementId>,
203 pub finished_at: Option<chrono::DateTime<chrono::Local>>,
204}
205
206#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
211pub struct StoredExit {
212 pub vtxo_id: VtxoId,
214 pub state: ExitState,
216 pub history: Vec<ExitState>,
218}
219
220impl StoredExit {
221 pub fn new(exit: &ExitVtxo) -> Self {
223 Self {
224 vtxo_id: exit.id(),
225 state: exit.state().clone(),
226 history: exit.history().clone(),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct SerdeExitChildTx {
234 #[serde(with = "bitcoin_ext::serde::encodable")]
235 pub child_tx: Transaction,
236 pub origin: ExitTxOrigin,
237}
238
239#[derive(Debug, Clone, Deserialize, Serialize)]
240struct SerdeVtxoRequest<'a> {
241 #[serde(with = "bitcoin::amount::serde::as_sat")]
242 amount: Amount,
243 #[serde(with = "ark::encode::serde")]
244 policy: Cow<'a, VtxoPolicy>,
245}
246
247impl<'a> From<&'a VtxoRequest> for SerdeVtxoRequest<'a> {
248 fn from(v: &'a VtxoRequest) -> Self {
249 Self {
250 amount: v.amount,
251 policy: Cow::Borrowed(&v.policy),
252 }
253 }
254}
255
256impl<'a> From<SerdeVtxoRequest<'a>> for VtxoRequest {
257 fn from(v: SerdeVtxoRequest<'a>) -> Self {
258 VtxoRequest {
259 amount: v.amount,
260 policy: v.policy.into_owned(),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267struct SerdeRoundParticipation<'a> {
268 #[serde(with = "ark::encode::serde::cow::vec")]
269 inputs: Cow<'a, [Vtxo<Full>]>,
270 outputs: Vec<SerdeVtxoRequest<'a>>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 unblinded_mailbox_id: Option<MailboxIdentifier>,
273}
274
275impl<'a> From<&'a RoundParticipation> for SerdeRoundParticipation<'a> {
276 fn from(v: &'a RoundParticipation) -> Self {
277 Self {
278 inputs: Cow::Borrowed(&v.inputs),
279 outputs: v.outputs.iter().map(|v| v.into()).collect(),
280 unblinded_mailbox_id: v.unblinded_mailbox_id,
281 }
282 }
283}
284
285impl<'a> From<SerdeRoundParticipation<'a>> for RoundParticipation {
286 fn from(v: SerdeRoundParticipation<'a>) -> Self {
287 Self {
288 inputs: v.inputs.into_owned(),
289 outputs: v.outputs.into_iter().map(|v| v.into()).collect(),
290 unblinded_mailbox_id: v.unblinded_mailbox_id,
291 }
292 }
293}
294
295#[derive(Debug, Serialize, Deserialize)]
297enum SerdeAttemptState<'a> {
298 AwaitingAttempt,
299 AwaitingUnsignedVtxoTree {
300 cosign_keys: Cow<'a, [Keypair]>,
301 secret_nonces: Cow<'a, [Vec<DangerousSecretNonce>]>,
302 unlock_hash: UnlockHash,
303 },
304 AwaitingFinishedRound {
305 #[serde(with = "bitcoin_ext::serde::encodable::cow")]
306 unsigned_round_tx: Cow<'a, Transaction>,
307 #[serde(with = "ark::encode::serde")]
308 vtxos_spec: Cow<'a, VtxoTreeSpec>,
309 unlock_hash: UnlockHash,
310 },
311}
312
313impl<'a> From<&'a AttemptState> for SerdeAttemptState<'a> {
314 fn from(state: &'a AttemptState) -> Self {
315 match state {
316 AttemptState::AwaitingAttempt => SerdeAttemptState::AwaitingAttempt,
317 AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
318 SerdeAttemptState::AwaitingUnsignedVtxoTree {
319 cosign_keys: Cow::Borrowed(cosign_keys),
320 secret_nonces: Cow::Borrowed(secret_nonces),
321 unlock_hash: *unlock_hash,
322 }
323 },
324 AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
325 SerdeAttemptState::AwaitingFinishedRound {
326 unsigned_round_tx: Cow::Borrowed(unsigned_round_tx),
327 vtxos_spec: Cow::Borrowed(vtxos_spec),
328 unlock_hash: *unlock_hash,
329 }
330 },
331 }
332 }
333}
334
335impl<'a> From<SerdeAttemptState<'a>> for AttemptState {
336 fn from(state: SerdeAttemptState<'a>) -> Self {
337 match state {
338 SerdeAttemptState::AwaitingAttempt => AttemptState::AwaitingAttempt,
339 SerdeAttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
340 AttemptState::AwaitingUnsignedVtxoTree {
341 cosign_keys: cosign_keys.into_owned(),
342 secret_nonces: secret_nonces.into_owned(),
343 unlock_hash: unlock_hash,
344 }
345 },
346 SerdeAttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
347 AttemptState::AwaitingFinishedRound {
348 unsigned_round_tx: unsigned_round_tx.into_owned(),
349 vtxos_spec: vtxos_spec.into_owned(),
350 unlock_hash: unlock_hash,
351 }
352 },
353 }
354 }
355}
356
357#[derive(Debug, Serialize, Deserialize)]
359enum SerdeRoundFlowState<'a> {
360 NonInteractivePending {
362 unlock_hash: UnlockHash,
363 },
364
365 InteractivePending,
367 InteractiveOngoing {
369 round_seq: RoundSeq,
370 attempt_seq: usize,
371 state: SerdeAttemptState<'a>,
372 },
373
374 Finished {
376 funding_tx: Cow<'a, Transaction>,
377 unlock_hash: UnlockHash,
378 },
379
380 Failed {
382 error: Cow<'a, str>,
383 },
384
385 Canceled,
387}
388
389impl<'a> From<&'a RoundFlowState> for SerdeRoundFlowState<'a> {
390 fn from(state: &'a RoundFlowState) -> Self {
391 match state {
392 RoundFlowState::NonInteractivePending { unlock_hash } => {
393 SerdeRoundFlowState::NonInteractivePending {
394 unlock_hash: *unlock_hash,
395 }
396 },
397 RoundFlowState::InteractivePending => SerdeRoundFlowState::InteractivePending,
398 RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
399 SerdeRoundFlowState::InteractiveOngoing {
400 round_seq: *round_seq,
401 attempt_seq: *attempt_seq,
402 state: state.into(),
403 }
404 },
405 RoundFlowState::Finished { funding_tx, unlock_hash } => {
406 SerdeRoundFlowState::Finished {
407 funding_tx: Cow::Borrowed(funding_tx),
408 unlock_hash: *unlock_hash,
409 }
410 },
411 RoundFlowState::Failed { error } => {
412 SerdeRoundFlowState::Failed {
413 error: Cow::Borrowed(error),
414 }
415 },
416 RoundFlowState::Canceled => SerdeRoundFlowState::Canceled,
417 }
418 }
419}
420
421impl<'a> From<SerdeRoundFlowState<'a>> for RoundFlowState {
422 fn from(state: SerdeRoundFlowState<'a>) -> Self {
423 match state {
424 SerdeRoundFlowState::NonInteractivePending { unlock_hash } => {
425 RoundFlowState::NonInteractivePending { unlock_hash }
426 },
427 SerdeRoundFlowState::InteractivePending => RoundFlowState::InteractivePending,
428 SerdeRoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
429 RoundFlowState::InteractiveOngoing {
430 round_seq: round_seq,
431 attempt_seq: attempt_seq,
432 state: state.into(),
433 }
434 },
435 SerdeRoundFlowState::Finished { funding_tx, unlock_hash } => {
436 RoundFlowState::Finished {
437 funding_tx: funding_tx.into_owned(),
438 unlock_hash,
439 }
440 },
441 SerdeRoundFlowState::Failed { error } => {
442 RoundFlowState::Failed {
443 error: error.into_owned(),
444 }
445 },
446 SerdeRoundFlowState::Canceled => RoundFlowState::Canceled,
447 }
448 }
449}
450
451#[derive(Debug, Serialize, Deserialize)]
453pub struct SerdeRoundState<'a> {
454 done: bool,
455 participation: SerdeRoundParticipation<'a>,
456 movement_id: Option<MovementId>,
457 flow: SerdeRoundFlowState<'a>,
458 #[serde(with = "ark::encode::serde::cow::vec")]
459 new_vtxos: Cow<'a, [Vtxo<Full>]>,
460 sent_forfeit_sigs: bool,
461}
462
463impl<'a> From<&'a RoundState> for SerdeRoundState<'a> {
464 fn from(state: &'a RoundState) -> Self {
465 Self {
466 done: state.done,
467 participation: (&state.participation).into(),
468 movement_id: state.movement_id,
469 flow: (&state.flow).into(),
470 new_vtxos: Cow::Borrowed(&state.new_vtxos),
471 sent_forfeit_sigs: state.sent_forfeit_sigs,
472 }
473 }
474}
475
476impl<'a> From<SerdeRoundState<'a>> for RoundState {
477 fn from(state: SerdeRoundState<'a>) -> Self {
478 Self {
479 done: state.done,
480 participation: state.participation.into(),
481 movement_id: state.movement_id,
482 flow: state.flow.into(),
483 new_vtxos: state.new_vtxos.into_owned(),
484 sent_forfeit_sigs: state.sent_forfeit_sigs,
485 }
486 }
487}
488
489#[cfg(test)]
490mod test {
491 use crate::exit::{ExitState, ExitTxOrigin};
492 use crate::vtxo::VtxoState;
493
494 #[test]
495 fn test_serialised_structs() {
498 let serialised = r#"{"type":"start","tip_height":119}"#;
500 serde_json::from_str::<ExitState>(serialised).unwrap();
501 let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-input-confirmation","txids":["ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de"]}}]}"#;
502 serde_json::from_str::<ExitState>(serialised).unwrap();
503 let serialised = r#"{"type":"awaiting-delta","tip_height":122,"confirmed_block":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f","claimable_height":134}"#;
504 serde_json::from_str::<ExitState>(serialised).unwrap();
505 let serialised = r#"{"type":"claimable","tip_height":134,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block":null}"#;
506 serde_json::from_str::<ExitState>(serialised).unwrap();
507 let serialised = r#"{"type":"claim-in-progress","tip_height":134, "claimable_since": "134:6585896bdda6f08d924bf45cc2b16418af56703b3c50930e4dccbc1728d3800a","claim_txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c"}"#;
508 serde_json::from_str::<ExitState>(serialised).unwrap();
509 let serialised = r#"{"type":"claimed","tip_height":134,"txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c","block": "122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f"}"#;
510 serde_json::from_str::<ExitState>(serialised).unwrap();
511
512 let serialized = r#"{"type":"wallet","confirmed_in":null}"#;
514 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
515 let serialized = r#"{"type":"wallet","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
516 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
517 let serialized = r#"{"type":"mempool","fee_rate_kwu":25000,"total_fee":27625}"#;
518 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
519 let serialized = r#"{"type":"block","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
520 serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
521
522 let serialised = r#"{"type": "spendable"}"#;
524 serde_json::from_str::<VtxoState>(serialised).unwrap();
525 let serialised = r#"{"type": "spent"}"#;
526 serde_json::from_str::<VtxoState>(serialised).unwrap();
527 let serialised = r#"{"type": "locked", "movement_id": null}"#;
528 serde_json::from_str::<VtxoState>(serialised).unwrap();
529 }
530}