ark_client/fee_estimation.rs
1use crate::batch;
2use crate::batch::BatchOutputType;
3use crate::error::ErrorContext;
4use crate::wallet::BoardingWallet;
5use crate::wallet::OnchainWallet;
6use crate::Client;
7use crate::Error;
8use crate::KeyProvider;
9use crate::SwapStorage;
10use ark_core::intent;
11use ark_core::ArkAddress;
12use bitcoin::Address;
13use bitcoin::Amount;
14use bitcoin::OutPoint;
15use bitcoin::SignedAmount;
16use bitcoin::TxOut;
17use rand::CryptoRng;
18use rand::Rng;
19
20impl<B, W, S, K> Client<B, W, S, K>
21where
22 B: crate::Blockchain,
23 W: BoardingWallet + OnchainWallet,
24 S: SwapStorage + 'static,
25 K: KeyProvider,
26{
27 /// Estimates the fee to collaboratively redeem VTXOs to an on-chain Bitcoin address.
28 ///
29 /// This function calculates the expected fee for moving funds from the Ark protocol
30 /// back to a standard on-chain Bitcoin address through a collaborative redemption process.
31 /// The fee is estimated by creating a simulated intent and querying the Ark server.
32 ///
33 /// # Arguments
34 ///
35 /// * `rng` - A random number generator for creating the intent
36 /// * `to_address` - The on-chain Bitcoin address to send funds to
37 /// * `to_amount` - The amount to send to the destination address
38 ///
39 /// # Returns
40 ///
41 /// Returns the estimated fee as a [`SignedAmount`]. The fee will be deducted from
42 /// the total available balance when performing the actual redemption.
43 ///
44 /// # Errors
45 ///
46 /// Returns an error if:
47 /// - The available balance is insufficient for the requested amount
48 /// - Failed to fetch VTXOs or boarding inputs
49 /// - Failed to communicate with the Ark server
50 pub async fn estimate_onchain_fees<R>(
51 &self,
52 rng: &mut R,
53 to_address: Address,
54 to_amount: Amount,
55 ) -> Result<SignedAmount, Error>
56 where
57 R: Rng + CryptoRng + Clone,
58 {
59 let (change_address, _) = self.get_offchain_address()?;
60
61 let (boarding_inputs, vtxo_inputs, total_amount) =
62 self.fetch_commitment_transaction_inputs().await?;
63
64 let change_amount = total_amount.checked_sub(to_amount).ok_or_else(|| {
65 Error::coin_select(format!(
66 "cannot afford to send {to_amount}, only have {total_amount}"
67 ))
68 })?;
69
70 tracing::info!(
71 %to_address,
72 gross_amount = %to_amount,
73 change_address = %change_address.encode(),
74 %change_amount,
75 ?boarding_inputs,
76 "Estimating fee to collaboratively redeem outputs"
77 );
78
79 let intent = self.prepare_intent(
80 &mut rng.clone(),
81 boarding_inputs,
82 vtxo_inputs,
83 BatchOutputType::OffBoard {
84 to_address,
85 to_amount,
86 change_address,
87 change_amount,
88 },
89 batch::PrepareIntentKind::EstimateFee,
90 )?;
91
92 let amount = self.network_client().estimate_fees(intent.intent).await?;
93
94 Ok(amount)
95 }
96
97 /// Estimates the fee to join the next batch and settle funds to an Ark address.
98 ///
99 /// This function calculates the expected fee for consolidating all available VTXOs
100 /// and boarding outputs into fresh VTXOs through the Ark batch process. The full
101 /// available balance will be used, with fees deducted from the resulting VTXO.
102 ///
103 /// Use this to estimate fees before calling [`settle`](crate::Client::settle) or
104 /// similar batch operations.
105 ///
106 /// # Arguments
107 ///
108 /// * `rng` - A random number generator for creating the intent
109 /// * `to_address` - The Ark address to receive the settled funds
110 ///
111 /// # Returns
112 ///
113 /// Returns the estimated fee as a [`SignedAmount`]. This fee will be deducted from
114 /// the total available balance when joining the actual batch.
115 ///
116 /// # Errors
117 ///
118 /// Returns an error if:
119 /// - Failed to fetch VTXOs or boarding inputs
120 /// - Failed to communicate with the Ark server
121 pub async fn estimate_batch_fees<R>(
122 &self,
123 rng: &mut R,
124 to_address: ArkAddress,
125 ) -> Result<SignedAmount, Error>
126 where
127 R: Rng + CryptoRng + Clone,
128 {
129 let (boarding_inputs, vtxo_inputs, total_amount) =
130 self.fetch_commitment_transaction_inputs().await?;
131
132 tracing::info!(
133 %to_address,
134 gross_amount = %total_amount,
135 ?boarding_inputs,
136 "Estimating fee to board outputs"
137 );
138
139 let intent = self.prepare_intent(
140 &mut rng.clone(),
141 boarding_inputs,
142 vtxo_inputs,
143 BatchOutputType::Board {
144 to_address,
145 to_amount: total_amount,
146 },
147 batch::PrepareIntentKind::EstimateFee,
148 )?;
149
150 let amount = self.network_client().estimate_fees(intent.intent).await?;
151
152 Ok(amount)
153 }
154
155 /// Estimates the fee to collaboratively redeem specific VTXOs to an on-chain Bitcoin address.
156 ///
157 /// This function is similar to [`estimate_onchain_fees`](Self::estimate_onchain_fees), but
158 /// allows you to specify exactly which VTXOs to use as inputs instead of using automatic
159 /// coin selection. This is useful when you want to estimate fees for redeeming specific
160 /// UTXOs.
161 ///
162 /// # Arguments
163 ///
164 /// * `rng` - A random number generator for creating the intent
165 /// * `input_vtxos` - An iterator of [`OutPoint`]s specifying which VTXOs to use as inputs
166 /// * `to_address` - The on-chain Bitcoin address to send funds to
167 /// * `to_amount` - The amount to send to the destination address
168 ///
169 /// # Returns
170 ///
171 /// Returns the estimated fee as a [`SignedAmount`]. The fee will be deducted from
172 /// the total input amount, with any remainder going to change.
173 ///
174 /// # Errors
175 ///
176 /// Returns an error if:
177 /// - No matching VTXO outpoints are found
178 /// - The total input amount is insufficient for the requested amount plus fees
179 /// - Failed to fetch VTXOs
180 /// - Failed to communicate with the Ark server
181 pub async fn estimate_onchain_fees_vtxo_selection<R>(
182 &self,
183 rng: &mut R,
184 input_vtxos: impl Iterator<Item = OutPoint> + Clone,
185 to_address: Address,
186 to_amount: Amount,
187 ) -> Result<SignedAmount, Error>
188 where
189 R: Rng + CryptoRng + Clone,
190 {
191 let (change_address, _) = self.get_offchain_address()?;
192
193 let (vtxo_list, script_pubkey_to_vtxo_map) =
194 self.list_vtxos().await.context("failed to get VTXO list")?;
195
196 let vtxo_inputs = vtxo_list
197 .all_unspent()
198 .filter(|v| input_vtxos.clone().any(|outpoint| outpoint == v.outpoint))
199 .map(|v| {
200 let vtxo = script_pubkey_to_vtxo_map.get(&v.script).ok_or_else(|| {
201 ark_core::Error::ad_hoc(format!("missing VTXO for script pubkey: {}", v.script))
202 })?;
203 let spend_info = vtxo.forfeit_spend_info()?;
204
205 Ok(intent::Input::new(
206 v.outpoint,
207 vtxo.exit_delay(),
208 // NOTE: This only works with default VTXOs (single-sig).
209 None,
210 TxOut {
211 value: v.amount,
212 script_pubkey: vtxo.script_pubkey(),
213 },
214 vtxo.tapscripts(),
215 spend_info,
216 false,
217 v.is_swept,
218 v.assets.clone(),
219 ))
220 })
221 .collect::<Result<Vec<_>, Error>>()?;
222
223 if vtxo_inputs.is_empty() {
224 return Err(Error::ad_hoc("no matching VTXO outpoints found"));
225 }
226
227 let total_input_amount = vtxo_inputs
228 .iter()
229 .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount());
230
231 let change_amount = total_input_amount.checked_sub(to_amount).ok_or_else(|| {
232 Error::coin_select(format!(
233 "cannot afford to send {to_amount}, only have {total_input_amount}"
234 ))
235 })?;
236
237 tracing::info!(
238 %to_address,
239 %to_amount,
240 %total_input_amount,
241 change_address = %change_address.encode(),
242 %change_amount,
243 num_vtxos = vtxo_inputs.len(),
244 "Estimating fee to collaboratively redeem selected VTXOs"
245 );
246
247 let intent = self.prepare_intent(
248 &mut rng.clone(),
249 vec![], // No boarding inputs when using specific VTXOs
250 vtxo_inputs,
251 BatchOutputType::OffBoard {
252 to_address,
253 to_amount,
254 change_address,
255 change_amount,
256 },
257 batch::PrepareIntentKind::EstimateFee,
258 )?;
259
260 let amount = self.network_client().estimate_fees(intent.intent).await?;
261
262 Ok(amount)
263 }
264
265 /// Estimates the fee to join the next batch with specific VTXOs and settle to an Ark address.
266 ///
267 /// This function is similar to [`estimate_batch_fees`](Self::estimate_batch_fees), but allows
268 /// you to specify exactly which VTXOs to use as inputs instead of using all available VTXOs.
269 /// This is useful when you want to estimate fees for settling specific UTXOs into fresh VTXOs.
270 ///
271 /// # Arguments
272 ///
273 /// * `rng` - A random number generator for creating the intent
274 /// * `input_vtxos` - An iterator of [`OutPoint`]s specifying which VTXOs to use as inputs
275 /// * `to_address` - The Ark address to receive the settled funds
276 ///
277 /// # Returns
278 ///
279 /// Returns the estimated fee as a [`SignedAmount`]. The fee will be deducted from
280 /// the total input amount when joining the actual batch.
281 ///
282 /// # Errors
283 ///
284 /// Returns an error if:
285 /// - No matching VTXO outpoints are found
286 /// - Failed to fetch VTXOs
287 /// - Failed to communicate with the Ark server
288 pub async fn estimate_batch_fees_vtxo_selection<R>(
289 &self,
290 rng: &mut R,
291 input_vtxos: impl Iterator<Item = OutPoint> + Clone,
292 to_address: ArkAddress,
293 ) -> Result<SignedAmount, Error>
294 where
295 R: Rng + CryptoRng + Clone,
296 {
297 let (vtxo_list, script_pubkey_to_vtxo_map) =
298 self.list_vtxos().await.context("failed to get VTXO list")?;
299
300 let vtxo_inputs = vtxo_list
301 .all_unspent()
302 .filter(|v| input_vtxos.clone().any(|outpoint| outpoint == v.outpoint))
303 .map(|v| {
304 let vtxo = script_pubkey_to_vtxo_map.get(&v.script).ok_or_else(|| {
305 ark_core::Error::ad_hoc(format!("missing VTXO for script pubkey: {}", v.script))
306 })?;
307 let spend_info = vtxo.forfeit_spend_info()?;
308
309 Ok(intent::Input::new(
310 v.outpoint,
311 vtxo.exit_delay(),
312 // NOTE: This only works with default VTXOs (single-sig).
313 None,
314 TxOut {
315 value: v.amount,
316 script_pubkey: vtxo.script_pubkey(),
317 },
318 vtxo.tapscripts(),
319 spend_info,
320 false,
321 v.is_swept,
322 v.assets.clone(),
323 ))
324 })
325 .collect::<Result<Vec<_>, Error>>()?;
326
327 if vtxo_inputs.is_empty() {
328 return Err(Error::ad_hoc("no matching VTXO outpoints found"));
329 }
330
331 let total_input_amount = vtxo_inputs
332 .iter()
333 .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount());
334
335 tracing::info!(
336 %to_address,
337 %total_input_amount,
338 num_vtxos = vtxo_inputs.len(),
339 "Estimating fee to settle selected VTXOs"
340 );
341
342 let intent = self.prepare_intent(
343 &mut rng.clone(),
344 vec![], // No boarding inputs when using specific VTXOs
345 vtxo_inputs,
346 BatchOutputType::Board {
347 to_address,
348 to_amount: total_input_amount,
349 },
350 batch::PrepareIntentKind::EstimateFee,
351 )?;
352
353 let amount = self.network_client().estimate_fees(intent.intent).await?;
354
355 Ok(amount)
356 }
357}