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