Skip to main content

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 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(&self, board_amount: Amount) -> Result<FeeEstimate> {
48		let (_, ark_info) = self.require_server().await?;
49		let fee = ark_info.fees.board.calculate(board_amount).context("fee overflowed")?;
50		let net_amount = board_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
51
52		Ok(FeeEstimate::new(board_amount, fee, net_amount, vec![]))
53	}
54
55	/// Estimate fees for a lightning receive operation. `FeeEstimate::gross_amount` is the
56	/// lightning payment amount, `FeeEstimate::net_amount` is how much the end user will receive.
57	pub async fn estimate_lightning_receive_fee(&self, amount: Amount) -> Result<FeeEstimate> {
58		let (_, ark_info) = self.require_server().await?;
59
60		let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
61		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
62
63		Ok(FeeEstimate::new(amount, fee, net_amount, vec![]))
64	}
65
66	/// Estimate fees for a lightning send operation. `FeeEstimate::net_amount` is the amount to be
67	/// paid to a given invoice/address.
68	///
69	/// Uses the same iterative approach as `make_lightning_payment` to account for
70	/// VTXO expiry-based fees.
71	///
72	/// If the wallet is lacking enough funds to send `amount` via lightning, then the estimate will
73	/// be the maximum possible fee, assuming the user acquires enough funds to cover the payment.
74	pub async fn estimate_lightning_send_fee(&self, amount: Amount) -> Result<FeeEstimate> {
75		let (_, ark_info) = self.require_server().await?;
76
77		let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
78			amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
79		).await {
80			Ok((inputs, fee)) => (inputs, fee),
81			Err(_) => {
82				// We choose to ignore every error, even those which are not due to insufficient
83				// funds.
84				let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
85				let fee = ark_info.fees.lightning_send.calculate(amount, info)
86					.context("fee overflowed")?;
87				(Vec::new(), fee)
88			},
89		};
90		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
91		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
92
93		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
94	}
95
96	/// Estimate fees for an offboard operation. `FeeEstimate::net_amount` is the onchain amount the
97	/// user can expect to receive by offboarding `FeeEstimate::vtxos_used`.
98	pub async fn estimate_offboard<G>(
99		&self,
100		address: &bitcoin::Address,
101		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
102	) -> Result<FeeEstimate> {
103		let (_, ark_info) = self.require_server().await?;
104		let script_buf = address.script_pubkey();
105		let current_height = self.chain.tip().await?;
106
107		let vtxos = vtxos.into_iter();
108		let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
109		let mut vtxo_ids = Vec::with_capacity(capacity);
110		let mut fee_info = Vec::with_capacity(capacity);
111		let mut amount = Amount::ZERO;
112		for vtxo in vtxos {
113			let vtxo = vtxo.as_ref();
114			vtxo_ids.push(vtxo.id());
115			fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
116			amount = amount + vtxo.amount();
117		}
118
119		let fee = ark_info.fees.offboard.calculate(
120			&script_buf,
121			amount,
122			ark_info.offboard_feerate,
123			fee_info,
124		).context("Error whilst calculating offboard fee")?;
125
126		let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
127		Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
128	}
129
130	/// Estimate fees for offboarding the entire Ark balance to a given address.
131	/// Uses the same fee calculation as `offboard_all`.
132	pub async fn estimate_offboard_all(
133		&self,
134		address: &bitcoin::Address,
135	) -> Result<FeeEstimate> {
136		let vtxos = self.spendable_vtxos().await?;
137		self.estimate_offboard(address, &vtxos).await
138	}
139
140	/// Estimate fees for a refresh operation (round participation). `FeeEstimate::net_amount` is
141	/// the sum of the newly refreshed VTXOs.
142	pub async fn estimate_refresh_fee<G>(
143		&self,
144		vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
145	) -> Result<FeeEstimate> {
146		let (_, ark_info) = self.require_server().await?;
147		let current_height = self.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 vtxo_fee_infos = Vec::with_capacity(capacity);
153		let mut total_amount = Amount::ZERO;
154		for vtxo in vtxos.into_iter() {
155			let vtxo = vtxo.as_ref();
156			vtxo_ids.push(vtxo.id());
157			vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
158			total_amount = total_amount + vtxo.amount();
159		}
160
161		// Calculate refresh fees
162		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
163		let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
164		Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
165	}
166
167	/// Estimate fees for a send-onchain operation. `FeeEstimate::net_amount` is the onchain amount
168	/// the user will receive and `FeeEstimate::gross_amount` is the offchain amount the user will
169	/// pay using `FeeEstimate::vtxos_used`.
170	///
171	/// Uses the same iterative approach as `send_onchain` to account for VTXO expiry-based fees.
172	///
173	/// If the wallet is lacking enough funds to send `amount` onchain, then the estimate will be
174	/// the maximum possible fee, assuming the user acquires enough funds to cover the payment.
175	pub async fn estimate_send_onchain(
176		&self,
177		address: &bitcoin::Address,
178		amount: Amount,
179	) -> Result<FeeEstimate> {
180		let (_, ark_info) = self.require_server().await?;
181		let script_buf = address.script_pubkey();
182
183		let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
184			amount, |a, v|
185				ark_info.fees.offboard.calculate(&script_buf, a, ark_info.offboard_feerate, v)
186					.ok_or_else(|| anyhow!("Error whilst calculating fee"))
187		).await {
188			Ok((inputs, fee)) => (inputs, fee),
189			Err(_) => {
190				// We choose to ignore every error, even those which are not due to insufficient
191				// funds.
192				let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
193				let fee = ark_info.fees.offboard.calculate(
194					&script_buf, amount, ark_info.offboard_feerate, info,
195				).context("fee overflowed")?;
196				(Vec::new(), fee)
197			}
198		};
199
200		let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
201		let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
202
203		Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
204	}
205}