Skip to main content

ark_core/
vtxo_list.rs

1use crate::server::Info;
2use crate::server::VirtualTxOutPoint;
3use crate::ExplorerUtxo;
4use crate::Vtxo;
5use bitcoin::Amount;
6use bitcoin::ScriptBuf;
7use bitcoin::XOnlyPublicKey;
8use std::collections::HashMap;
9use std::time::Duration;
10
11#[derive(Clone, Debug)]
12pub struct VtxoList {
13    // Unspent
14    pre_confirmed: Vec<VirtualTxOutPoint>,
15    confirmed: Vec<VirtualTxOutPoint>,
16    recoverable: Vec<VirtualTxOutPoint>,
17
18    // Spent
19    spent: Vec<VirtualTxOutPoint>,
20}
21
22impl VtxoList {
23    pub fn new(
24        // The dust amount according to the Arkade server. Dust outputs are considered recoverable.
25        dust: Amount,
26        virtual_tx_outpoints: Vec<VirtualTxOutPoint>,
27    ) -> Self {
28        let mut recoverable = Vec::new();
29        let mut spent = Vec::new();
30        let mut pre_confirmed = Vec::new();
31        let mut confirmed = Vec::new();
32        for virtual_tx_outpoint in virtual_tx_outpoints {
33            if virtual_tx_outpoint.is_recoverable(dust) {
34                recoverable.push(virtual_tx_outpoint);
35            } else if virtual_tx_outpoint.is_unrolled
36                || virtual_tx_outpoint.is_spent
37                || virtual_tx_outpoint.is_swept
38            {
39                spent.push(virtual_tx_outpoint);
40            } else if virtual_tx_outpoint.is_preconfirmed {
41                pre_confirmed.push(virtual_tx_outpoint);
42            } else {
43                confirmed.push(virtual_tx_outpoint);
44            }
45        }
46
47        VtxoList {
48            pre_confirmed,
49            confirmed,
50            recoverable,
51            spent,
52        }
53    }
54
55    pub fn all(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
56        self.all_unspent().chain(self.spent())
57    }
58
59    pub fn all_unspent(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
60        self.pre_confirmed
61            .iter()
62            .chain(self.confirmed.iter())
63            .chain(self.recoverable.iter())
64    }
65
66    /// VTXOs that are in a state that allows for unilateral exit.
67    ///
68    /// This does _not_ mean that the VTXOs are readily spendable on-chain, just that their ancestor
69    /// chain can still be published.
70    pub fn could_exit_unilaterally(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
71        self.pre_confirmed.iter().chain(self.confirmed.iter())
72    }
73
74    /// VTXOs that can be spent in an offchain transaction.
75    pub fn spendable_offchain(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
76        self.pre_confirmed.iter().chain(self.confirmed.iter())
77    }
78
79    /// VTXOs that can be spent in an offchain transaction at `now_unix_secs`.
80    ///
81    /// This excludes otherwise-spendable VTXOs minted under a deprecated signer whose
82    /// cooperative-sign window has closed. Those VTXOs cannot be forfeited by the server anymore;
83    /// they become usable again only after they expire and move into the recovery path.
84    pub fn spendable_offchain_at<'a, F>(
85        &'a self,
86        server_info: &'a Info,
87        now_unix_secs: i64,
88        server_pk_for_script: F,
89    ) -> impl Iterator<Item = &'a VirtualTxOutPoint> + 'a
90    where
91        F: Fn(&ScriptBuf) -> Option<XOnlyPublicKey> + 'a,
92    {
93        self.spendable_offchain().filter(move |vtxo| {
94            !server_pk_for_script(&vtxo.script)
95                .map(|server_pk| server_info.signer_requires_recovery_at(server_pk, now_unix_secs))
96                .unwrap_or(false)
97        })
98    }
99
100    /// Otherwise-spendable VTXOs blocked only by a deprecated signer's closed cooperative-sign
101    /// window. These remain wallet funds, but they are pending recovery until expiry.
102    pub fn pending_recovery_due_to_signer_at<'a, F>(
103        &'a self,
104        server_info: &'a Info,
105        now_unix_secs: i64,
106        server_pk_for_script: F,
107    ) -> impl Iterator<Item = &'a VirtualTxOutPoint> + 'a
108    where
109        F: Fn(&ScriptBuf) -> Option<XOnlyPublicKey> + 'a,
110    {
111        self.spendable_offchain().filter(move |vtxo| {
112            server_pk_for_script(&vtxo.script)
113                .map(|server_pk| server_info.signer_requires_recovery_at(server_pk, now_unix_secs))
114                .unwrap_or(false)
115        })
116    }
117
118    /// Unspent VTXOs that may be included in a cooperative batch settlement at `now_unix_secs`.
119    ///
120    /// Recoverable VTXOs are always safe: they no longer need a server forfeit signature. Healthy
121    /// VTXOs still need that signature, so VTXOs under an expired deprecated signer are excluded.
122    pub fn batch_settleable_at<'a, F>(
123        &'a self,
124        server_info: &'a Info,
125        now_unix_secs: i64,
126        server_pk_for_script: F,
127    ) -> impl Iterator<Item = &'a VirtualTxOutPoint> + 'a
128    where
129        F: Fn(&ScriptBuf) -> Option<XOnlyPublicKey> + 'a,
130    {
131        let dust = server_info.dust;
132        self.all_unspent().filter(move |vtxo| {
133            vtxo.is_recoverable(dust)
134                || !server_pk_for_script(&vtxo.script)
135                    .map(|server_pk| {
136                        server_info.signer_requires_recovery_at(server_pk, now_unix_secs)
137                    })
138                    .unwrap_or(false)
139        })
140    }
141
142    pub fn pre_confirmed(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
143        self.pre_confirmed.iter()
144    }
145
146    pub fn confirmed(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
147        self.confirmed.iter()
148    }
149
150    /// Returns the list of recoverable VTXOs
151    ///
152    /// A VTXO is recoverable if it:
153    ///
154    /// - has expired;
155    /// - was swept already; or
156    /// - is sub-dust.
157    pub fn recoverable(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
158        self.recoverable.iter()
159    }
160
161    /// VTXOs that are already on-chain and can be spent unilaterally (the exit path is active).
162    pub fn exit_ready(
163        &self,
164        now: Duration,
165        // Corresponds to every VTXO in `vtxos` which has been found on the blockchain.
166        explorer_utxos: Vec<ExplorerUtxo>,
167        // TODO: We probably shouldn't involve the opinionated `Vtxo` type here.
168        vtxos: HashMap<ScriptBuf, Vtxo>,
169    ) -> impl Iterator<Item = &VirtualTxOutPoint> {
170        self.all_unspent().filter(move |v| {
171            match explorer_utxos
172                .iter()
173                .find(|explorer_utxo| explorer_utxo.outpoint == v.outpoint)
174            {
175                // VTXOs that have been confirmed on the blockchain.
176                Some(ExplorerUtxo {
177                    confirmation_blocktime: Some(confirmation_blocktime),
178                    confirmations,
179                    ..
180                }) => {
181                    // VTXOs with an _active_ exit path. These should be claimed unilaterally.
182                    if let Some(vtxo) = vtxos.get(&v.script) {
183                        vtxo.can_be_claimed_unilaterally_by_owner(
184                            now,
185                            Duration::from_secs(*confirmation_blocktime),
186                            *confirmations,
187                        )
188                    } else {
189                        false
190                    }
191                }
192                _ => false,
193            }
194        })
195    }
196
197    pub fn spent(&self) -> impl Iterator<Item = &VirtualTxOutPoint> {
198        self.spent.iter()
199    }
200}