Skip to main content

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}