use num_bigint::BigUint;
use num_traits::ToPrimitive;
pub const MAX_DECIMALS: u8 = 38; pub const MIN_RATE: u128 = 1; pub const MAX_RATE: u128 = u128::MAX / 2; pub const MAX_DECIMAL_DIFF: u8 = 32;
#[derive(Debug, Clone, PartialEq)]
pub struct PairRate {
pub token_pair: (String, String),
pub rate: (u128, u128),
pub decimals: (u8, u8),
}
impl PairRate {
fn validate_decimals(decimals: (u8, u8)) -> Result<(), String> {
if decimals.0 > MAX_DECIMALS {
return Err(format!(
"Input decimals {} exceeds maximum allowed {}",
decimals.0, MAX_DECIMALS
));
}
if decimals.1 > MAX_DECIMALS {
return Err(format!(
"Output decimals {} exceeds maximum allowed {}",
decimals.1, MAX_DECIMALS
));
}
Ok(())
}
fn validate_rate(rate: (u128, u128)) -> Result<(), String> {
if rate.0 < MIN_RATE || rate.1 < MIN_RATE {
return Err("Rate components must be greater than 0".to_string());
}
if rate.0 > MAX_RATE || rate.1 > MAX_RATE {
return Err(format!("Rate components must be less than {}", MAX_RATE));
}
Ok(())
}
pub fn new(
token_pair: (String, String),
rate: (u128, u128),
decimals: (u8, u8),
) -> Result<Self, String> {
Self::validate_decimals(decimals)?;
Self::validate_rate(rate)?;
Ok(Self {
token_pair,
rate,
decimals,
})
}
pub fn calculate_output_amount(price: &PairRate, input_amount: u128) -> Result<u128, String> {
if input_amount == 0 {
return Err("Input amount must be greater than 0".to_string());
}
Self::validate_rate(price.rate)?;
Self::validate_decimals(price.decimals)?;
let (input_rate, output_rate) = price.rate;
if input_amount > u128::MAX / output_rate {
return Err("Input amount too large, would cause overflow".to_string());
}
let base_output = Self::safe_multiply_divide(input_amount, output_rate, input_rate)?;
let adjusted_output =
Self::adjust_decimals(base_output, price.decimals.0, price.decimals.1)?;
if adjusted_output == 0 {
return Err("Calculated output amount is zero, increase input amount".to_string());
}
Ok(adjusted_output)
}
pub fn calculate_input_amount(price: &PairRate, output_amount: u128) -> Result<u128, String> {
if output_amount == 0 {
return Err("Output amount must be greater than 0".to_string());
}
Self::validate_rate(price.rate)?;
Self::validate_decimals(price.decimals)?;
let (input_rate, output_rate) = price.rate;
if output_amount > u128::MAX / input_rate {
return Err("Output amount too large, would cause overflow".to_string());
}
let base_input = Self::safe_multiply_divide(output_amount, input_rate, output_rate)?;
let adjusted_input = Self::adjust_decimals(base_input, price.decimals.1, price.decimals.0)?;
if adjusted_input == 0 {
return Err("Calculated input amount is zero, increase output amount".to_string());
}
Ok(adjusted_input)
}
fn adjust_decimals(amount: u128, from_decimals: u8, to_decimals: u8) -> Result<u128, String> {
if from_decimals > MAX_DECIMALS || to_decimals > MAX_DECIMALS {
return Err(format!(
"Decimals must be less than or equal to {}",
MAX_DECIMALS
));
}
let decimal_diff = if from_decimals > to_decimals {
from_decimals - to_decimals
} else {
to_decimals - from_decimals
};
if decimal_diff > MAX_DECIMAL_DIFF {
return Err(format!(
"Decimal difference {} exceeds maximum allowed {}",
decimal_diff, MAX_DECIMAL_DIFF
));
}
if from_decimals == to_decimals {
return Ok(amount);
}
if from_decimals > to_decimals {
let decimal_diff = from_decimals - to_decimals;
let divisor = 10u128
.checked_pow(decimal_diff as u32)
.ok_or("Decimal divisor overflow")?;
Ok(amount / divisor)
} else {
let decimal_diff = to_decimals - from_decimals;
let multiplier = 10u128
.checked_pow(decimal_diff as u32)
.ok_or("Decimal multiplier overflow")?;
amount
.checked_mul(multiplier)
.ok_or("Decimal adjustment caused overflow".to_string())
}
}
fn safe_multiply_divide(amount: u128, multiplier: u128, divisor: u128) -> Result<u128, String> {
if divisor == 0 {
return Err("Division by zero".to_string());
}
if amount > MAX_RATE || multiplier > MAX_RATE {
return Err("Input values too large for safe calculation".to_string());
}
let amount = BigUint::from(amount);
let multiplier = BigUint::from(multiplier);
let divisor = BigUint::from(divisor);
let result = amount * multiplier / divisor;
result.to_u128().ok_or("Result exceeds u128".to_string())
}
pub fn get_price_rate(&self) -> ((u128, u128), (u8, u8)) {
(self.rate, self.decimals)
}
pub fn get_human_readable_rate(&self) -> f64 {
let (input_rate, output_rate) = self.rate;
let (input_decimals, output_decimals) = self.decimals;
let decimal_adjustment = if input_decimals > output_decimals {
10f64.powi((input_decimals - output_decimals) as i32)
} else {
1f64 / 10f64.powi((output_decimals - input_decimals) as i32)
};
(output_rate as f64 / input_rate as f64) * decimal_adjustment
}
pub fn is_valid(&self) -> bool {
Self::validate_rate(self.rate).is_ok() && Self::validate_decimals(self.decimals).is_ok()
}
}
impl Default for PairRate {
fn default() -> Self {
PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (1, 1), decimals: (18, 18), }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_output_amount() {
let price = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(10, 19), (24, 24),
)
.unwrap();
let input_amount = 200_000_000_000_000_000_000_000_000u128; let output = PairRate::calculate_output_amount(&price, input_amount).unwrap();
println!("Output amount: {}", output);
}
#[test]
fn test_calculate_output_amount_r() {
let price = PairRate::new(
("TOKEN_B".to_string(), "TOKEN_A".to_string()),
(19, 10),
(24, 24),
)
.unwrap();
let input_amount = 200_000_000_000_000_000_000_000_000u128; let output = PairRate::calculate_output_amount(&price, input_amount).unwrap();
println!("Output amount: {}", output);
}
#[test]
fn test_calculate_input_amount() {
let price = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 2),
(24, 18),
)
.unwrap();
let output_amount = 2_000_000_000_000_000_000u128; let input = PairRate::calculate_input_amount(&price, output_amount).unwrap();
println!("Input amount: {}", input);
assert_eq!(input, 1_000_000_000_000_000_000_000_000);
}
#[test]
fn test_same_decimals() {
let price = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 3),
(18, 18),
)
.unwrap();
let input_amount = 1_000_000_000_000_000_000u128; let output = PairRate::calculate_output_amount(&price, input_amount).unwrap();
assert_eq!(output, 3_000_000_000_000_000_000);
}
#[test]
fn test_output_higher_decimals() {
let price = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 2),
(18, 24),
)
.unwrap();
let input_amount = 1_000_000_000_000_000_000u128; let output = PairRate::calculate_output_amount(&price, input_amount).unwrap();
assert_eq!(output, 2_000_000_000_000_000_000_000_000);
}
#[test]
fn test_adjust_decimals() {
let result = PairRate::adjust_decimals(1_000_000_000_000_000_000_000_000, 24, 18).unwrap();
assert_eq!(result, 1_000_000_000_000_000_000);
let result = PairRate::adjust_decimals(1_000_000_000_000_000_000, 18, 24).unwrap();
assert_eq!(result, 1_000_000_000_000_000_000_000_000);
let result = PairRate::adjust_decimals(1_000_000_000_000_000_000, 18, 18).unwrap();
assert_eq!(result, 1_000_000_000_000_000_000);
}
#[test]
fn test_price_consistency() {
let price = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (3, 5), decimals: (24, 18),
};
let input_amount = 3_000_000_000_000_000_000_000_000u128; let output = PairRate::calculate_output_amount(&price, input_amount).unwrap();
let back_to_input = PairRate::calculate_input_amount(&price, output).unwrap();
assert_eq!(back_to_input, input_amount);
}
#[test]
fn test_error_handling() {
let price = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (1, 1),
decimals: (18, 18),
};
let result = PairRate::calculate_output_amount(&price, 0);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Input amount must be greater than 0".to_string()
);
let result = PairRate::calculate_input_amount(&price, 0);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Output amount must be greater than 0".to_string()
);
let invalid_price = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (0, 1),
decimals: (18, 18),
};
let result = PairRate::calculate_output_amount(&invalid_price, 1000);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Rate components must be greater than 0".to_string()
);
}
#[test]
fn test_edge_cases() {
let price = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (1, 1),
decimals: (18, 18),
};
let large_number = u128::MAX / 2;
let result = PairRate::calculate_output_amount(&price, large_number);
assert!(result.is_ok());
let small_number = 1u128;
let result = PairRate::calculate_output_amount(&price, small_number);
assert!(result.is_ok());
let price_high_precision = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (1, 1),
decimals: (6, 24),
};
let input = 1_000_000u128; let result = PairRate::calculate_output_amount(&price_high_precision, input);
assert!(result.is_ok());
let expected = input * 10u128.pow(18); assert_eq!(result.unwrap(), expected);
let result = PairRate::calculate_input_amount(&price_high_precision, expected);
assert!(result.is_ok());
assert_eq!(result.unwrap(), input); }
#[test]
fn test_helper_functions() {
let valid_price = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 1),
(18, 18),
)
.unwrap();
assert!(valid_price.is_valid());
let invalid_result = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(0, 1),
(18, 18),
);
assert!(invalid_result.is_err());
let invalid_rate_result = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(MAX_RATE + 1, 1),
(18, 18),
);
assert!(invalid_rate_result.is_err());
let invalid_decimals_result = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 1),
(MAX_DECIMALS + 1, 18),
);
assert!(invalid_decimals_result.is_err());
let price = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(2, 5),
(18, 18),
)
.unwrap();
let ((rate_in, rate_out), (dec_in, dec_out)) = price.get_price_rate();
assert_eq!(rate_in, 2);
assert_eq!(rate_out, 5);
assert_eq!(dec_in, 18);
assert_eq!(dec_out, 18);
let price1 = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(2, 5),
(18, 18),
)
.unwrap();
assert_eq!(price1.get_human_readable_rate(), 2.5);
let price2 = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 1),
(6, 9),
)
.unwrap();
assert_eq!(price2.get_human_readable_rate(), 0.001);
let price3 = PairRate::new(
("TOKEN_A".to_string(), "TOKEN_B".to_string()),
(1, 1),
(9, 6),
)
.unwrap();
assert_eq!(price3.get_human_readable_rate(), 1000.0);
let default_price = PairRate::default();
assert_eq!(default_price.rate, (1, 1));
assert_eq!(default_price.decimals, (18, 18));
assert_eq!(default_price.token_pair.0, "TOKEN_A");
assert_eq!(default_price.token_pair.1, "TOKEN_B");
assert!(default_price.is_valid());
}
#[test]
fn test_overflow_handling() {
let price = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (1, u128::MAX),
decimals: (18, 18),
};
let result = PairRate::calculate_output_amount(&price, u128::MAX);
assert!(result.is_err());
let price_high_precision = PairRate {
token_pair: ("TOKEN_A".to_string(), "TOKEN_B".to_string()),
rate: (1, 1),
decimals: (0, 38), };
let result = PairRate::calculate_output_amount(&price_high_precision, u128::MAX / 2);
assert!(result.is_err());
}
}