1use anyhow::{Context, Result};
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(&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 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 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 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 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 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 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 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 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 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 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 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}