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