use anyhow::Context;
use bitcoin::Amount;
use ark::{Vtxo, VtxoId};
use ark::fees::VtxoFeeInfo;
use crate::Wallet;
#[derive(Debug, Clone)]
pub struct FeeEstimate {
pub gross_amount: Amount,
pub fee: Amount,
pub net_amount: Amount,
pub vtxos_spent: Vec<VtxoId>,
}
impl FeeEstimate {
pub fn new(
gross_amount: Amount,
fee: Amount,
net_amount: Amount,
vtxos_spent: Vec<VtxoId>,
) -> Self {
Self {
gross_amount,
fee,
net_amount,
vtxos_spent,
}
}
}
impl Wallet {
pub async fn estimate_board_offchain_fee(
&self,
board_amount: Amount,
) -> anyhow::Result<FeeEstimate> {
let (_, ark_info) = self.require_server().await?;
if board_amount < ark_info.min_board_amount {
bail!("board amount of {} does not meet minimum value of {}",
board_amount, ark_info.min_board_amount,
);
}
if let Some(max) = ark_info.max_vtxo_amount {
if board_amount > max {
bail!("board amount of {} exceeds maximum value of {}", board_amount, max);
}
}
let fee = ark_info.fees.board.calculate(board_amount).context("fee overflowed")?;
let net_amount = board_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
Ok(FeeEstimate::new(board_amount, fee, net_amount, vec![]))
}
pub async fn estimate_arkoor_payment_fee(&self, amount: Amount) -> anyhow::Result<FeeEstimate> {
let zero_fee = Amount::ZERO;
let inputs = match self.select_vtxos_to_cover(amount).await {
Ok(inputs) => inputs,
Err(_) => {
vec![]
},
};
let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
Ok(FeeEstimate::new(amount, zero_fee, amount, vtxo_ids))
}
pub async fn estimate_lightning_receive_fee(
&self,
amount: Amount,
) -> anyhow::Result<FeeEstimate> {
let (_, ark_info) = self.require_server().await?;
if let Some(max) = ark_info.max_vtxo_amount {
if amount > max {
bail!("amount of {} exceeds maximum value of {}", amount, max);
}
}
let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
Ok(FeeEstimate::new(amount, fee, net_amount, vec![]))
}
pub async fn estimate_lightning_send_fee(&self, amount: Amount) -> anyhow::Result<FeeEstimate> {
let (_, ark_info) = self.require_server().await?;
let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
).await {
Ok((inputs, fee)) => (inputs, fee),
Err(_) => {
let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
let fee = ark_info.fees.lightning_send.calculate(amount, info)
.context("fee overflowed")?;
(Vec::new(), fee)
},
};
let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
}
pub async fn estimate_offboard<G>(
&self,
address: &bitcoin::Address,
vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
) -> anyhow::Result<FeeEstimate> {
let (_, ark_info) = self.require_server().await?;
let script_buf = address.script_pubkey();
let current_height = self.chain.tip().await?;
let vtxos = vtxos.into_iter();
let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
let mut vtxo_ids = Vec::with_capacity(capacity);
let mut fee_info = Vec::with_capacity(capacity);
let mut amount = Amount::ZERO;
for vtxo in vtxos {
let vtxo = vtxo.as_ref();
vtxo_ids.push(vtxo.id());
fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
amount = amount + vtxo.amount();
}
let fee = ark_info.fees.offboard.calculate(
&script_buf,
amount,
ark_info.offboard_feerate,
fee_info,
).context("Error whilst calculating offboard fee")?;
let net_amount = amount.checked_sub(fee).unwrap_or(Amount::ZERO);
Ok(FeeEstimate::new(amount, fee, net_amount, vtxo_ids))
}
pub async fn estimate_offboard_all(
&self,
address: &bitcoin::Address,
) -> anyhow::Result<FeeEstimate> {
let vtxos = self.spendable_vtxos().await?;
self.estimate_offboard(address, &vtxos).await
}
pub async fn estimate_refresh_fee<G>(
&self,
vtxos: impl IntoIterator<Item = impl AsRef<Vtxo<G>>>,
) -> anyhow::Result<FeeEstimate> {
let (_, ark_info) = self.require_server().await?;
let current_height = self.chain.tip().await?;
let vtxos = vtxos.into_iter();
let capacity = vtxos.size_hint().1.unwrap_or(vtxos.size_hint().0);
let mut vtxo_ids = Vec::with_capacity(capacity);
let mut vtxo_fee_infos = Vec::with_capacity(capacity);
let mut total_amount = Amount::ZERO;
for vtxo in vtxos.into_iter() {
let vtxo = vtxo.as_ref();
vtxo_ids.push(vtxo.id());
vtxo_fee_infos.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, current_height));
total_amount = total_amount + vtxo.amount();
}
if let Some(max) = ark_info.max_vtxo_amount {
if total_amount > max {
bail!("total refresh amount of {} exceeds maximum value of {}", total_amount, max);
}
}
let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
let output_amount = total_amount.checked_sub(fee).unwrap_or(Amount::ZERO);
Ok(FeeEstimate::new(total_amount, fee, output_amount, vtxo_ids))
}
pub async fn estimate_send_onchain(
&self,
address: &bitcoin::Address,
amount: Amount,
) -> anyhow::Result<FeeEstimate> {
let (_, ark_info) = self.require_server().await?;
let script_buf = address.script_pubkey();
let (inputs, fee) = match self.select_vtxos_to_cover_with_fee(
amount, |a, v|
ark_info.fees.offboard.calculate(&script_buf, a, ark_info.offboard_feerate, v)
.ok_or_else(|| anyhow!("Error whilst calculating fee"))
).await {
Ok((inputs, fee)) => (inputs, fee),
Err(_) => {
let info = [VtxoFeeInfo { amount, expiry_blocks: u32::MAX }];
let fee = ark_info.fees.offboard.calculate(
&script_buf, amount, ark_info.offboard_feerate, info,
).context("fee overflowed")?;
(Vec::new(), fee)
}
};
let total_cost = amount.checked_add(fee).unwrap_or(Amount::MAX);
let vtxo_ids = inputs.into_iter().map(|v| v.id()).collect();
Ok(FeeEstimate::new(total_cost, fee, amount, vtxo_ids))
}
}