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