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_lightning_receive_fee(&self, amount: Amount) -> Result<FeeEstimate> {
58 let (_, ark_info) = self.require_server().await?;
59
60 let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
61 let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
62
63 Ok(FeeEstimate::new(amount, fee, net_amount, vec![]))
64 }
65
66 pub async fn estimate_lightning_send_fee(&self, amount: Amount) -> Result<FeeEstimate> {
75 let (_, ark_info) = self.require_server().await?;
76
77 let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
78 amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
79 ).await {
80 Ok((inputs, fee)) => (inputs, fee),
81 Err(_) => {
82 let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
85 let fee = ark_info.fees.lightning_send.calculate(amount, info)
86 .context("fee overflowed")?;
87 (Vec::new(), fee)
88 },
89 };
90 let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
91 let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
92
93 Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
94 }
95
96 pub async fn estimate_offboard<G>(
99 &self,
100 address: &bitcoin::Address,
101 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
102 ) -> Result<FeeEstimate> {
103 let (_, ark_info) = self.require_server().await?;
104 let script_buf = address.script_pubkey();
105 let current_height = self.chain.tip().await?;
106
107 let vtxos = vtxos.into_iter();
108 let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
109 let mut vtxo_ids = Vec::with_capacity(capacity);
110 let mut fee_info = Vec::with_capacity(capacity);
111 let mut amount = Amount::ZERO;
112 for vtxo in vtxos {
113 let vtxo = vtxo.as_ref();
114 vtxo_ids.push(vtxo.id());
115 fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
116 amount = amount + vtxo.amount();
117 }
118
119 let fee = ark_info.fees.offboard.calculate(
120 &script_buf,
121 amount,
122 ark_info.offboard_feerate,
123 fee_info,
124 ).context("Error whilst calculating offboard fee")?;
125
126 let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
127 Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
128 }
129
130 pub async fn estimate_offboard_all(
133 &self,
134 address: &bitcoin::Address,
135 ) -> Result<FeeEstimate> {
136 let vtxos = self.spendable_vtxos().await?;
137 self.estimate_offboard(address, &vtxos).await
138 }
139
140 pub async fn estimate_refresh_fee<G>(
143 &self,
144 vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
145 ) -> Result<FeeEstimate> {
146 let (_, ark_info) = self.require_server().await?;
147 let current_height = self.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 vtxo_fee_infos = Vec::with_capacity(capacity);
153 let mut total_amount = Amount::ZERO;
154 for vtxo in vtxos.into_iter() {
155 let vtxo = vtxo.as_ref();
156 vtxo_ids.push(vtxo.id());
157 vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
158 total_amount = total_amount + vtxo.amount();
159 }
160
161 let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
163 let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
164 Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
165 }
166
167 pub async fn estimate_send_onchain(
176 &self,
177 address: &bitcoin::Address,
178 amount: Amount,
179 ) -> Result<FeeEstimate> {
180 let (_, ark_info) = self.require_server().await?;
181 let script_buf = address.script_pubkey();
182
183 let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
184 amount, |a, v|
185 ark_info.fees.offboard.calculate(&script_buf, a, ark_info.offboard_feerate, v)
186 .ok_or_else(|| anyhow!("Error whilst calculating fee"))
187 ).await {
188 Ok((inputs, fee)) => (inputs, fee),
189 Err(_) => {
190 let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
193 let fee = ark_info.fees.offboard.calculate(
194 &script_buf, amount, ark_info.offboard_feerate, info,
195 ).context("fee overflowed")?;
196 (Vec::new(), fee)
197 }
198 };
199
200 let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
201 let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
202
203 Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
204 }
205}