zera-sdk 0.1.0

Rust SDK for ZERA transactions, validator APIs, and bridge workflows
Documentation
use std::str::FromStr;

use bigdecimal::{BigDecimal, RoundingMode, Zero};

use crate::api::TokenInfo;
use crate::error::{Result, ZeraError};
use crate::fees::{get_decimal_places_from_denomination, get_denomination_fallback};

pub(crate) fn to_decimal(value: impl AsRef<str>) -> Result<BigDecimal> {
    BigDecimal::from_str(value.as_ref()).map_err(|error| {
        ZeraError::Serialization(format!("Invalid decimal \"{}\": {error}", value.as_ref()))
    })
}

pub(crate) fn decimal_from_f64(value: f64) -> Result<BigDecimal> {
    to_decimal(value.to_string())
}

pub(crate) fn pow10(decimals: u32) -> BigDecimal {
    if decimals == 0 {
        BigDecimal::from(1u32)
    } else {
        BigDecimal::from_str(&format!("1{}", "0".repeat(decimals as usize))).expect("pow10")
    }
}

pub(crate) fn floor_decimal(value: &BigDecimal) -> BigDecimal {
    value.with_scale_round(0, RoundingMode::Down)
}

pub(crate) fn floor_decimal_to_string(value: &BigDecimal) -> String {
    floor_decimal(value).normalized().to_string()
}

pub(crate) fn floor_to_scale(value: &BigDecimal, decimals: u32) -> BigDecimal {
    value.with_scale_round(decimals as i64, RoundingMode::Down)
}

pub(crate) fn to_smallest_units(
    amount: impl AsRef<str>,
    contract_id: &str,
    denomination: Option<&str>,
    token_info_map: Option<&std::collections::HashMap<String, TokenInfo>>,
) -> Result<String> {
    let amount = amount.as_ref();
    if amount.is_empty() {
        return Ok(String::new());
    }

    let decimals = if let Some(denomination) = denomination {
        get_decimal_places_from_denomination(denomination)?
    } else if let Some(token_info_map) = token_info_map {
        let token = token_info_map.get(contract_id).ok_or_else(|| {
            ZeraError::Validation(format!("Token info not found for {contract_id}"))
        })?;
        let denomination = if token.denomination.is_empty() {
            get_denomination_fallback(contract_id)?
        } else {
            token.denomination.clone()
        };
        get_decimal_places_from_denomination(&denomination)?
    } else {
        let denomination = get_denomination_fallback(contract_id)?;
        get_decimal_places_from_denomination(&denomination)?
    };

    let result = to_decimal(amount)? * pow10(decimals);
    Ok(floor_decimal_to_string(&result))
}

pub(crate) fn add_amounts<'a>(values: impl IntoIterator<Item = &'a str>) -> Result<BigDecimal> {
    let mut total = BigDecimal::zero();
    for value in values {
        total += to_decimal(value)?;
    }
    Ok(total)
}

pub(crate) fn validate_exact_amount_balance(
    input_amounts: &[String],
    output_amounts: &[String],
) -> Result<()> {
    let total_inputs = add_amounts(input_amounts.iter().map(String::as_str))?;
    let total_outputs = add_amounts(output_amounts.iter().map(String::as_str))?;

    if total_inputs != total_outputs {
        return Err(ZeraError::Validation(format!(
            "Amount mismatch in coin transaction: inputs ({}) !== outputs ({}). Difference: {}",
            total_inputs.normalized(),
            total_outputs.normalized(),
            (total_inputs - total_outputs).normalized()
        )));
    }

    Ok(())
}