use crate::server::VirtualTxOutPoint;
use crate::Error;
use bitcoin::Amount;
use bitcoin::SignedAmount;
use bitcoin::Txid;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Transaction {
Boarding {
txid: Txid,
amount: Amount,
confirmed_at: Option<i64>,
},
Commitment {
txid: Txid,
amount: SignedAmount,
created_at: i64,
},
Ark {
txid: Txid,
amount: SignedAmount,
is_settled: bool,
created_at: i64,
},
Offboard {
commitment_txid: Txid,
amount: Amount,
confirmed_at: Option<i64>,
},
}
impl Transaction {
pub fn created_at(&self) -> Option<i64> {
match self {
Transaction::Boarding { confirmed_at, .. }
| Transaction::Offboard { confirmed_at, .. } => *confirmed_at,
Transaction::Commitment { created_at, .. } | Transaction::Ark { created_at, .. } => {
Some(*created_at)
}
}
}
pub fn txid(&self) -> Txid {
match self {
Transaction::Boarding { txid, .. }
| Transaction::Commitment { txid, .. }
| Transaction::Ark { txid, .. } => *txid,
Transaction::Offboard {
commitment_txid, ..
} => *commitment_txid,
}
}
}
pub fn sort_transactions_by_created_at(txs: &mut [Transaction]) {
txs.sort_by(|a, b| match (a.created_at(), b.created_at()) {
(None, None) => std::cmp::Ordering::Equal,
(None, Some(_)) => std::cmp::Ordering::Less,
(Some(_), None) => std::cmp::Ordering::Greater,
(Some(a_time), Some(b_time)) => b_time.cmp(&a_time),
});
}
pub fn generate_incoming_vtxo_transaction_history(
spent_vtxos: &[VirtualTxOutPoint],
spendable_vtxos: &[VirtualTxOutPoint],
boarding_commitment_txs: &[Txid],
) -> Result<Vec<Transaction>, Error> {
let mut txs = Vec::new();
let all_vtxos = spent_vtxos.iter().chain(spendable_vtxos.iter());
let mut spent_vtxos_left_to_check = spent_vtxos.to_vec();
for vtxo in all_vtxos {
if !vtxo.is_preconfirmed
&& boarding_commitment_txs.contains(
&vtxo.commitment_txids[0],
)
{
continue;
}
if vtxo.is_preconfirmed {
let spent_amount = {
let mut spent_amount = Amount::ZERO;
let mut remaining_spent_vtxos = Vec::new();
for spent_vtxo in spent_vtxos_left_to_check.iter() {
if spent_vtxo.ark_txid == Some(vtxo.outpoint.txid) {
spent_amount += spent_vtxo.amount;
} else {
remaining_spent_vtxos.push(spent_vtxo.clone());
}
}
spent_vtxos_left_to_check = remaining_spent_vtxos;
spent_amount
};
let receive_amount = vtxo.amount.to_signed().map_err(Error::ad_hoc)?;
let spent_amount = spent_amount.to_signed().map_err(Error::ad_hoc)?;
let net_amount = receive_amount - spent_amount;
if net_amount.is_positive() {
txs.push(Transaction::Ark {
txid: vtxo.outpoint.txid,
amount: net_amount,
is_settled: vtxo.spent_by.is_some() ||
vtxo.settled_by.is_some(),
created_at: vtxo.created_at,
})
}
} else {
let spent_amount = {
let mut spent_amount = Amount::ZERO;
let mut remaining_spent_vtxos = Vec::new();
for spent_vtxo in spent_vtxos_left_to_check.iter() {
let commitment_txid = vtxo.commitment_txids[0];
if spent_vtxo.settled_by == Some(commitment_txid) {
spent_amount += spent_vtxo.amount;
} else {
remaining_spent_vtxos.push(spent_vtxo.clone());
}
}
spent_vtxos_left_to_check = remaining_spent_vtxos;
spent_amount
};
let receive_amount = vtxo.amount.to_signed().map_err(Error::ad_hoc)?;
let spent_amount = spent_amount.to_signed().map_err(Error::ad_hoc)?;
let net_amount = receive_amount - spent_amount;
if net_amount.is_positive() {
txs.push(Transaction::Commitment {
txid: vtxo.outpoint.txid,
amount: receive_amount,
created_at: vtxo.created_at,
})
}
}
}
Ok(txs)
}
pub fn generate_outgoing_vtxo_transaction_history(
spent_vtxos: &[VirtualTxOutPoint],
spendable_vtxos: &[VirtualTxOutPoint],
) -> Result<impl Iterator<Item = OutgoingTransaction>, Error> {
let all_vtxos = [spent_vtxos, spendable_vtxos].concat();
let mut vtxos_by_spent_by = HashMap::<Txid, Vec<VirtualTxOutPoint>>::new();
let mut vtxos_by_settled_by = HashMap::<Txid, Vec<VirtualTxOutPoint>>::new();
for spent_vtxo in spent_vtxos.iter() {
if let Some(settled_by) = spent_vtxo.settled_by {
match vtxos_by_settled_by.entry(settled_by) {
Entry::Occupied(mut occupied_entry) => {
occupied_entry.get_mut().push(spent_vtxo.clone());
}
Entry::Vacant(e) => {
e.insert(vec![spent_vtxo.clone()]);
}
}
} else if let Some(ark_txid) = spent_vtxo.ark_txid {
if spent_vtxo.spent_by.is_some() {
match vtxos_by_spent_by.entry(ark_txid) {
Entry::Occupied(mut occupied_entry) => {
occupied_entry.get_mut().push(spent_vtxo.clone());
}
Entry::Vacant(e) => {
e.insert(vec![spent_vtxo.clone()]);
}
}
}
}
}
let mut outgoing_txs = Vec::new();
for (spend_txid, spent_vtxos) in vtxos_by_spent_by.iter() {
let spent_amount = spent_vtxos
.iter()
.fold(Amount::ZERO, |acc, x| acc + x.amount)
.to_signed()
.map_err(Error::ad_hoc)?;
let produced_virtual_tx_outpoints = all_vtxos
.iter()
.filter(|v| v.outpoint.txid == *spend_txid)
.collect::<Vec<_>>();
let produced_amount = produced_virtual_tx_outpoints
.iter()
.fold(Amount::ZERO, |acc, x| acc + x.amount)
.to_signed()
.map_err(Error::ad_hoc)?;
let net_amount = produced_amount - spent_amount;
if !net_amount.is_negative() {
continue;
}
let tx = match produced_virtual_tx_outpoints.first() {
Some(virtual_tx_change_outpoint) => {
OutgoingTransaction::with_change(virtual_tx_change_outpoint, net_amount)
}
None => OutgoingTransaction::without_change(*spend_txid, net_amount),
};
outgoing_txs.push(tx);
}
for (commitment_txid, settled_vtxos) in vtxos_by_settled_by.iter() {
let input_amount = settled_vtxos
.iter()
.fold(Amount::ZERO, |acc, x| acc + x.amount)
.to_signed()
.map_err(Error::ad_hoc)?;
let produced_vtxos = all_vtxos
.iter()
.filter(|v| v.commitment_txids.contains(commitment_txid))
.collect::<Vec<_>>();
let output_amount = produced_vtxos
.iter()
.fold(Amount::ZERO, |acc, x| acc + x.amount)
.to_signed()
.map_err(Error::ad_hoc)?;
let offboarded_amount = input_amount - output_amount;
if offboarded_amount.is_positive() {
outgoing_txs.push(OutgoingTransaction::IncompleteOffboard(
IncompleteOffboardTransaction {
commitment_txid: *commitment_txid,
amount: offboarded_amount.to_unsigned().map_err(Error::ad_hoc)?,
},
));
}
}
Ok(OutgoingTransactionIter::new(outgoing_txs))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OutgoingTransaction {
Complete(Transaction),
Incomplete(IncompleteOutgoingTransaction),
IncompleteOffboard(IncompleteOffboardTransaction),
}
impl OutgoingTransaction {
fn with_change(
virtual_tx_change_outpoint: &VirtualTxOutPoint,
net_amount: SignedAmount,
) -> Self {
Self::Complete(build_outgoing_transaction(
virtual_tx_change_outpoint,
net_amount,
))
}
fn without_change(txid: Txid, net_amount: SignedAmount) -> Self {
Self::Incomplete(IncompleteOutgoingTransaction {
first_outpoint: bitcoin::OutPoint { txid, vout: 0 },
net_amount,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IncompleteOutgoingTransaction {
first_outpoint: bitcoin::OutPoint,
net_amount: SignedAmount,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IncompleteOffboardTransaction {
commitment_txid: Txid,
amount: Amount,
}
impl IncompleteOffboardTransaction {
pub fn commitment_txid(&self) -> Txid {
self.commitment_txid
}
pub fn finish(self, confirmed_at: Option<i64>) -> Transaction {
Transaction::Offboard {
commitment_txid: self.commitment_txid,
amount: self.amount,
confirmed_at,
}
}
}
impl IncompleteOutgoingTransaction {
pub fn first_outpoint(&self) -> bitcoin::OutPoint {
self.first_outpoint
}
pub fn finish(self, virtual_tx_outpoint: &VirtualTxOutPoint) -> Result<Transaction, Error> {
if self.first_outpoint.txid != virtual_tx_outpoint.outpoint.txid {
return Err(Error::ad_hoc(format!(
"cannot finish outgoing transaction with unrelated \
virtual TX outpoint: expected {}, got {}",
self.first_outpoint.txid, virtual_tx_outpoint.outpoint.txid
)));
}
Ok(build_outgoing_transaction(
virtual_tx_outpoint,
self.net_amount,
))
}
}
struct OutgoingTransactionIter {
inner: std::vec::IntoIter<OutgoingTransaction>,
}
impl OutgoingTransactionIter {
fn new(txs: Vec<OutgoingTransaction>) -> Self {
Self {
inner: txs.into_iter(),
}
}
}
impl Iterator for OutgoingTransactionIter {
type Item = OutgoingTransaction;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
fn build_outgoing_transaction(
vtxo_outpoint: &VirtualTxOutPoint,
net_amount: SignedAmount,
) -> Transaction {
let created_at = vtxo_outpoint.created_at;
match vtxo_outpoint.is_preconfirmed {
true => Transaction::Ark {
txid: vtxo_outpoint.outpoint.txid,
amount: net_amount,
is_settled: true,
created_at,
},
false => Transaction::Commitment {
txid: vtxo_outpoint.commitment_txids[0],
amount: net_amount,
created_at,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::OutPoint;
use bitcoin::ScriptBuf;
#[test]
fn alice_before_sending() {
let boarding_commitment_txs = [
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
];
let spendable_vtxos = [VirtualTxOutPoint {
outpoint: OutPoint {
txid: "2646aea682389e1739a33a617d1f3ee28ccc7e4e16210936cece7a823e37527e"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330127,
expires_at: 1730934927,
amount: Amount::from_sat(20_000),
script: ScriptBuf::new(),
is_preconfirmed: false,
is_swept: false,
is_unrolled: false,
is_spent: false,
spent_by: None,
commitment_txids: vec![
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: None,
assets: Vec::new(),
}];
let inc_txs = generate_incoming_vtxo_transaction_history(
&[],
&spendable_vtxos,
&boarding_commitment_txs,
)
.unwrap();
let out_txs = generate_outgoing_vtxo_transaction_history(&[], &spendable_vtxos)
.unwrap()
.collect::<Vec<_>>();
assert!(inc_txs.is_empty());
assert!(out_txs.is_empty());
}
#[test]
fn alice_after_sending() {
let boarding_commitment_txs = [
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
];
let spendable_vtxos = [VirtualTxOutPoint {
outpoint: OutPoint {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
vout: 1,
},
created_at: 1730330256,
expires_at: 1730934927,
amount: Amount::from_sat(18_784),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: false,
spent_by: None,
commitment_txids: vec![
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: None,
assets: Vec::new(),
}];
let spent_vtxos = [VirtualTxOutPoint {
outpoint: OutPoint {
txid: "2646aea682389e1739a33a617d1f3ee28ccc7e4e16210936cece7a823e37527e"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330127,
expires_at: 1730934927,
amount: Amount::from_sat(20_000),
script: ScriptBuf::new(),
is_preconfirmed: false,
is_swept: false,
is_unrolled: false,
is_spent: true,
spent_by: Some(
"e3c4f18d0418935db8000c5b8c8fc8d776b5741cd625369eceea9aebb8bcee03"
.parse()
.unwrap(),
),
commitment_txids: vec![
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: Some(
"33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
),
assets: Vec::new(),
}];
let inc_txs = generate_incoming_vtxo_transaction_history(
&spent_vtxos,
&spendable_vtxos,
&boarding_commitment_txs,
)
.unwrap();
let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
.unwrap()
.filter_map(|tx| {
if let OutgoingTransaction::Complete(tx) = tx {
Some(tx)
} else {
None
}
})
.collect::<Vec<_>>();
assert!(inc_txs.is_empty());
assert_eq!(
out_txs,
[Transaction::Ark {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(-1_216),
is_settled: true,
created_at: 1730330256,
}]
);
}
#[test]
fn bob_before_settling() {
let spendable_vtxos = [
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330256,
expires_at: 1730934927,
amount: Amount::from_sat(1_000),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: false,
spent_by: None,
commitment_txids: vec![
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: None,
assets: Vec::new(),
},
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330748,
expires_at: 1730935548,
amount: Amount::from_sat(2_000),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: false,
spent_by: None,
commitment_txids: vec![
"a4e91c211398e0be0edad322fb74a739b1c77bb82b9e4ea94b0115b8e4dfe645"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: None,
assets: Vec::new(),
},
];
let spent_vtxos = [];
let mut inc_txs =
generate_incoming_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos, &[])
.unwrap();
sort_transactions_by_created_at(&mut inc_txs);
let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
.unwrap()
.collect::<Vec<_>>();
assert_eq!(
inc_txs,
[
Transaction::Ark {
txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(2_000),
is_settled: false,
created_at: 1730330748,
},
Transaction::Ark {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(1_000),
is_settled: false,
created_at: 1730330256,
}
]
);
assert!(out_txs.is_empty());
}
#[test]
fn bob_after_settling() {
let spendable_vtxos = [VirtualTxOutPoint {
outpoint: OutPoint {
txid: "d9c95372c0c419fd007005edd54e21dabac0375a37fc5f17c313bc1e5f483af9"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730331035,
expires_at: 1730935835,
amount: Amount::from_sat(3_000),
script: ScriptBuf::new(),
is_preconfirmed: false,
is_swept: false,
is_unrolled: false,
is_spent: false,
spent_by: None,
commitment_txids: vec![
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: None,
assets: Vec::new(),
}];
let spent_vtxos = [
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330256,
expires_at: 1730934927,
amount: Amount::from_sat(1_000),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: true,
spent_by: Some(
"c9bdde5595c5479394e805a8c468657cd94ae75a504172e514030b3c549f3646"
.parse()
.unwrap(),
),
commitment_txids: vec![
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
],
settled_by: Some(
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
),
ark_txid: None,
assets: Vec::new(),
},
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330748,
expires_at: 1730935548,
amount: Amount::from_sat(2_000),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: true,
spent_by: Some(
"a7c06a495dd145fd95693a5190b26ffa391aa4440c1af26f9ff293166d97d807"
.parse()
.unwrap(),
),
commitment_txids: vec![
"a4e91c211398e0be0edad322fb74a739b1c77bb82b9e4ea94b0115b8e4dfe645"
.parse()
.unwrap(),
],
settled_by: Some(
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
),
ark_txid: None,
assets: Vec::new(),
},
];
let mut inc_txs =
generate_incoming_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos, &[])
.unwrap();
sort_transactions_by_created_at(&mut inc_txs);
let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
.unwrap()
.collect::<Vec<_>>();
assert_eq!(
inc_txs,
[
Transaction::Ark {
txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(2_000),
is_settled: true,
created_at: 1730330748,
},
Transaction::Ark {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(1_000),
is_settled: true,
created_at: 1730330256,
}
]
);
assert!(out_txs.is_empty());
}
#[test]
fn bob_after_sending() {
let spendable_vtxos = [VirtualTxOutPoint {
outpoint: OutPoint {
txid: "c59004f8c468a922216f513ec7d63d9b6a13571af0bacd51910709351d27fe55"
.parse()
.unwrap(),
vout: 1,
},
created_at: 1730331198,
expires_at: 1730935835,
amount: Amount::from_sat(684),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: false,
spent_by: None,
commitment_txids: vec![
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: None,
assets: Vec::new(),
}];
let spent_vtxos = [
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330256,
expires_at: 1730934927,
amount: Amount::from_sat(1_000),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: true,
spent_by: Some(
"c9bdde5595c5479394e805a8c468657cd94ae75a504172e514030b3c549f3646"
.parse()
.unwrap(),
),
commitment_txids: vec![
"c16ae0d917ac400790da18456015975521bec6e1d1962ad728c0070808c564e8"
.parse()
.unwrap(),
],
settled_by: Some(
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
),
ark_txid: None,
assets: Vec::new(),
},
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730330748,
expires_at: 1730935548,
amount: Amount::from_sat(2_000),
script: ScriptBuf::new(),
is_preconfirmed: true,
is_swept: false,
is_unrolled: false,
is_spent: true,
spent_by: Some(
"a7c06a495dd145fd95693a5190b26ffa391aa4440c1af26f9ff293166d97d807"
.parse()
.unwrap(),
),
commitment_txids: vec![
"a4e91c211398e0be0edad322fb74a739b1c77bb82b9e4ea94b0115b8e4dfe645"
.parse()
.unwrap(),
],
settled_by: Some(
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
),
ark_txid: None,
assets: Vec::new(),
},
VirtualTxOutPoint {
outpoint: OutPoint {
txid: "d9c95372c0c419fd007005edd54e21dabac0375a37fc5f17c313bc1e5f483af9"
.parse()
.unwrap(),
vout: 0,
},
created_at: 1730331035,
expires_at: 1730935835,
amount: Amount::from_sat(3_000),
script: ScriptBuf::new(),
is_preconfirmed: false,
is_swept: false,
is_unrolled: false,
is_spent: true,
spent_by: Some(
"cfcfec99c9767162fc2432fac7cac6240eae2ce344d2d0e1600284399f5dd493"
.parse()
.unwrap(),
),
commitment_txids: vec![
"7fd65ce87e0f9a7af583593d5b0124aabd65c97e05159525d0a98201d6ae95a4"
.parse()
.unwrap(),
],
settled_by: None,
ark_txid: Some(
"c59004f8c468a922216f513ec7d63d9b6a13571af0bacd51910709351d27fe55"
.parse()
.unwrap(),
),
assets: Vec::new(),
},
];
let inc_txs =
generate_incoming_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos, &[])
.unwrap();
let out_txs = generate_outgoing_vtxo_transaction_history(&spent_vtxos, &spendable_vtxos)
.unwrap()
.filter_map(|tx| {
if let OutgoingTransaction::Complete(tx) = tx {
Some(tx)
} else {
None
}
})
.collect::<Vec<_>>();
let mut txs = [inc_txs, out_txs].concat();
sort_transactions_by_created_at(&mut txs);
assert_eq!(
txs,
[
Transaction::Ark {
txid: "c59004f8c468a922216f513ec7d63d9b6a13571af0bacd51910709351d27fe55"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(-2_316),
is_settled: true,
created_at: 1730331198,
},
Transaction::Ark {
txid: "884d85c0db6b52139c39337d54c1f20cd8c5c0d2e83109d69246a345ccc9d169"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(2_000),
is_settled: true,
created_at: 1730330748,
},
Transaction::Ark {
txid: "33fd8ca9ea9cfb53802c42be10ae428573e19fb89484dfe536d06d43efa82034"
.parse()
.unwrap(),
amount: SignedAmount::from_sat(1_000),
is_settled: true,
created_at: 1730330256,
}
]
);
}
}