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 ))
219 })
220 .collect::<Result<Vec<_>, Error>>()?;
221
222 if vtxo_inputs.is_empty() {
223 return Err(Error::ad_hoc("no matching VTXO outpoints found"));
224 }
225
226 let total_input_amount = vtxo_inputs
227 .iter()
228 .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount());
229
230 let change_amount = total_input_amount.checked_sub(to_amount).ok_or_else(|| {
231 Error::coin_select(format!(
232 "cannot afford to send {to_amount}, only have {total_input_amount}"
233 ))
234 })?;
235
236 tracing::info!(
237 %to_address,
238 %to_amount,
239 %total_input_amount,
240 change_address = %change_address.encode(),
241 %change_amount,
242 num_vtxos = vtxo_inputs.len(),
243 "Estimating fee to collaboratively redeem selected VTXOs"
244 );
245
246 let intent = self.prepare_intent(
247 &mut rng.clone(),
248 vec![], // No boarding inputs when using specific VTXOs
249 vtxo_inputs,
250 BatchOutputType::OffBoard {
251 to_address,
252 to_amount,
253 change_address,
254 change_amount,
255 },
256 batch::PrepareIntentKind::EstimateFee,
257 )?;
258
259 let amount = self.network_client().estimate_fees(intent.intent).await?;
260
261 Ok(amount)
262 }
263
264 /// Estimates the fee to join the next batch with specific VTXOs and settle to an Ark address.
265 ///
266 /// This function is similar to [`estimate_batch_fees`](Self::estimate_batch_fees), but allows
267 /// you to specify exactly which VTXOs to use as inputs instead of using all available VTXOs.
268 /// This is useful when you want to estimate fees for settling specific UTXOs into fresh VTXOs.
269 ///
270 /// # Arguments
271 ///
272 /// * `rng` - A random number generator for creating the intent
273 /// * `input_vtxos` - An iterator of [`OutPoint`]s specifying which VTXOs to use as inputs
274 /// * `to_address` - The Ark address to receive the settled funds
275 ///
276 /// # Returns
277 ///
278 /// Returns the estimated fee as a [`SignedAmount`]. The fee will be deducted from
279 /// the total input amount when joining the actual batch.
280 ///
281 /// # Errors
282 ///
283 /// Returns an error if:
284 /// - No matching VTXO outpoints are found
285 /// - Failed to fetch VTXOs
286 /// - Failed to communicate with the Ark server
287 pub async fn estimate_batch_fees_vtxo_selection<R>(
288 &self,
289 rng: &mut R,
290 input_vtxos: impl Iterator<Item = OutPoint> + Clone,
291 to_address: ArkAddress,
292 ) -> Result<SignedAmount, Error>
293 where
294 R: Rng + CryptoRng + Clone,
295 {
296 let (vtxo_list, script_pubkey_to_vtxo_map) =
297 self.list_vtxos().await.context("failed to get VTXO list")?;
298
299 let vtxo_inputs = vtxo_list
300 .all_unspent()
301 .filter(|v| input_vtxos.clone().any(|outpoint| outpoint == v.outpoint))
302 .map(|v| {
303 let vtxo = script_pubkey_to_vtxo_map.get(&v.script).ok_or_else(|| {
304 ark_core::Error::ad_hoc(format!("missing VTXO for script pubkey: {}", v.script))
305 })?;
306 let spend_info = vtxo.forfeit_spend_info()?;
307
308 Ok(intent::Input::new(
309 v.outpoint,
310 vtxo.exit_delay(),
311 // NOTE: This only works with default VTXOs (single-sig).
312 None,
313 TxOut {
314 value: v.amount,
315 script_pubkey: vtxo.script_pubkey(),
316 },
317 vtxo.tapscripts(),
318 spend_info,
319 false,
320 v.is_swept,
321 ))
322 })
323 .collect::<Result<Vec<_>, Error>>()?;
324
325 if vtxo_inputs.is_empty() {
326 return Err(Error::ad_hoc("no matching VTXO outpoints found"));
327 }
328
329 let total_input_amount = vtxo_inputs
330 .iter()
331 .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount());
332
333 tracing::info!(
334 %to_address,
335 %total_input_amount,
336 num_vtxos = vtxo_inputs.len(),
337 "Estimating fee to settle selected VTXOs"
338 );
339
340 let intent = self.prepare_intent(
341 &mut rng.clone(),
342 vec![], // No boarding inputs when using specific VTXOs
343 vtxo_inputs,
344 BatchOutputType::Board {
345 to_address,
346 to_amount: total_input_amount,
347 },
348 batch::PrepareIntentKind::EstimateFee,
349 )?;
350
351 let amount = self.network_client().estimate_fees(intent.intent).await?;
352
353 Ok(amount)
354 }
355}