Skip to main content

bark/
fees.rs

1//! Fee estimation for various wallet operations.
2
3use anyhow::Context;
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, PartialEq, Eq)]
15pub struct FeeEstimate {
16	/// The total amount including fees.
17	pub gross_amount: Amount,
18	/// The fee amount charged by the server.
19	pub fee: Amount,
20	/// The amount excluding fees. For sends, this is the amount the recipient
21	/// receives. For receives, this is the amount the user gets.
22	pub net_amount: Amount,
23	/// The VTXOs that would be used for this operation, if necessary.
24	pub vtxos_spent: Vec<VtxoId>,
25}
26
27impl FeeEstimate {
28	pub fn new(
29		gross_amount: Amount,
30		fee: Amount,
31		net_amount: Amount,
32		vtxos_spent: Vec<VtxoId>,
33	) -> Self {
34		Self {
35			gross_amount,
36			fee,
37			net_amount,
38			vtxos_spent,
39		}
40	}
41}
42
43impl Wallet {
44	/// Estimate fees for a board operation. `FeeEstimate::net_amount` will be the amount of the
45	/// newly boarded VTXO. Note: This doesn't include the onchain cost of creating the chain
46	/// anchor transaction.
47	pub async fn estimate_board_offchain_fee(
48		&self,
49		board_amount: Amount,
50	) -> anyhow::Result<FeeEstimate> {
51		let (_, ark_info) = self.require_server().await?;
52
53		if board_amount < ark_info.min_board_amount {
54			bail!("board amount of {} does not meet minimum value of {}",
55				board_amount, ark_info.min_board_amount,
56			);
57		}
58		if let Some(max) = ark_info.max_vtxo_amount {
59			if board_amount > max {
60				bail!("board amount of {} exceeds maximum value of {}", board_amount, max);
61			}
62		}
63
64		let fee = ark_info.fees.board.calculate(board_amount).context("fee overflowed")?;
65		let net_amount = board_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
66
67		Ok(FeeEstimate::new(board_amount, fee, net_amount, vec![]))
68	}
69
70	/// Estimate fees for an arkoor payment operation. Currently, this is a no-op as the server
71	/// does not charge any fees for arkoor payments.
72	pub async fn estimate_arkoor_payment_fee(&self, amount: Amount) -> anyhow::Result<FeeEstimate> {
73		let zero_fee = Amount::ZERO;
74		let inputs = match self.select_vtxos_to_cover(amount).await {
75			Ok(inputs) => inputs,
76			Err(_) => {
77				// We choose to ignore every error, even those which are not due to insufficient
78				// funds.
79				vec![]
80			},
81		};
82
83		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
84		Ok(FeeEstimate::new(amount, zero_fee, amount, vtxo_ids))
85	}
86
87	/// Estimate fees for a lightning receive operation. `FeeEstimate::gross_amount` is the
88	/// lightning payment amount, `FeeEstimate::net_amount` is how much the end user will receive.
89	pub async fn estimate_lightning_receive_fee(
90		&self,
91		amount: Amount,
92	) -> anyhow::Result<FeeEstimate> {
93		let (_, ark_info) = self.require_server().await?;
94
95		if let Some(max) = ark_info.max_vtxo_amount {
96			if amount > max {
97				bail!("amount of {} exceeds maximum value of {}", amount, max);
98			}
99		}
100
101		let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
102		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
103
104		Ok(FeeEstimate::new(amount, fee, net_amount, vec![]))
105	}
106
107	/// Estimate fees for a lightning send operation. `FeeEstimate::net_amount` is the amount to be
108	/// paid to a given invoice/address.
109	///
110	/// Uses the same iterative approach as `make_lightning_payment` to account for
111	/// VTXO expiry-based fees.
112	///
113	/// If the wallet is lacking enough funds to send `amount` via lightning, then the estimate will
114	/// be the maximum possible fee, assuming the user acquires enough funds to cover the payment.
115	pub async fn estimate_lightning_send_fee(&self, amount: Amount) -> anyhow::Result<FeeEstimate> {
116		let (_, ark_info) = self.require_server().await?;
117
118		let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
119			amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
120		).await {
121			Ok((inputs, fee)) => (inputs, fee),
122			Err(_) => {
123				// We choose to ignore every error, even those which are not due to insufficient
124				// funds.
125				let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
126				let fee = ark_info.fees.lightning_send.calculate(amount, info)
127					.context("fee overflowed")?;
128				(Vec::new(), fee)
129			},
130		};
131		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
132		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
133
134		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
135	}
136
137	/// Estimate fees for an offboard operation. `FeeEstimate::net_amount` is the onchain amount the
138	/// user can expect to receive by offboarding `FeeEstimate::vtxos_used`.
139	pub async fn estimate_offboard<G>(
140		&self,
141		address: &bitcoin::Address,
142		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
143	) -> anyhow::Result<FeeEstimate> {
144		let (srv, ark_info) = self.require_server().await?;
145		let offboard_feerate = srv.offboard_feerate().await?;
146		let script_buf = address.script_pubkey();
147		let current_height = self.inner.chain.tip().await?;
148
149		let vtxos = vtxos.into_iter();
150		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
151		let mut vtxo_ids = Vec::with_capacity(capacity);
152		let mut fee_info = Vec::with_capacity(capacity);
153		let mut amount = Amount::ZERO;
154		for vtxo in vtxos {
155			let vtxo = vtxo.as_ref();
156			vtxo_ids.push(vtxo.id());
157			fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
158			amount = amount + vtxo.amount();
159		}
160
161		let fee = ark_info.fees.offboard.calculate(
162			&script_buf,
163			amount,
164			offboard_feerate,
165			fee_info,
166		).context("Error whilst calculating offboard fee")?;
167
168		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
169		Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
170	}
171
172	/// Estimate fees for offboarding the entire Ark balance to a given address.
173	/// Uses the same fee calculation as `offboard_all`.
174	pub async fn estimate_offboard_all(
175		&self,
176		address: &bitcoin::Address,
177	) -> anyhow::Result<FeeEstimate> {
178		let vtxos = self.spendable_vtxos().await?;
179		self.estimate_offboard(address, &vtxos).await
180	}
181
182	/// Estimate fees for a refresh operation (round participation). `FeeEstimate::net_amount` is
183	/// the sum of the newly refreshed VTXOs.
184	pub async fn estimate_refresh_fee<G>(
185		&self,
186		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
187	) -> anyhow::Result<FeeEstimate> {
188		let (_, ark_info) = self.require_server().await?;
189		let current_height = self.inner.chain.tip().await?;
190
191		let vtxos = vtxos.into_iter();
192		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
193		let mut vtxo_ids = Vec::with_capacity(capacity);
194		let mut vtxo_fee_infos = Vec::with_capacity(capacity);
195		let mut total_amount = Amount::ZERO;
196		for vtxo in vtxos.into_iter() {
197			let vtxo = vtxo.as_ref();
198			vtxo_ids.push(vtxo.id());
199			vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
200			total_amount = total_amount + vtxo.amount();
201		}
202
203		if let Some(max) = ark_info.max_vtxo_amount {
204			if total_amount > max {
205				bail!("total refresh amount of {} exceeds maximum value of {}", total_amount, max);
206			}
207		}
208
209		// Calculate refresh fees
210		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
211		let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
212		Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
213	}
214
215	/// Estimate fees for a send-onchain operation. `FeeEstimate::net_amount` is the onchain amount
216	/// the user will receive and `FeeEstimate::gross_amount` is the offchain amount the user will
217	/// pay using `FeeEstimate::vtxos_used`.
218	///
219	/// Uses the same iterative approach as `send_onchain` to account for VTXO expiry-based fees.
220	///
221	/// If the wallet is lacking enough funds to send `amount` onchain, then the estimate will be
222	/// the maximum possible fee, assuming the user acquires enough funds to cover the payment.
223	pub async fn estimate_send_onchain(
224		&self,
225		address: &bitcoin::Address,
226		amount: Amount,
227	) -> anyhow::Result<FeeEstimate> {
228		let (srv, ark_info) = self.require_server().await?;
229		let offboard_feerate = srv.offboard_feerate().await?;
230		let script_buf = address.script_pubkey();
231
232		let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
233			amount, |a, v|
234				ark_info.fees.offboard.calculate(&script_buf, a, offboard_feerate, v)
235					.ok_or_else(|| anyhow!("Error whilst calculating fee"))
236		).await {
237			Ok((inputs, fee)) => (inputs, fee),
238			Err(_) => {
239				// We choose to ignore every error, even those which are not due to insufficient
240				// funds.
241				let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
242				let fee = ark_info.fees.offboard.calculate(
243					&script_buf, amount, offboard_feerate, info,
244				).context("fee overflowed")?;
245				(Vec::new(), fee)
246			}
247		};
248
249		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
250		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
251
252		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
253	}
254}