1use anyhow::Context;
4use bitcoin::Amount;
5
6use ark::{Vtxo, VtxoId};
7use ark::fees::VtxoFeeInfo;
8
9use crate::Wallet;
10use crate::vtxo::selection::InputSelection;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct FeeEstimate {
17 pub gross_amount: Amount,
19 pub fee: Amount,
21 pub net_amount: Amount,
24 pub vtxos_spent: Vec<VtxoId>,
26}
27
28impl FeeEstimate {
29 pub fn new(
30 gross_amount: Amount,
31 fee: Amount,
32 net_amount: Amount,
33 vtxos_spent: Vec<VtxoId>,
34 ) -> Self {
35 Self {
36 gross_amount,
37 fee,
38 net_amount,
39 vtxos_spent,
40 }
41 }
42}
43
44impl Wallet {
45 pub async fn estimate_board_offchain_fee(
49 &self,
50 board_amount: Amount,
51 ) -> anyhow::Result<FeeEstimate> {
52 let (_, ark_info) = self.require_server().await?;
53
54 if board_amount < ark_info.min_board_amount {
55 bail!("board amount of {} does not meet minimum value of {}",
56 board_amount, ark_info.min_board_amount,
57 );
58 }
59 if let Some(max) = ark_info.max_vtxo_amount {
60 if board_amount > max {
61 bail!("board amount of {} exceeds maximum value of {}", board_amount, max);
62 }
63 }
64
65 let fee = ark_info.fees.board.calculate(board_amount).context("fee overflowed")?;
66 let net_amount = board_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
67
68 Ok(FeeEstimate::new(board_amount, fee, net_amount, vec![]))
69 }
70
71 pub async fn estimate_arkoor_payment_fee(&self, amount: Amount) -> anyhow::Result<FeeEstimate> {
74 let zero_fee = Amount::ZERO;
75 let inputs = match self.select_any_vtxos_to_cover(amount).await {
76 Ok(inputs) => inputs,
77 Err(_) => {
78 vec![]
81 },
82 };
83
84 let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
85 Ok(FeeEstimate::new(amount, zero_fee, amount, vtxo_ids))
86 }
87
88 pub async fn estimate_lightning_receive_fee(
91 &self,
92 amount: Amount,
93 ) -> anyhow::Result<FeeEstimate> {
94 let (_, ark_info) = self.require_server().await?;
95
96 if let Some(max) = ark_info.max_vtxo_amount {
97 if amount > max {
98 bail!("amount of {} exceeds maximum value of {}", amount, max);
99 }
100 }
101
102 let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
103 let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
104
105 Ok(FeeEstimate::new(amount, fee, net_amount, vec![]))
106 }
107
108 pub async fn estimate_lightning_send_fee(&self, amount: Amount) -> anyhow::Result<FeeEstimate> {
117 let (_, ark_info) = self.require_server().await?;
118
119 let (inputs, fee) = match self.select_any_vtxos_to_cover_with_fee(
120 amount,
121 |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
122 ).await {
123 Ok((inputs, fee)) => (inputs, fee),
124 Err(_) => {
125 let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
128 let fee = ark_info.fees.lightning_send.calculate(amount, info)
129 .context("fee overflowed")?;
130 (Vec::new(), fee)
131 },
132 };
133 let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
134 let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
135
136 Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
137 }
138
139 pub async fn estimate_offboard<G>(
142 &self,
143 address: &bitcoin::Address,
144 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
145 ) -> anyhow::Result<FeeEstimate> {
146 let (srv, ark_info) = self.require_server().await?;
147 let offboard_feerate = srv.offboard_feerate().await?;
148 let script_buf = address.script_pubkey();
149 let current_height = self.inner.chain.tip().await?;
150
151 let vtxos = vtxos.into_iter();
152 let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
153 let mut vtxo_ids = Vec::with_capacity(capacity);
154 let mut fee_info = Vec::with_capacity(capacity);
155 let mut amount = Amount::ZERO;
156 for vtxo in vtxos {
157 let vtxo = vtxo.as_ref();
158 vtxo_ids.push(vtxo.id());
159 fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
160 amount = amount + vtxo.amount();
161 }
162
163 let fee = ark_info.fees.offboard.calculate(
164 &script_buf,
165 amount,
166 offboard_feerate,
167 fee_info,
168 ).context("Error whilst calculating offboard fee")?;
169
170 let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
171 Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
172 }
173
174 pub async fn estimate_offboard_all(
177 &self,
178 address: &bitcoin::Address,
179 ) -> anyhow::Result<FeeEstimate> {
180 let vtxos = self.spendable_vtxos().await?;
181 self.estimate_offboard(address, &vtxos).await
182 }
183
184 pub async fn estimate_refresh_fee<G>(
187 &self,
188 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
189 ) -> anyhow::Result<FeeEstimate> {
190 let (_, ark_info) = self.require_server().await?;
191 let current_height = self.inner.chain.tip().await?;
192
193 let vtxos = vtxos.into_iter();
194 let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
195 let mut vtxo_ids = Vec::with_capacity(capacity);
196 let mut vtxo_fee_infos = Vec::with_capacity(capacity);
197 let mut total_amount = Amount::ZERO;
198 for vtxo in vtxos.into_iter() {
199 let vtxo = vtxo.as_ref();
200 vtxo_ids.push(vtxo.id());
201 vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
202 total_amount = total_amount + vtxo.amount();
203 }
204
205 if let Some(max) = ark_info.max_vtxo_amount {
206 if total_amount > max {
207 bail!("total refresh amount of {} exceeds maximum value of {}", total_amount, max);
208 }
209 }
210
211 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
213 let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
214 Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
215 }
216
217 pub async fn estimate_send_onchain(
226 &self,
227 address: &bitcoin::Address,
228 amount: Amount,
229 ) -> anyhow::Result<FeeEstimate> {
230 let (srv, ark_info) = self.require_server().await?;
231 let offboard_feerate = srv.offboard_feerate().await?;
232 let script_buf = address.script_pubkey();
233
234 let selection = InputSelection::new()
235 .max_inputs(srv.ark_info().await.max_offboard_inputs)
236 .fee_scheme(self.inner.chain.tip().await?, |a, v|
237 ark_info.fees.offboard.calculate(&script_buf, a, offboard_feerate, v)
238 .ok_or_else(|| anyhow!("Error whilst calculating fee")),
239 );
240 let (inputs, fee) = match selection.select(self.spendable_vtxos().await?, amount) {
241 Ok((inputs, fee)) => (inputs, fee),
242 Err(_) => {
243 let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
246 let fee = ark_info.fees.offboard.calculate(
247 &script_buf, amount, offboard_feerate, info,
248 ).context("fee overflowed")?;
249 (Vec::new(), fee)
250 }
251 };
252
253 let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
254 let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
255
256 Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
257 }
258}