1use crate::server::VirtualTxOutPoint;
2use crate::Error;
3use bitcoin::Amount;
4use bitcoin::SignedAmount;
5use bitcoin::Txid;
6use std::collections::hash_map::Entry;
7use std::collections::HashMap;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum Transaction {
11 Boarding {
13 txid: Txid,
14 amount: Amount,
17 confirmed_at: Option<i64>,
18 },
19 Commitment {
21 txid: Txid,
22 amount: SignedAmount,
25 created_at: i64,
26 },
27 Ark {
29 txid: Txid,
30 amount: SignedAmount,
33 is_settled: bool,
36 created_at: i64,
37 },
38 Offboard {
40 commitment_txid: Txid,
42 amount: Amount,
44 confirmed_at: Option<i64>,
47 },
48}
49
50impl Transaction {
51 pub fn created_at(&self) -> Option<i64> {
65 match self {
66 Transaction::Boarding { confirmed_at, .. }
67 | Transaction::Offboard { confirmed_at, .. } => *confirmed_at,
68 Transaction::Commitment { created_at, .. } | Transaction::Ark { created_at, .. } => {
69 Some(*created_at)
70 }
71 }
72 }
73
74 pub fn txid(&self) -> Txid {
75 match self {
76 Transaction::Boarding { txid, .. }
77 | Transaction::Commitment { txid, .. }
78 | Transaction::Ark { txid, .. } => *txid,
79 Transaction::Offboard {
80 commitment_txid, ..
81 } => *commitment_txid,
82 }
83 }
84}
85
86pub fn sort_transactions_by_created_at(txs: &mut [Transaction]) {
91 txs.sort_by(|a, b| match (a.created_at(), b.created_at()) {
92 (None, None) => std::cmp::Ordering::Equal,
93 (None, Some(_)) => std::cmp::Ordering::Less,
94 (Some(_), None) => std::cmp::Ordering::Greater,
95 (Some(a_time), Some(b_time)) => b_time.cmp(&a_time),
96 });
97}
98
99pub fn generate_incoming_vtxo_transaction_history(
103 spent_vtxos: &[VirtualTxOutPoint],
104 spendable_vtxos: &[VirtualTxOutPoint],
105 boarding_commitment_txs: &[Txid],
107) -> Result<Vec<Transaction>, Error> {
108 let mut txs = Vec::new();
109
110 let all_vtxos = spent_vtxos.iter().chain(spendable_vtxos.iter());
111
112 let mut spent_vtxos_left_to_check = spent_vtxos.to_vec();
113
114 for vtxo in all_vtxos {
116 if !vtxo.is_preconfirmed
118 && boarding_commitment_txs.contains(
119 &vtxo.commitment_txids[0],
121 )
122 {
123 continue;
124 }
125
126 if vtxo.is_preconfirmed {
131 let spent_amount = {
133 let mut spent_amount = Amount::ZERO;
134 let mut remaining_spent_vtxos = Vec::new();
135 for spent_vtxo in spent_vtxos_left_to_check.iter() {
136 if spent_vtxo.ark_txid == Some(vtxo.outpoint.txid) {
137 spent_amount += spent_vtxo.amount;
138 } else {
139 remaining_spent_vtxos.push(spent_vtxo.clone());
140 }
141 }
142
143 spent_vtxos_left_to_check = remaining_spent_vtxos;
144
145 spent_amount
146 };
147
148 let receive_amount = vtxo.amount.to_signed().map_err(Error::ad_hoc)?;
149 let spent_amount = spent_amount.to_signed().map_err(Error::ad_hoc)?;
150
151 let net_amount = receive_amount - spent_amount;
152
153 if net_amount.is_positive() {
157 txs.push(Transaction::Ark {
158 txid: vtxo.outpoint.txid,
159 amount: net_amount,
160 is_settled: vtxo.spent_by.is_some() ||
161 vtxo.settled_by.is_some(),
163 created_at: vtxo.created_at,
164 })
165 }
166 } else {
167 let spent_amount = {
169 let mut spent_amount = Amount::ZERO;
170 let mut remaining_spent_vtxos = Vec::new();
171 for spent_vtxo in spent_vtxos_left_to_check.iter() {
172 let commitment_txid = vtxo.commitment_txids[0];
174
175 if spent_vtxo.settled_by == Some(commitment_txid) {
176 spent_amount += spent_vtxo.amount;
177 } else {
178 remaining_spent_vtxos.push(spent_vtxo.clone());
179 }
180 }
181
182 spent_vtxos_left_to_check = remaining_spent_vtxos;
183
184 spent_amount
185 };
186
187 let receive_amount = vtxo.amount.to_signed().map_err(Error::ad_hoc)?;
188 let spent_amount = spent_amount.to_signed().map_err(Error::ad_hoc)?;
189
190 let net_amount = receive_amount - spent_amount;
191
192 if net_amount.is_positive() {
196 txs.push(Transaction::Commitment {
197 txid: vtxo.outpoint.txid,
198 amount: receive_amount,
199 created_at: vtxo.created_at,
200 })
201 }
202 }
203 }
204
205 Ok(txs)
206}
207
208pub fn generate_outgoing_vtxo_transaction_history(
269 spent_vtxos: &[VirtualTxOutPoint],
270 spendable_vtxos: &[VirtualTxOutPoint],
271) -> Result<impl Iterator<Item = OutgoingTransaction>, Error> {
272 let all_vtxos = [spent_vtxos, spendable_vtxos].concat();
273
274 let mut vtxos_by_spent_by = HashMap::<Txid, Vec<VirtualTxOutPoint>>::new();
276 let mut vtxos_by_settled_by = HashMap::<Txid, Vec<VirtualTxOutPoint>>::new();
278
279 for spent_vtxo in spent_vtxos.iter() {
280 if let Some(settled_by) = spent_vtxo.settled_by {
281 match vtxos_by_settled_by.entry(settled_by) {
283 Entry::Occupied(mut occupied_entry) => {
284 occupied_entry.get_mut().push(spent_vtxo.clone());
285 }
286 Entry::Vacant(e) => {
287 e.insert(vec![spent_vtxo.clone()]);
288 }
289 }
290 } else if let Some(ark_txid) = spent_vtxo.ark_txid {
291 if spent_vtxo.spent_by.is_some() {
292 match vtxos_by_spent_by.entry(ark_txid) {
293 Entry::Occupied(mut occupied_entry) => {
294 occupied_entry.get_mut().push(spent_vtxo.clone());
295 }
296 Entry::Vacant(e) => {
297 e.insert(vec![spent_vtxo.clone()]);
298 }
299 }
300 }
301 }
302 }
303
304 let mut outgoing_txs = Vec::new();
308
309 for (spend_txid, spent_vtxos) in vtxos_by_spent_by.iter() {
311 let spent_amount = spent_vtxos
312 .iter()
313 .fold(Amount::ZERO, |acc, x| acc + x.amount)
314 .to_signed()
315 .map_err(Error::ad_hoc)?;
316
317 let produced_virtual_tx_outpoints = all_vtxos
318 .iter()
319 .filter(|v| v.outpoint.txid == *spend_txid)
320 .collect::<Vec<_>>();
321
322 let produced_amount = produced_virtual_tx_outpoints
323 .iter()
324 .fold(Amount::ZERO, |acc, x| acc + x.amount)
325 .to_signed()
326 .map_err(Error::ad_hoc)?;
327
328 let net_amount = produced_amount - spent_amount;
329
330 if !net_amount.is_negative() {
331 continue;
333 }
334
335 let tx = match produced_virtual_tx_outpoints.first() {
336 Some(virtual_tx_change_outpoint) => {
337 OutgoingTransaction::with_change(virtual_tx_change_outpoint, net_amount)
338 }
339 None => OutgoingTransaction::without_change(*spend_txid, net_amount),
340 };
341
342 outgoing_txs.push(tx);
343 }
344
345 for (commitment_txid, settled_vtxos) in vtxos_by_settled_by.iter() {
354 let input_amount = settled_vtxos
355 .iter()
356 .fold(Amount::ZERO, |acc, x| acc + x.amount)
357 .to_signed()
358 .map_err(Error::ad_hoc)?;
359
360 let produced_vtxos = all_vtxos
363 .iter()
364 .filter(|v| v.commitment_txids.contains(commitment_txid))
365 .collect::<Vec<_>>();
366
367 let output_amount = produced_vtxos
368 .iter()
369 .fold(Amount::ZERO, |acc, x| acc + x.amount)
370 .to_signed()
371 .map_err(Error::ad_hoc)?;
372
373 let offboarded_amount = input_amount - output_amount;
374
375 if offboarded_amount.is_positive() {
376 outgoing_txs.push(OutgoingTransaction::IncompleteOffboard(
378 IncompleteOffboardTransaction {
379 commitment_txid: *commitment_txid,
380 amount: offboarded_amount.to_unsigned().map_err(Error::ad_hoc)?,
381 },
382 ));
383 }
384 }
386
387 Ok(OutgoingTransactionIter::new(outgoing_txs))
388}
389
390#[derive(Clone, Copy, Debug, PartialEq)]
399pub enum OutgoingTransaction {
400 Complete(Transaction),
401 Incomplete(IncompleteOutgoingTransaction),
402 IncompleteOffboard(IncompleteOffboardTransaction),
403}
404
405impl OutgoingTransaction {
406 fn with_change(
411 virtual_tx_change_outpoint: &VirtualTxOutPoint,
412 net_amount: SignedAmount,
413 ) -> Self {
414 Self::Complete(build_outgoing_transaction(
415 virtual_tx_change_outpoint,
416 net_amount,
417 ))
418 }
419
420 fn without_change(txid: Txid, net_amount: SignedAmount) -> Self {
425 Self::Incomplete(IncompleteOutgoingTransaction {
426 first_outpoint: bitcoin::OutPoint { txid, vout: 0 },
427 net_amount,
428 })
429 }
430}
431
432#[derive(Clone, Copy, Debug, PartialEq)]
435pub struct IncompleteOutgoingTransaction {
436 first_outpoint: bitcoin::OutPoint,
441 net_amount: SignedAmount,
442}
443
444#[derive(Clone, Copy, Debug, PartialEq)]
449pub struct IncompleteOffboardTransaction {
450 commitment_txid: Txid,
451 amount: Amount,
452}
453
454impl IncompleteOffboardTransaction {
455 pub fn commitment_txid(&self) -> Txid {
459 self.commitment_txid
460 }
461
462 pub fn finish(self, confirmed_at: Option<i64>) -> Transaction {
469 Transaction::Offboard {
470 commitment_txid: self.commitment_txid,
471 amount: self.amount,
472 confirmed_at,
473 }
474 }
475}
476
477impl IncompleteOutgoingTransaction {
478 pub fn first_outpoint(&self) -> bitcoin::OutPoint {
483 self.first_outpoint
484 }
485
486 pub fn finish(self, virtual_tx_outpoint: &VirtualTxOutPoint) -> Result<Transaction, Error> {
501 if self.first_outpoint.txid != virtual_tx_outpoint.outpoint.txid {
502 return Err(Error::ad_hoc(format!(
503 "cannot finish outgoing transaction with unrelated \
504 virtual TX outpoint: expected {}, got {}",
505 self.first_outpoint.txid, virtual_tx_outpoint.outpoint.txid
506 )));
507 }
508
509 Ok(build_outgoing_transaction(
510 virtual_tx_outpoint,
511 self.net_amount,
512 ))
513 }
514}
515
516struct OutgoingTransactionIter {
518 inner: std::vec::IntoIter<OutgoingTransaction>,
519}
520
521impl OutgoingTransactionIter {
522 fn new(txs: Vec<OutgoingTransaction>) -> Self {
524 Self {
525 inner: txs.into_iter(),
526 }
527 }
528}
529
530impl Iterator for OutgoingTransactionIter {
531 type Item = OutgoingTransaction;
532
533 fn next(&mut self) -> Option<Self::Item> {
534 self.inner.next()
535 }
536}
537
538fn build_outgoing_transaction(
540 vtxo_outpoint: &VirtualTxOutPoint,
542 net_amount: SignedAmount,
544) -> Transaction {
545 let created_at = vtxo_outpoint.created_at;
546 match vtxo_outpoint.is_preconfirmed {
547 true => Transaction::Ark {
548 txid: vtxo_outpoint.outpoint.txid,
549 amount: net_amount,
550 is_settled: true,
553 created_at,
554 },
555 false => Transaction::Commitment {
556 txid: vtxo_outpoint.commitment_txids[0],
557 amount: net_amount,
558 created_at,
559 },
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use bitcoin::OutPoint;
567 use bitcoin::ScriptBuf;
568
569 #[test]
573 fn alice_before_sending() {
574 let boarding_commitment_txs = [
575 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
576 .parse()
577 .unwrap(),
578 ];
579
580 let spendable_vtxos = [VirtualTxOutPoint {
581 outpoint: OutPoint {
582 txid: "2646aea682389e1739a33a617d1f3ee28ccc7e4e16210936cece7a823e37527e"
583 .parse()
584 .unwrap(),
585 vout: 0,
586 },
587 created_at: 1730330127,
588 expires_at: 1730934927,
589 amount: Amount::from_sat(20_000),
590 script: ScriptBuf::new(),
591 is_preconfirmed: false,
592 is_swept: false,
593 is_unrolled: false,
594 is_spent: false,
595 spent_by: None,
596 commitment_txids: vec![
597 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
598 .parse()
599 .unwrap(),
600 ],
601 settled_by: None,
602 ark_txid: None,
603 assets: Vec::new(),
604 }];
605
606 let inc_txs = generate_incoming_vtxo_transaction_history(
607 &[],
608 &spendable_vtxos,
609 &boarding_commitment_txs,
610 )
611 .unwrap();
612
613 let out_txs = generate_outgoing_vtxo_transaction_history(&[], &spendable_vtxos)
614 .unwrap()
615 .collect::<Vec<_>>();
616
617 assert!(inc_txs.is_empty());
618 assert!(out_txs.is_empty());
619 }
620
621 #[test]
622 fn alice_after_sending() {
623 let boarding_commitment_txs = [
624 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
625 .parse()
626 .unwrap(),
627 ];
628
629 let spendable_vtxos = [VirtualTxOutPoint {
630 outpoint: OutPoint {
631 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
632 .parse()
633 .unwrap(),
634 vout: 1,
635 },
636 created_at: 1730330256,
637 expires_at: 1730934927,
638 amount: Amount::from_sat(18_784),
639 script: ScriptBuf::new(),
640 is_preconfirmed: true,
641 is_swept: false,
642 is_unrolled: false,
643 is_spent: false,
644 spent_by: None,
645 commitment_txids: vec![
646 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
647 .parse()
648 .unwrap(),
649 ],
650 settled_by: None,
651 ark_txid: None,
652 assets: Vec::new(),
653 }];
654
655 let spent_vtxos = [VirtualTxOutPoint {
656 outpoint: OutPoint {
657 txid: "2646aea682389e1739a33a617d1f3ee28ccc7e4e16210936cece7a823e37527e"
658 .parse()
659 .unwrap(),
660 vout: 0,
661 },
662 created_at: 1730330127,
663 expires_at: 1730934927,
664 amount: Amount::from_sat(20_000),
665 script: ScriptBuf::new(),
666 is_preconfirmed: false,
667 is_swept: false,
668 is_unrolled: false,
669 is_spent: true,
670 spent_by: Some(
671 "e3c4f18d0418935db8000c5b8c8fc8d776b5741cd625369eceea9aebb8bcee03"
672 .parse()
673 .unwrap(),
674 ),
675 commitment_txids: vec![
676 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
677 .parse()
678 .unwrap(),
679 ],
680 settled_by: None,
681 ark_txid: Some(
682 "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
683 .parse()
684 .unwrap(),
685 ),
686 assets: Vec::new(),
687 }];
688
689 let inc_txs = generate_incoming_vtxo_transaction_history(
690 &spent_vtxos,
691 &spendable_vtxos,
692 &boarding_commitment_txs,
693 )
694 .unwrap();
695
696 let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
697 .unwrap()
698 .filter_map(|tx| {
699 if let OutgoingTransaction::Complete(tx) = tx {
700 Some(tx)
701 } else {
702 None
703 }
704 })
705 .collect::<Vec<_>>();
706
707 assert!(inc_txs.is_empty());
708
709 assert_eq!(
710 out_txs,
711 [Transaction::Ark {
712 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
713 .parse()
714 .unwrap(),
715 amount: SignedAmount::from_sat(-1_216),
716 is_settled: true,
717 created_at: 1730330256,
718 }]
719 );
720 }
721
722 #[test]
723 fn bob_before_settling() {
724 let spendable_vtxos = [
725 VirtualTxOutPoint {
726 outpoint: OutPoint {
727 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
728 .parse()
729 .unwrap(),
730 vout: 0,
731 },
732 created_at: 1730330256,
733 expires_at: 1730934927,
734 amount: Amount::from_sat(1_000),
735 script: ScriptBuf::new(),
736 is_preconfirmed: true,
737 is_swept: false,
738 is_unrolled: false,
739 is_spent: false,
740 spent_by: None,
741 commitment_txids: vec![
742 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
743 .parse()
744 .unwrap(),
745 ],
746 settled_by: None,
747 ark_txid: None,
748 assets: Vec::new(),
749 },
750 VirtualTxOutPoint {
751 outpoint: OutPoint {
752 txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
753 .parse()
754 .unwrap(),
755 vout: 0,
756 },
757 created_at: 1730330748,
758 expires_at: 1730935548,
759 amount: Amount::from_sat(2_000),
760 script: ScriptBuf::new(),
761 is_preconfirmed: true,
762 is_swept: false,
763 is_unrolled: false,
764 is_spent: false,
765 spent_by: None,
766 commitment_txids: vec![
767 "a4e91c211398e0be0edad322fb74a739b1c77bb82b9e4ea94b0115b8e4dfe645"
768 .parse()
769 .unwrap(),
770 ],
771 settled_by: None,
772 ark_txid: None,
773 assets: Vec::new(),
774 },
775 ];
776
777 let spent_vtxos = [];
778
779 let mut inc_txs =
780 generate_incoming_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos, &[])
781 .unwrap();
782
783 sort_transactions_by_created_at(&mut inc_txs);
784
785 let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
786 .unwrap()
787 .collect::<Vec<_>>();
788
789 assert_eq!(
790 inc_txs,
791 [
792 Transaction::Ark {
793 txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
794 .parse()
795 .unwrap(),
796 amount: SignedAmount::from_sat(2_000),
797 is_settled: false,
798 created_at: 1730330748,
799 },
800 Transaction::Ark {
801 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
802 .parse()
803 .unwrap(),
804 amount: SignedAmount::from_sat(1_000),
805 is_settled: false,
806 created_at: 1730330256,
807 }
808 ]
809 );
810
811 assert!(out_txs.is_empty());
812 }
813
814 #[test]
815 fn bob_after_settling() {
816 let spendable_vtxos = [VirtualTxOutPoint {
817 outpoint: OutPoint {
818 txid: "d9c95372c0c419fd007005edd54e21dabac0375a37fc5f17c313bc1e5f483af9"
819 .parse()
820 .unwrap(),
821 vout: 0,
822 },
823 created_at: 1730331035,
824 expires_at: 1730935835,
825 amount: Amount::from_sat(3_000),
826 script: ScriptBuf::new(),
827 is_preconfirmed: false,
828 is_swept: false,
829 is_unrolled: false,
830 is_spent: false,
831 spent_by: None,
832 commitment_txids: vec![
833 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
834 .parse()
835 .unwrap(),
836 ],
837 settled_by: None,
838 ark_txid: None,
839 assets: Vec::new(),
840 }];
841
842 let spent_vtxos = [
843 VirtualTxOutPoint {
844 outpoint: OutPoint {
845 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
846 .parse()
847 .unwrap(),
848 vout: 0,
849 },
850 created_at: 1730330256,
851 expires_at: 1730934927,
852 amount: Amount::from_sat(1_000),
853 script: ScriptBuf::new(),
854 is_preconfirmed: true,
855 is_swept: false,
856 is_unrolled: false,
857 is_spent: true,
858 spent_by: Some(
859 "c9bdde5595c5479394e805a8c468657cd94ae75a504172e514030b3c549f3646"
860 .parse()
861 .unwrap(),
862 ),
863 commitment_txids: vec![
864 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
865 .parse()
866 .unwrap(),
867 ],
868 settled_by: Some(
869 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
870 .parse()
871 .unwrap(),
872 ),
873 ark_txid: None,
874 assets: Vec::new(),
875 },
876 VirtualTxOutPoint {
877 outpoint: OutPoint {
878 txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
879 .parse()
880 .unwrap(),
881 vout: 0,
882 },
883 created_at: 1730330748,
884 expires_at: 1730935548,
885 amount: Amount::from_sat(2_000),
886 script: ScriptBuf::new(),
887 is_preconfirmed: true,
888 is_swept: false,
889 is_unrolled: false,
890 is_spent: true,
891 spent_by: Some(
892 "a7c06a495dd145fd95693a5190b26ffa391aa4440c1af26f9ff293166d97d807"
893 .parse()
894 .unwrap(),
895 ),
896 commitment_txids: vec![
897 "a4e91c211398e0be0edad322fb74a739b1c77bb82b9e4ea94b0115b8e4dfe645"
898 .parse()
899 .unwrap(),
900 ],
901 settled_by: Some(
902 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
903 .parse()
904 .unwrap(),
905 ),
906 ark_txid: None,
907 assets: Vec::new(),
908 },
909 ];
910
911 let mut inc_txs =
912 generate_incoming_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos, &[])
913 .unwrap();
914
915 sort_transactions_by_created_at(&mut inc_txs);
916
917 let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
918 .unwrap()
919 .collect::<Vec<_>>();
920
921 assert_eq!(
922 inc_txs,
923 [
924 Transaction::Ark {
925 txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
926 .parse()
927 .unwrap(),
928 amount: SignedAmount::from_sat(2_000),
929 is_settled: true,
930 created_at: 1730330748,
931 },
932 Transaction::Ark {
933 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
934 .parse()
935 .unwrap(),
936 amount: SignedAmount::from_sat(1_000),
937 is_settled: true,
938 created_at: 1730330256,
939 }
940 ]
941 );
942
943 assert!(out_txs.is_empty());
944 }
945
946 #[test]
947 fn bob_after_sending() {
948 let spendable_vtxos = [VirtualTxOutPoint {
949 outpoint: OutPoint {
950 txid: "c59004f8c468a922216f513ec7d63d9b6a13571af0bacd51910709351d27fe55"
951 .parse()
952 .unwrap(),
953 vout: 1,
954 },
955 created_at: 1730331198,
956 expires_at: 1730935835,
957 amount: Amount::from_sat(684),
958 script: ScriptBuf::new(),
959 is_preconfirmed: true,
960 is_swept: false,
961 is_unrolled: false,
962 is_spent: false,
963 spent_by: None,
964 commitment_txids: vec![
965 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
966 .parse()
967 .unwrap(),
968 ],
969 settled_by: None,
970 ark_txid: None,
971 assets: Vec::new(),
972 }];
973
974 let spent_vtxos = [
975 VirtualTxOutPoint {
976 outpoint: OutPoint {
977 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
978 .parse()
979 .unwrap(),
980 vout: 0,
981 },
982 created_at: 1730330256,
983 expires_at: 1730934927,
984 amount: Amount::from_sat(1_000),
985 script: ScriptBuf::new(),
986 is_preconfirmed: true,
987 is_swept: false,
988 is_unrolled: false,
989 is_spent: true,
990 spent_by: Some(
991 "c9bdde5595c5479394e805a8c468657cd94ae75a504172e514030b3c549f3646"
992 .parse()
993 .unwrap(),
994 ),
995 commitment_txids: vec![
996 "c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
997 .parse()
998 .unwrap(),
999 ],
1000 settled_by: Some(
1001 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
1002 .parse()
1003 .unwrap(),
1004 ),
1005 ark_txid: None,
1006 assets: Vec::new(),
1007 },
1008 VirtualTxOutPoint {
1009 outpoint: OutPoint {
1010 txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
1011 .parse()
1012 .unwrap(),
1013 vout: 0,
1014 },
1015 created_at: 1730330748,
1016 expires_at: 1730935548,
1017 amount: Amount::from_sat(2_000),
1018 script: ScriptBuf::new(),
1019 is_preconfirmed: true,
1020 is_swept: false,
1021 is_unrolled: false,
1022 is_spent: true,
1023 spent_by: Some(
1024 "a7c06a495dd145fd95693a5190b26ffa391aa4440c1af26f9ff293166d97d807"
1025 .parse()
1026 .unwrap(),
1027 ),
1028 commitment_txids: vec![
1029 "a4e91c211398e0be0edad322fb74a739b1c77bb82b9e4ea94b0115b8e4dfe645"
1030 .parse()
1031 .unwrap(),
1032 ],
1033 settled_by: Some(
1034 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
1035 .parse()
1036 .unwrap(),
1037 ),
1038 ark_txid: None,
1039 assets: Vec::new(),
1040 },
1041 VirtualTxOutPoint {
1042 outpoint: OutPoint {
1043 txid: "d9c95372c0c419fd007005edd54e21dabac0375a37fc5f17c313bc1e5f483af9"
1044 .parse()
1045 .unwrap(),
1046 vout: 0,
1047 },
1048 created_at: 1730331035,
1049 expires_at: 1730935835,
1050 amount: Amount::from_sat(3_000),
1051 script: ScriptBuf::new(),
1052 is_preconfirmed: false,
1053 is_swept: false,
1054 is_unrolled: false,
1055 is_spent: true,
1056 spent_by: Some(
1057 "cfcfec99c9767162fc2432fac7cac6240eae2ce344d2d0e1600284399f5dd493"
1058 .parse()
1059 .unwrap(),
1060 ),
1061 commitment_txids: vec![
1062 "7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
1063 .parse()
1064 .unwrap(),
1065 ],
1066 settled_by: None,
1067 ark_txid: Some(
1068 "c59004f8c468a922216f513ec7d63d9b6a13571af0bacd51910709351d27fe55"
1069 .parse()
1070 .unwrap(),
1071 ),
1072 assets: Vec::new(),
1073 },
1074 ];
1075
1076 let inc_txs =
1077 generate_incoming_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos, &[])
1078 .unwrap();
1079
1080 let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
1081 .unwrap()
1082 .filter_map(|tx| {
1083 if let OutgoingTransaction::Complete(tx) = tx {
1084 Some(tx)
1085 } else {
1086 None
1087 }
1088 })
1089 .collect::<Vec<_>>();
1090
1091 let mut txs = [inc_txs, out_txs].concat();
1092 sort_transactions_by_created_at(&mut txs);
1093
1094 assert_eq!(
1095 txs,
1096 [
1097 Transaction::Ark {
1098 txid: "c59004f8c468a922216f513ec7d63d9b6a13571af0bacd51910709351d27fe55"
1099 .parse()
1100 .unwrap(),
1101 amount: SignedAmount::from_sat(-2_316),
1102 is_settled: true,
1103 created_at: 1730331198,
1104 },
1105 Transaction::Ark {
1106 txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
1107 .parse()
1108 .unwrap(),
1109 amount: SignedAmount::from_sat(2_000),
1110 is_settled: true,
1111 created_at: 1730330748,
1112 },
1113 Transaction::Ark {
1114 txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
1115 .parse()
1116 .unwrap(),
1117 amount: SignedAmount::from_sat(1_000),
1118 is_settled: true,
1119 created_at: 1730330256,
1120 }
1121 ]
1122 );
1123 }
1124}