1use crate::asset::AssetId;
2use crate::server::Asset;
3use crate::Error;
4use bitcoin::Amount;
5use bitcoin::OutPoint;
6use bitcoin::ScriptBuf;
7
8#[derive(Clone, Debug)]
9pub struct VirtualTxOutPoint {
10 pub outpoint: OutPoint,
11 pub script_pubkey: ScriptBuf,
12 pub expire_at: i64,
13 pub amount: Amount,
14 pub assets: Vec<Asset>,
15}
16
17pub fn select_vtxos(
19 mut virtual_tx_outpoints: Vec<VirtualTxOutPoint>,
20 amount: Amount,
21 dust: Amount,
22 sort_by_expiration_time: bool,
23) -> Result<Vec<VirtualTxOutPoint>, Error> {
24 let mut selected = Vec::new();
25 let mut not_selected = Vec::new();
26 let mut selected_amount = Amount::ZERO;
27
28 if sort_by_expiration_time {
29 virtual_tx_outpoints.sort_by_key(|a| a.expire_at);
31 }
32
33 for virtual_tx_outpoint in virtual_tx_outpoints {
35 if selected_amount >= amount {
36 not_selected.push(virtual_tx_outpoint);
37 } else {
38 selected.push(virtual_tx_outpoint.clone());
39 selected_amount += virtual_tx_outpoint.amount;
40 }
41 }
42
43 if selected_amount < amount {
44 return Err(Error::coin_select(format!(
45 "insufficient funds: selected = {selected_amount}, needed = {amount}"
46 )));
47 }
48
49 let change_amount = selected_amount - amount;
51 if let Some(vtxo) = not_selected.first() {
52 if change_amount < dust {
53 selected.push(vtxo.clone());
54 }
55 }
56
57 Ok(selected)
58}
59
60pub fn select_vtxos_for_asset(
64 virtual_tx_outpoints: &[VirtualTxOutPoint],
65 amount: u64,
66 asset_id: AssetId,
67) -> Result<(Vec<VirtualTxOutPoint>, u64), Error> {
68 let mut candidates: Vec<VirtualTxOutPoint> = virtual_tx_outpoints
70 .iter()
71 .filter(|v| v.assets.iter().any(|a| a.asset_id == asset_id))
72 .cloned()
73 .collect();
74
75 candidates.sort_by_key(|a| a.expire_at);
77
78 let mut selected = Vec::new();
79 let mut selected_amount: u64 = 0;
80
81 for vtxo in candidates {
82 if selected_amount >= amount {
83 break;
84 }
85
86 if let Some(asset) = vtxo
87 .assets
88 .iter()
89 .find(|a| a.asset_id == asset_id && a.amount != 0)
90 {
91 selected_amount += asset.amount;
92 selected.push(vtxo);
93 }
94 }
95
96 let change = match selected_amount.checked_sub(amount) {
97 Some(change) => change,
98 None => {
99 return Err(Error::coin_select(format!(
100 "insufficient asset funds for {asset_id}: selected = {selected_amount}, needed = {amount}"
101 )));
102 }
103 };
104
105 Ok((selected, change))
106}
107
108#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn vtxo(expire_at: i64, amount: Amount) -> VirtualTxOutPoint {
114 VirtualTxOutPoint {
115 outpoint: OutPoint::default(),
116 script_pubkey: ScriptBuf::new(),
117 expire_at,
118 amount,
119 assets: Vec::new(),
120 }
121 }
122
123 #[test]
124 fn test_basic_coin_selection() {
125 let vtxos = vec![vtxo(123456789, Amount::from_sat(3000))];
126
127 let result = select_vtxos(vtxos, Amount::from_sat(2500), Amount::from_sat(100), true);
128 assert!(result.is_ok());
129
130 let selected = result.unwrap();
131 assert_eq!(selected.len(), 1);
132 }
133
134 #[test]
135 fn test_insufficient_funds() {
136 let vtxos = vec![vtxo(123456789, Amount::from_sat(100))];
137
138 let result = select_vtxos(vtxos, Amount::from_sat(1000), Amount::from_sat(50), true);
139 assert!(result.is_err());
140 }
141}