zera-sdk 0.1.0

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

use bigdecimal::BigDecimal;
use zera_proto::zera_api::BalanceResponse;

use crate::error::{Result, ZeraError};
use crate::grpc::{UnaryTransport, ValidatorApiClient};
use crate::types::RpcConfig;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnhancedBalanceResponse {
    pub balance: String,
    pub denomination: String,
    pub rate: String,
    pub balance_nice: String,
    pub rate_nice: String,
}

pub async fn get_balance(
    address: &str,
    contract_id: &str,
    config: RpcConfig,
) -> Result<EnhancedBalanceResponse> {
    let client = ValidatorApiClient::new(config)?;
    get_balance_with_client(address, contract_id, &client).await
}

pub async fn get_balance_with_client<T>(
    address: &str,
    contract_id: &str,
    client: &ValidatorApiClient<T>,
) -> Result<EnhancedBalanceResponse>
where
    T: UnaryTransport,
{
    if address.trim().is_empty() {
        return Err(ZeraError::InvalidInput(
            "Address must be a non-empty string".to_string(),
        ));
    }
    if contract_id.trim().is_empty() {
        return Err(ZeraError::InvalidInput(
            "Contract ID must be a non-empty string".to_string(),
        ));
    }

    let response = match client.get_balance(address, contract_id).await {
        Ok(response) => response,
        Err(error) => {
            let message = error.to_string();
            if message.contains("Invalid Wallet") || message.contains("INVALID_ARGUMENT") {
                BalanceResponse {
                    balance: "0".to_string(),
                    denomination: "1".to_string(),
                    rate: "0".to_string(),
                }
            } else {
                return Err(ZeraError::Rpc(format!(
                    "Failed to get balance from validator: {message}"
                )));
            }
        }
    };

    enhance_balance_response(response)
}

pub fn enhance_balance_response(response: BalanceResponse) -> Result<EnhancedBalanceResponse> {
    let denomination = if response.denomination.is_empty() {
        "1".to_string()
    } else {
        response.denomination.clone()
    };
    let rate = if response.rate.is_empty() {
        "0".to_string()
    } else {
        response.rate.clone()
    };

    let balance_nice = decimal_ratio(&response.balance, &denomination)?;
    let rate_nice = decimal_ratio(&rate, "1000000000000000000")?;

    Ok(EnhancedBalanceResponse {
        balance: response.balance,
        denomination,
        rate,
        balance_nice,
        rate_nice,
    })
}

fn decimal_ratio(numerator: &str, denominator: &str) -> Result<String> {
    let numerator = BigDecimal::from_str(numerator).map_err(|error| {
        ZeraError::Serialization(format!("Invalid decimal \"{numerator}\": {error}"))
    })?;
    let denominator = BigDecimal::from_str(denominator).map_err(|error| {
        ZeraError::Serialization(format!("Invalid decimal \"{denominator}\": {error}"))
    })?;
    if denominator == 0u32 {
        return Ok("0".to_string());
    }

    Ok((numerator / denominator).normalized().to_string())
}