use crate::types::{Chain, SwapType};
pub mod bitcoin;
pub mod liquid;
pub fn swap_fee(receive_amount: u64, service_ppm: u64, base_fee: u64) -> u64 {
base_fee.saturating_add(service_fee(receive_amount, service_ppm))
}
pub fn service_fee(receive_amount: u64, service_ppm: u64) -> u64 {
(receive_amount as u128 * service_ppm as u128 / 1_000_000).min(u64::MAX as u128) as u64
}
pub fn min_send_for_receive(receive_amount: u64, service_ppm: u64, base_fee: u64) -> Option<u64> {
receive_amount.checked_add(swap_fee(receive_amount, service_ppm, base_fee))
}
pub fn max_receive_for_send(send_amount: u64, service_ppm: u64, base_fee: u64) -> Option<u64> {
let net = send_amount.checked_sub(base_fee)? as u128;
Some((((net + 1) * 1_000_000 - 1) / (1_000_000 + service_ppm as u128)) as u64)
}
pub fn base_fee(chain: Chain, swap_type: SwapType, fee_rate: f64, safety: f64) -> u64 {
match chain {
Chain::Bitcoin => bitcoin::base_fee(swap_type, fee_rate, safety),
Chain::Liquid => liquid::base_fee(swap_type, fee_rate, safety),
}
}
pub fn min_fee_rate(chain: Chain) -> f64 {
match chain {
Chain::Bitcoin => 1.0,
Chain::Liquid => 0.1,
}
}
pub fn fee_for_vsize(chain: Chain, vsize: u64, fee_rate: f64) -> Option<u64> {
match chain {
Chain::Bitcoin => bitcoin::fee_for_vsize(vsize, fee_rate),
Chain::Liquid => liquid::fee_for_vsize(vsize, fee_rate),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fee_for_vsize_rejects_a_bad_rate_the_same_way_on_every_chain() {
for chain in [Chain::Bitcoin, Chain::Liquid] {
for rate in [f64::NAN, -1.0, f64::INFINITY] {
assert_eq!(fee_for_vsize(chain, 250, rate), None, "{chain:?} at {rate}");
}
}
}
#[test]
fn service_fee_cases() {
assert_eq!(service_fee(1_000_000, 0), 0);
assert_eq!(service_fee(0, 10_000), 0);
assert_eq!(service_fee(1_000_000, 1_000), 1_000);
assert_eq!(service_fee(2_000_000, 500), 1_000);
assert_eq!(service_fee(1, 1), 0);
assert_eq!(service_fee(999_999, 1), 0);
assert_eq!(service_fee(1_000_000, 1), 1);
assert_eq!(service_fee(u64::MAX, u64::MAX), u64::MAX);
}
#[test]
fn max_receive_for_send_is_the_largest_that_fits() {
for (send, ppm, base) in [
(1_000_000, 0, 0),
(253_000, 4_000, 2_000),
(253_001, 4_000, 2_000),
(2_250, 4_000, 2_000),
(1, 10_000, 0),
(2_000, 1_000, 2_000),
(13_171_977_363_711, 123_456, 0),
] {
let receive = max_receive_for_send(send, ppm, base).expect("covers the base fee");
let spent = receive + swap_fee(receive, ppm, base);
assert!(spent <= send, "{send}/{ppm}/{base} spends {spent}");
assert!(
receive + 1 + swap_fee(receive + 1, ppm, base) > send,
"{send}/{ppm}/{base} leaves room for one more"
);
assert!(
send - spent <= 1,
"{send}/{ppm}/{base} leaves {}",
send - spent
);
}
}
#[test]
fn max_receive_for_send_inverts_the_worked_example() {
assert_eq!(swap_fee(250_000, 4_000, 2_000), 3_000);
assert_eq!(min_send_for_receive(250_000, 4_000, 2_000), Some(253_000));
assert_eq!(max_receive_for_send(253_000, 4_000, 2_000), Some(250_000));
assert_eq!(max_receive_for_send(2_000, 4_000, 2_000), Some(0));
assert_eq!(max_receive_for_send(1_999, 4_000, 2_000), None);
}
#[test]
fn min_send_for_receive_overflows_to_none() {
assert_eq!(min_send_for_receive(u64::MAX, 0, 1), None);
assert_eq!(min_send_for_receive(1, 1_000_000, u64::MAX), None);
}
#[test]
fn swap_fee_adds_base_fee() {
assert_eq!(swap_fee(1_000_000, 1_000, 500), 1_500);
assert_eq!(swap_fee(0, 0, 0), 0);
assert_eq!(swap_fee(1_000, 0, u64::MAX), u64::MAX);
}
#[test]
fn base_fee_dispatches_to_the_chain() {
assert_eq!(
base_fee(Chain::Bitcoin, SwapType::SwapIn, 10.0, 1.0),
bitcoin::base_fee(SwapType::SwapIn, 10.0, 1.0),
);
assert_eq!(
base_fee(Chain::Liquid, SwapType::SwapOut, 0.1, 1.0),
liquid::base_fee(SwapType::SwapOut, 0.1, 1.0),
);
}
}