ark_client/
unilateral_exit.rs1use crate::coin_select::coin_select_for_onchain;
2use crate::error::Error;
3use crate::error::ErrorContext;
4use crate::swap_storage::SwapStorage;
5use crate::utils::sleep;
6use crate::utils::timeout_op;
7use crate::wallet::BoardingWallet;
8use crate::wallet::OnchainWallet;
9use crate::Blockchain;
10use crate::Client;
11use ark_core::build_unilateral_exit_tree_txids;
12use ark_core::script::extract_checksig_pubkeys;
13use ark_core::unilateral_exit;
14use ark_core::unilateral_exit::create_unilateral_exit_transaction;
15use ark_core::unilateral_exit::sign_unilateral_exit_tree;
16use ark_core::unilateral_exit::UnilateralExitTree;
17use backon::ExponentialBuilder;
18use backon::Retryable;
19use bitcoin::key::Secp256k1;
20use bitcoin::psbt;
21use bitcoin::Address;
22use bitcoin::Amount;
23use bitcoin::Transaction;
24use bitcoin::TxOut;
25use bitcoin::Txid;
26use std::collections::HashSet;
27
28impl<B, W, S, K> Client<B, W, S, K>
31where
32 B: Blockchain,
33 W: BoardingWallet + OnchainWallet,
34 S: SwapStorage + 'static,
35 K: crate::KeyProvider,
36{
37 pub async fn build_unilateral_exit_trees(&self) -> Result<Vec<Vec<Transaction>>, Error> {
45 let (vtxo_list, _) = self
46 .list_vtxos()
47 .await
48 .context("failed to get spendable VTXOs")?;
49
50 let mut unilateral_exit_trees = Vec::new();
51
52 for virtual_tx_outpoint in vtxo_list.could_exit_unilaterally() {
54 let vtxo_chain_response = timeout_op(
55 self.inner.timeout,
56 self.network_client()
57 .get_vtxo_chain(Some(virtual_tx_outpoint.outpoint), None),
58 )
59 .await
60 .context(format!(
61 "failed to get VTXO chain for outpoint {}",
62 virtual_tx_outpoint.outpoint
63 ))??;
64
65 let paths = build_unilateral_exit_tree_txids(
66 &vtxo_chain_response.chains,
67 virtual_tx_outpoint.outpoint.txid,
68 )?;
69
70 let txs = HashSet::<Txid>::from_iter(paths.concat().into_iter());
72
73 let virtual_txs_response = timeout_op(
74 self.inner.timeout,
75 self.network_client()
76 .get_virtual_txs(txs.iter().map(|tx| tx.to_string()).collect(), None),
77 )
78 .await
79 .context("failed to get virtual TXs")??;
80
81 let paths = paths
82 .into_iter()
83 .map(|path| {
84 path.into_iter()
85 .map(|txid| {
86 virtual_txs_response
87 .txs
88 .iter()
89 .find(|t| t.unsigned_tx.compute_txid() == txid)
90 .cloned()
91 .ok_or_else(|| {
92 Error::ad_hoc(format!("no PSBT found for virtual TX {txid}"))
93 })
94 })
95 .collect::<Result<Vec<_>, _>>()
96 })
97 .collect::<Result<Vec<_>, _>>()?;
98
99 let unilateral_exit_tree =
100 UnilateralExitTree::new(virtual_tx_outpoint.commitment_txids.clone(), paths);
101
102 unilateral_exit_trees.push(unilateral_exit_tree);
103 }
104
105 let mut branches: Vec<Vec<Transaction>> = Vec::new();
106 for unilateral_exit_tree in unilateral_exit_trees {
107 let commitment_txids = unilateral_exit_tree.commitment_txids();
108
109 let mut commitment_txs = Vec::new();
110 for commitment_txid in commitment_txids.iter() {
111 let commitment_tx = timeout_op(
112 self.inner.timeout,
113 self.blockchain().find_tx(commitment_txid),
114 )
115 .await??
116 .ok_or_else(|| {
117 Error::ad_hoc(format!("could not find commitment TX {commitment_txid}"))
118 })?;
119
120 commitment_txs.push(commitment_tx);
121 }
122
123 let signed_unilateral_exit_tree =
124 sign_unilateral_exit_tree(&unilateral_exit_tree, commitment_txs.as_slice())?;
125 branches.extend(signed_unilateral_exit_tree);
126 }
127
128 Ok(branches)
129 }
130
131 pub async fn broadcast_next_unilateral_exit_node(
138 &self,
139 branch: &[Transaction],
140 ) -> Result<Option<Txid>, Error> {
141 let blockchain = &self.blockchain();
142
143 for parent_tx in branch {
144 let parent_txid = parent_tx.compute_txid();
145
146 let broadcast = || async {
147 let is_not_published = blockchain.find_tx(&parent_txid).await?.is_none();
148
149 if is_not_published {
150 let child_tx = self.bump_tx(parent_tx).await?;
151 let bump_txid = child_tx.compute_txid();
152
153 tracing::info!(
154 txid = %parent_txid,
155 %bump_txid,
156 "Broadcasting unilateral exit TX"
157 );
158
159 blockchain
160 .broadcast_package(&[parent_tx, &child_tx])
161 .await?;
162
163 Ok(Some(parent_txid))
164 } else {
165 tracing::debug!(
166 %parent_txid,
167 "Unilateral exit TX already found on the blockchain"
168 );
169
170 Ok(None)
171 }
172 };
173
174 let res = broadcast
175 .retry(ExponentialBuilder::default().with_max_times(5))
176 .sleep(sleep)
177 .notify(|err: &Error, dur: std::time::Duration| {
178 tracing::warn!(
179 "Retrying broadcasting VTXO transaction {parent_txid} after {dur:?}. Error: {err}",
180 );
181 })
182 .await
183 .with_context(|| format!("Failed to broadcast VTXO transaction {parent_txid}"))?;
184
185 if let Some(bump_txid) = res {
186 tracing::info!(
187 txid = %parent_txid,
188 %bump_txid,
189 "Broadcast VTXO transaction"
190 );
191
192 return Ok(Some(parent_txid));
193 }
194 }
195
196 Ok(None)
198 }
199
200 pub async fn send_on_chain(
209 &self,
210 to_address: Address,
211 to_amount: Amount,
212 ) -> Result<Txid, Error> {
213 let (tx, _) = self
214 .create_send_on_chain_transaction_inner(to_address, to_amount)
215 .await?;
216
217 let txid = tx.compute_txid();
218 tracing::info!(
219 %txid,
220 "Broadcasting transaction sending Ark outputs onchain"
221 );
222
223 timeout_op(self.inner.timeout, self.blockchain().broadcast(&tx))
224 .await
225 .with_context(|| format!("failed to broadcast transaction {txid}"))??;
226
227 Ok(txid)
228 }
229
230 #[cfg(feature = "test-utils")]
234 pub async fn create_send_on_chain_transaction(
235 &self,
236 to_address: Address,
237 to_amount: Amount,
238 ) -> Result<(Transaction, Vec<TxOut>), Error> {
239 self.create_send_on_chain_transaction_inner(to_address, to_amount)
240 .await
241 }
242
243 pub(crate) async fn create_send_on_chain_transaction_inner(
244 &self,
245 to_address: Address,
246 to_amount: Amount,
247 ) -> Result<(Transaction, Vec<TxOut>), Error> {
248 if to_amount < self.server_info.dust {
249 return Err(Error::ad_hoc(format!(
250 "invalid amount {to_amount}, must be greater than dust: {}",
251 self.server_info.dust,
252 )));
253 }
254
255 let fee = Amount::from_sat(1_000);
257
258 let (onchain_inputs, vtxo_inputs) = coin_select_for_onchain(self, to_amount + fee).await?;
259
260 let change_address = self.inner.wallet.get_onchain_address()?;
261
262 let sign = move |input: &mut psbt::Input, msg: bitcoin::secp256k1::Message| match &input
263 .witness_script
264 {
265 None => Err(ark_core::Error::ad_hoc(
266 "Missing witness script for psbt::Input when signing unilateral exit transaction",
267 )),
268 Some(script) => {
269 let mut res = vec![];
270 let pks = extract_checksig_pubkeys(script);
271
272 for pk in pks {
273 if let Ok(keypair) = self.keypair_by_pk(&pk) {
274 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
275 let pk = keypair.x_only_public_key().0;
276 res.push((sig, pk))
277 }
278
279 if let Ok(sig) = self.inner.wallet.sign_for_pk(&pk, &msg) {
280 res.push((sig, pk))
281 }
282 }
283
284 Ok(res)
285 }
286 };
287
288 let tx = create_unilateral_exit_transaction(
289 to_address,
290 to_amount,
291 change_address,
292 &onchain_inputs,
293 &vtxo_inputs,
294 sign,
295 )
296 .map_err(Error::from)?;
297
298 let prevouts = onchain_inputs
299 .iter()
300 .map(unilateral_exit::OnChainInput::previous_output)
301 .chain(
302 vtxo_inputs
303 .iter()
304 .map(unilateral_exit::VtxoInput::previous_output),
305 )
306 .collect();
307
308 Ok((tx, prevouts))
309 }
310}