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