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