Skip to main content

ark_core/
coin_select.rs

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
17/// Select VTXOs to be used as inputs in Ark transactions.
18pub 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        // Sort vtxos by expiration (older first)
30        virtual_tx_outpoints.sort_by_key(|a| a.expire_at);
31    }
32
33    // Process VTXOs
34    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    // Try to avoid generating dust.
50    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
60/// Select VTXOs that hold a specific asset, accumulating until `amount` is reached.
61///
62/// Returns the selected VTXOs and the asset change amount.
63pub fn select_vtxos_for_asset(
64    virtual_tx_outpoints: &[VirtualTxOutPoint],
65    amount: u64,
66    asset_id: AssetId,
67) -> Result<(Vec<VirtualTxOutPoint>, u64), Error> {
68    // Filter to only VTXOs containing this asset.
69    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    // Sort by expiration (older first).
76    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// Tests for the coin selection function
109#[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}