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