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