bark/
fees.rs

1//! Fee estimation for various wallet operations.
2
3use anyhow::{Context, Result};
4use bitcoin::Amount;
5
6use ark::{Vtxo, VtxoId};
7use ark::fees::VtxoFeeInfo;
8
9use crate::Wallet;
10
11/// Result of a fee estimation containing the total cost, fee amount, and VTXOs used. It's very
12/// important to consider that fees can change over time, so you should expect to renew this
13/// estimate frequently when presenting this information to users.
14#[derive(Debug, Clone)]
15pub struct FeeEstimate {
16	/// The gross amount that will be received/sent
17	pub gross_amount: Amount,
18	/// The fee amount charged by the server.
19	pub fee: Amount,
20	/// The net amount that will be received/sent.
21	pub net_amount: Amount,
22	/// The VTXOs that would be used for this operation, if necessary.
23	pub vtxos_spent: Vec<VtxoId>,
24}
25
26impl FeeEstimate {
27	pub fn new(
28		gross_amount: Amount,
29		fee: Amount,
30		net_amount: Amount,
31		vtxos_spent: Vec<VtxoId>,
32	) -> Self {
33		Self {
34			gross_amount,
35			fee,
36			net_amount,
37			vtxos_spent,
38		}
39	}
40}
41
42impl Wallet {
43	/// Estimate fees for a board operation. `FeeEstimate::net_amount` will be the amount of the
44	/// newly boarded VTXO. Note: This doesn't include the onchain cost of creating the chain
45	/// anchor transaction.
46	pub async fn estimate_board_offchain_fee(&self, board_amount: Amount) -> Result<FeeEstimate> {
47		let (_, ark_info) = self.require_server().await?;
48		let fee = ark_info.fees.board.calculate(board_amount).context("fee overflowed")?;
49		let net_amount = board_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
50
51		Ok(FeeEstimate::new(board_amount, fee, net_amount, vec![]))
52	}
53
54	/// Estimate fees for a lightning receive operation. `FeeEstimate::gross_amount` is the
55	/// lightning payment amount, `FeeEstimate::net_amount` is how much the end user will receive.
56	pub async fn estimate_lightning_receive_fee(&self, amount: Amount) -> Result<FeeEstimate> {
57		let (_, ark_info) = self.require_server().await?;
58
59		let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
60		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
61
62		Ok(FeeEstimate::new(amount, fee, net_amount, vec![]))
63	}
64
65	/// Estimate fees for a lightning send operation. `FeeEstimate::net_amount` is the amount to be
66	/// paid to a given invoice/address.
67	///
68	/// Uses the same iterative approach as `make_lightning_payment` to account for
69	/// VTXO expiry-based fees.
70	///
71	/// Will fail to estimate fees if there aren't enough funds in the wallet to make the payment.
72	pub async fn estimate_lightning_send_fee(&self, amount: Amount) -> Result<FeeEstimate> {
73		let (_, ark_info) = self.require_server().await?;
74
75		let (inputs, fee) = self.select_vtxos_to_cover_with_fee(
76			amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
77		).await?;
78
79		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
80		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
81
82		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
83	}
84
85	/// Estimate fees for an offboard operation. `FeeEstimate::net_amount` is the onchain amount the
86	/// user can expect to receive by offboarding `FeeEstimate::vtxos_used`.
87	pub async fn estimate_offboard<G>(
88		&self,
89		address: &bitcoin::Address,
90		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
91	) -> Result<FeeEstimate> {
92		let (_, ark_info) = self.require_server().await?;
93		let script_buf = address.script_pubkey();
94		let current_height = self.chain.tip().await?;
95
96		let vtxos = vtxos.into_iter();
97		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
98		let mut vtxo_ids = Vec::with_capacity(capacity);
99		let mut fee_info = Vec::with_capacity(capacity);
100		let mut amount = Amount::ZERO;
101		for vtxo in vtxos {
102			let vtxo = vtxo.as_ref();
103			vtxo_ids.push(vtxo.id());
104			fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
105			amount = amount + vtxo.amount();
106		}
107
108		let fee = ark_info.fees.offboard.calculate(
109			&script_buf,
110			amount,
111			ark_info.offboard_feerate,
112			fee_info,
113		).context("Error whilst calculating offboard fee")?;
114
115		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
116		Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
117	}
118
119	/// Estimate fees for a refresh operation (round participation). `FeeEstimate::net_amount` is
120	/// the sum of the newly refreshed VTXOs.
121	pub async fn estimate_refresh_fee<G>(
122		&self,
123		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
124	) -> Result<FeeEstimate> {
125		let (_, ark_info) = self.require_server().await?;
126		let current_height = self.chain.tip().await?;
127
128		let vtxos = vtxos.into_iter();
129		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
130		let mut vtxo_ids = Vec::with_capacity(capacity);
131		let mut vtxo_fee_infos = Vec::with_capacity(capacity);
132		let mut total_amount = Amount::ZERO;
133		for vtxo in vtxos.into_iter() {
134			let vtxo = vtxo.as_ref();
135			vtxo_ids.push(vtxo.id());
136			vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
137			total_amount = total_amount + vtxo.amount();
138		}
139
140		// Calculate refresh fees
141		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
142		let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
143		Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
144	}
145
146	/// Estimate fees for a send-onchain operation. `FeeEstimate::net_amount` is the onchain amount
147	/// the user will receive and `FeeEstimate::gross_amount` is the offchain amount the user will
148	/// pay using `FeeEstimate::vtxos_used`.
149	///
150	/// Uses the same iterative approach as `send_onchain` to account for VTXO expiry-based fees.
151	///
152	/// Will fail to estimate fees if there aren't enough funds in the wallet to make the payment.
153	pub async fn estimate_send_onchain(
154		&self,
155		address: &bitcoin::Address,
156		amount: Amount,
157	) -> Result<FeeEstimate> {
158		let (_, ark_info) = self.require_server().await?;
159		let script_buf = address.script_pubkey();
160
161		let (inputs, fee) = self.select_vtxos_to_cover_with_fee(
162			amount, |a, v|
163				ark_info.fees.offboard.calculate(&script_buf, a, ark_info.offboard_feerate, v)
164					.ok_or_else(|| anyhow!("Error whilst calculating fee"))
165		).await?;
166
167		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
168		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
169
170		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
171	}
172}