1use crate::script::csv_sig_script;
2use crate::script::multisig_script;
3use crate::script::tr_script_pubkey;
4use crate::Error;
5use crate::ExitDelayKind;
6use crate::ExplorerUtxo;
7use crate::UNSPENDABLE_KEY;
8use bitcoin::key::PublicKey;
9use bitcoin::key::Secp256k1;
10use bitcoin::key::Verification;
11use bitcoin::taproot;
12use bitcoin::taproot::LeafVersion;
13use bitcoin::taproot::TaprootBuilder;
14use bitcoin::taproot::TaprootSpendInfo;
15use bitcoin::Address;
16use bitcoin::Amount;
17use bitcoin::Network;
18use bitcoin::OutPoint;
19use bitcoin::ScriptBuf;
20use bitcoin::XOnlyPublicKey;
21use std::time::Duration;
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24pub struct BoardingOutput {
25 server: XOnlyPublicKey,
26 owner: XOnlyPublicKey,
27 spend_info: TaprootSpendInfo,
28 address: Address,
29 exit_delay: bitcoin::Sequence,
30 exit_delay_kind: ExitDelayKind,
31}
32
33impl BoardingOutput {
34 pub fn new<C>(
35 secp: &Secp256k1<C>,
36 server: XOnlyPublicKey,
37 owner: XOnlyPublicKey,
38 exit_delay: bitcoin::Sequence,
39 network: Network,
40 ) -> Result<Self, Error>
41 where
42 C: Verification,
43 {
44 let unspendable_key: PublicKey = UNSPENDABLE_KEY
45 .parse()
46 .map_err(|e| Error::ad_hoc(format!("invalid unspendable key: {e}")))?;
47 let (unspendable_key, _) = unspendable_key.inner.x_only_public_key();
48
49 let multisig_script = multisig_script(server, owner);
50 let exit_script = csv_sig_script(exit_delay, owner);
51
52 let spend_info = TaprootBuilder::new()
53 .add_leaf(1, multisig_script)
54 .map_err(|e| Error::ad_hoc(format!("invalid multisig leaf: {e}")))?
55 .add_leaf(1, exit_script)
56 .map_err(|e| Error::ad_hoc(format!("invalid exit leaf: {e}")))?
57 .finalize(secp, unspendable_key)
58 .map_err(|_| Error::ad_hoc("failed to finalize taproot builder"))?;
59
60 let exit_delay_kind = ExitDelayKind::from_sequence(exit_delay)?;
61
62 let script_pubkey = tr_script_pubkey(&spend_info);
63 let address = Address::from_script(&script_pubkey, network)
64 .map_err(|e| Error::ad_hoc(format!("invalid script: {e}")))?;
65
66 Ok(Self {
67 server,
68 owner,
69 spend_info,
70 address,
71 exit_delay,
72 exit_delay_kind,
73 })
74 }
75
76 pub fn address(&self) -> &Address {
77 &self.address
78 }
79
80 pub fn owner_pk(&self) -> XOnlyPublicKey {
81 self.owner
82 }
83
84 pub fn script_pubkey(&self) -> ScriptBuf {
85 self.address.script_pubkey()
86 }
87
88 pub fn forfeit_spend_info(&self) -> (ScriptBuf, taproot::ControlBlock) {
89 let forfeit_script = self.forfeit_script();
94
95 let control_block = self
96 .spend_info
97 .control_block(&(forfeit_script.clone(), LeafVersion::TapScript))
98 .expect("forfeit script");
99
100 (forfeit_script, control_block)
101 }
102
103 pub fn exit_spend_info(&self) -> (ScriptBuf, taproot::ControlBlock) {
104 let exit_script = self.exit_script();
105
106 let control_block = self
107 .spend_info
108 .control_block(&(exit_script.clone(), LeafVersion::TapScript))
109 .expect("exit script");
110
111 (exit_script, control_block)
112 }
113
114 pub fn exit_delay(&self) -> bitcoin::Sequence {
115 self.exit_delay
116 }
117
118 pub fn output_key(&self) -> bitcoin::key::TweakedPublicKey {
119 self.spend_info.output_key()
120 }
121
122 pub fn to_ark_address(&self, network: Network, server: XOnlyPublicKey) -> crate::ArkAddress {
123 crate::ArkAddress::new(network, server, self.output_key())
124 }
125
126 pub fn tapscripts(&self) -> Vec<ScriptBuf> {
127 let (exit_script, _) = self.exit_spend_info();
128 let (forfeit_script, _) = self.forfeit_spend_info();
129
130 vec![exit_script, forfeit_script]
131 }
132
133 pub fn can_be_claimed_unilaterally_by_owner(
136 &self,
137 now: Duration,
138 confirmation_blocktime: Duration,
139 confirmations: u64,
140 ) -> bool {
141 match self.exit_delay_kind {
142 ExitDelayKind::Time(seconds) => {
143 let exit_path_time = confirmation_blocktime + seconds;
144
145 now > exit_path_time
146 }
147 ExitDelayKind::Blocks(confirmations_required) => {
148 confirmations >= confirmations_required
149 }
150 }
151 }
152
153 fn forfeit_script(&self) -> ScriptBuf {
154 multisig_script(self.server, self.owner)
155 }
156
157 fn exit_script(&self) -> ScriptBuf {
158 csv_sig_script(self.exit_delay, self.owner)
159 }
160}
161
162#[derive(Debug, Clone, Default)]
164pub struct BoardingOutpoints {
165 pub spendable: Vec<(OutPoint, Amount, BoardingOutput)>,
167 pub expired: Vec<(OutPoint, Amount, BoardingOutput)>,
169 pub pending: Vec<(OutPoint, Amount, BoardingOutput)>,
171 pub spent: Vec<(OutPoint, Amount)>,
173}
174
175impl BoardingOutpoints {
176 pub fn spendable_balance(&self) -> Amount {
177 self.spendable.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
178 }
179
180 pub fn expired_balance(&self) -> Amount {
181 self.expired.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
182 }
183
184 pub fn pending_balance(&self) -> Amount {
185 self.pending.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
186 }
187}
188
189pub fn list_boarding_outpoints<F>(
191 find_outpoints_fn: F,
192 boarding_outputs: &[BoardingOutput],
193) -> Result<BoardingOutpoints, Error>
194where
195 F: Fn(&Address) -> Result<Vec<ExplorerUtxo>, Error>,
196{
197 let mut spendable = Vec::new();
198 let mut expired = Vec::new();
199 let mut pending = Vec::new();
200 let mut spent = Vec::new();
201 for boarding_output in boarding_outputs.iter() {
202 let boarding_address = boarding_output.address();
203
204 let boarding_utxos = find_outpoints_fn(boarding_address)?;
206
207 for boarding_utxo in boarding_utxos.iter() {
208 match *boarding_utxo {
209 ExplorerUtxo {
211 confirmation_blocktime: Some(confirmation_blocktime),
212 confirmations,
213 outpoint,
214 amount,
215 is_spent: false,
216 } => {
217 let now = std::time::UNIX_EPOCH.elapsed().map_err(Error::ad_hoc)?;
218
219 if boarding_output.can_be_claimed_unilaterally_by_owner(
221 now,
222 Duration::from_secs(confirmation_blocktime),
223 confirmations,
224 ) {
225 expired.push((outpoint, amount, boarding_output.clone()));
226 }
227 else {
230 spendable.push((outpoint, amount, boarding_output.clone()));
231 }
232 }
233 ExplorerUtxo {
235 confirmation_blocktime: None,
236 outpoint,
237 amount,
238 is_spent: false,
239 ..
240 } => {
241 pending.push((outpoint, amount, boarding_output.clone()));
242 }
243 ExplorerUtxo {
245 outpoint,
246 amount,
247 is_spent: true,
248 ..
249 } => spent.push((outpoint, amount)),
250 }
251 }
252 }
253
254 Ok(BoardingOutpoints {
255 spendable,
256 expired,
257 pending,
258 spent,
259 })
260}