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)]
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 (_, ark_info) = self.require_server().await?;
145		let script_buf = address.script_pubkey();
146		let current_height = self.chain.tip().await?;
147
148		let vtxos = vtxos.into_iter();
149		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
150		let mut vtxo_ids = Vec::with_capacity(capacity);
151		let mut fee_info = Vec::with_capacity(capacity);
152		let mut amount = Amount::ZERO;
153		for vtxo in vtxos {
154			let vtxo = vtxo.as_ref();
155			vtxo_ids.push(vtxo.id());
156			fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
157			amount = amount + vtxo.amount();
158		}
159
160		let fee = ark_info.fees.offboard.calculate(
161			&script_buf,
162			amount,
163			ark_info.offboard_feerate,
164			fee_info,
165		).context("Error whilst calculating offboard fee")?;
166
167		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
168		Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
169	}
170
171	/// Estimate fees for offboarding the entire Ark balance to a given address.
172	/// Uses the same fee calculation as `offboard_all`.
173	pub async fn estimate_offboard_all(
174		&self,
175		address: &bitcoin::Address,
176	) -> anyhow::Result<FeeEstimate> {
177		let vtxos = self.spendable_vtxos().await?;
178		self.estimate_offboard(address, &vtxos).await
179	}
180
181	/// Estimate fees for a refresh operation (round participation). `FeeEstimate::net_amount` is
182	/// the sum of the newly refreshed VTXOs.
183	pub async fn estimate_refresh_fee<G>(
184		&self,
185		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
186	) -> anyhow::Result<FeeEstimate> {
187		let (_, ark_info) = self.require_server().await?;
188		let current_height = self.chain.tip().await?;
189
190		let vtxos = vtxos.into_iter();
191		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
192		let mut vtxo_ids = Vec::with_capacity(capacity);
193		let mut vtxo_fee_infos = Vec::with_capacity(capacity);
194		let mut total_amount = Amount::ZERO;
195		for vtxo in vtxos.into_iter() {
196			let vtxo = vtxo.as_ref();
197			vtxo_ids.push(vtxo.id());
198			vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
199			total_amount = total_amount + vtxo.amount();
200		}
201
202		if let Some(max) = ark_info.max_vtxo_amount {
203			if total_amount > max {
204				bail!("total refresh amount of {} exceeds maximum value of {}", total_amount, max);
205			}
206		}
207
208		// Calculate refresh fees
209		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
210		let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
211		Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
212	}
213
214	/// Estimate fees for a send-onchain operation. `FeeEstimate::net_amount` is the onchain amount
215	/// the user will receive and `FeeEstimate::gross_amount` is the offchain amount the user will
216	/// pay using `FeeEstimate::vtxos_used`.
217	///
218	/// Uses the same iterative approach as `send_onchain` to account for VTXO expiry-based fees.
219	///
220	/// If the wallet is lacking enough funds to send `amount` onchain, then the estimate will be
221	/// the maximum possible fee, assuming the user acquires enough funds to cover the payment.
222	pub async fn estimate_send_onchain(
223		&self,
224		address: &bitcoin::Address,
225		amount: Amount,
226	) -> anyhow::Result<FeeEstimate> {
227		let (_, ark_info) = self.require_server().await?;
228		let script_buf = address.script_pubkey();
229
230		let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
231			amount, |a, v|
232				ark_info.fees.offboard.calculate(&script_buf, a, ark_info.offboard_feerate, v)
233					.ok_or_else(|| anyhow!("Error whilst calculating fee"))
234		).await {
235			Ok((inputs, fee)) => (inputs, fee),
236			Err(_) => {
237				// We choose to ignore every error, even those which are not due to insufficient
238				// funds.
239				let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
240				let fee = ark_info.fees.offboard.calculate(
241					&script_buf, amount, ark_info.offboard_feerate, info,
242				).context("fee overflowed")?;
243				(Vec::new(), fee)
244			}
245		};
246
247		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
248		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
249
250		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
251	}
252}