1use anyhow::Context;
4use bitcoin::Amount;
5
6use ark::{Vtxo, VtxoId};
7use ark::fees::VtxoFeeInfo;
8
9use crate::Wallet;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct FeeEstimate {
16 pub gross_amount: Amount,
18 pub fee: Amount,
20 pub net_amount: Amount,
23 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 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 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 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 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 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 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 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 (srv, ark_info) = self.require_server().await?;
145 let offboard_feerate = srv.offboard_feerate().await?;
146 let script_buf = address.script_pubkey();
147 let current_height = self.inner.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 fee_info = Vec::with_capacity(capacity);
153 let mut amount = Amount::ZERO;
154 for vtxo in vtxos {
155 let vtxo = vtxo.as_ref();
156 vtxo_ids.push(vtxo.id());
157 fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
158 amount = amount + vtxo.amount();
159 }
160
161 let fee = ark_info.fees.offboard.calculate(
162 &script_buf,
163 amount,
164 offboard_feerate,
165 fee_info,
166 ).context("Error whilst calculating offboard fee")?;
167
168 let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
169 Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
170 }
171
172 pub async fn estimate_offboard_all(
175 &self,
176 address: &bitcoin::Address,
177 ) -> anyhow::Result<FeeEstimate> {
178 let vtxos = self.spendable_vtxos().await?;
179 self.estimate_offboard(address, &vtxos).await
180 }
181
182 pub async fn estimate_refresh_fee<G>(
185 &self,
186 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
187 ) -> anyhow::Result<FeeEstimate> {
188 let (_, ark_info) = self.require_server().await?;
189 let current_height = self.inner.chain.tip().await?;
190
191 let vtxos = vtxos.into_iter();
192 let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
193 let mut vtxo_ids = Vec::with_capacity(capacity);
194 let mut vtxo_fee_infos = Vec::with_capacity(capacity);
195 let mut total_amount = Amount::ZERO;
196 for vtxo in vtxos.into_iter() {
197 let vtxo = vtxo.as_ref();
198 vtxo_ids.push(vtxo.id());
199 vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
200 total_amount = total_amount + vtxo.amount();
201 }
202
203 if let Some(max) = ark_info.max_vtxo_amount {
204 if total_amount > max {
205 bail!("total refresh amount of {} exceeds maximum value of {}", total_amount, max);
206 }
207 }
208
209 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
211 let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
212 Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
213 }
214
215 pub async fn estimate_send_onchain(
224 &self,
225 address: &bitcoin::Address,
226 amount: Amount,
227 ) -> anyhow::Result<FeeEstimate> {
228 let (srv, ark_info) = self.require_server().await?;
229 let offboard_feerate = srv.offboard_feerate().await?;
230 let script_buf = address.script_pubkey();
231
232 let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
233 amount, |a, v|
234 ark_info.fees.offboard.calculate(&script_buf, a, offboard_feerate, v)
235 .ok_or_else(|| anyhow!("Error whilst calculating fee"))
236 ).await {
237 Ok((inputs, fee)) => (inputs, fee),
238 Err(_) => {
239 let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
242 let fee = ark_info.fees.offboard.calculate(
243 &script_buf, amount, offboard_feerate, info,
244 ).context("fee overflowed")?;
245 (Vec::new(), fee)
246 }
247 };
248
249 let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
250 let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
251
252 Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
253 }
254}