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