chio-settle 0.1.2

Settlement runtime for Chio web3 escrow and bond execution
Documentation
use chio_core::canonical::canonical_json_bytes;
use chio_core::crypto::sha256_hex;
use serde::{Deserialize, Deserializer, Serialize};

use super::ChannelError;

pub(super) const I_JSON_MAX_SAFE_INTEGER: u64 = (1_u64 << 53) - 1;
pub(super) const MAX_TEXT_BYTES: usize = 2_048;

pub(super) fn digest<T: Serialize>(domain: &[u8], value: &T) -> Result<String, ChannelError> {
    let canonical = canonical_json_bytes(value)
        .map_err(|error| ChannelError::Canonicalization(error.to_string()))?;
    let mut bytes = Vec::with_capacity(domain.len() + canonical.len());
    bytes.extend_from_slice(domain);
    bytes.extend_from_slice(&canonical);
    Ok(sha256_hex(&bytes))
}

pub(super) fn deserialize_present_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    T::deserialize(deserializer).map(Some)
}

pub(super) fn validate_text(field: &'static str, value: &str) -> Result<(), ChannelError> {
    if value.is_empty()
        || value.len() > MAX_TEXT_BYTES
        || value.trim() != value
        || value.chars().any(char::is_control)
    {
        return Err(ChannelError::InvalidField(field));
    }
    Ok(())
}

pub(super) fn validate_digest(field: &'static str, value: &str) -> Result<(), ChannelError> {
    if value.len() != 64
        || !value
            .bytes()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    {
        return Err(ChannelError::InvalidField(field));
    }
    Ok(())
}

pub(super) fn validate_currency(currency: &str) -> Result<(), ChannelError> {
    if currency.len() != 3 || !currency.bytes().all(|byte| byte.is_ascii_uppercase()) {
        return Err(ChannelError::InvalidField("currency"));
    }
    Ok(())
}

pub(super) fn validate_positive(field: &'static str, value: u64) -> Result<(), ChannelError> {
    if value == 0 || value > I_JSON_MAX_SAFE_INTEGER {
        return Err(ChannelError::InvalidField(field));
    }
    Ok(())
}

pub(super) fn validate_chain_id(value: &str) -> Result<(), ChannelError> {
    let Some(numeric) = value.strip_prefix("eip155:") else {
        return Err(ChannelError::InvalidField("chain_id"));
    };
    if numeric.starts_with('0')
        || numeric
            .parse::<u64>()
            .ok()
            .filter(|value| *value != 0)
            .is_none()
    {
        return Err(ChannelError::InvalidField("chain_id"));
    }
    Ok(())
}

pub(super) fn validate_evm_address(field: &'static str, value: &str) -> Result<(), ChannelError> {
    if value.len() != 42
        || !value.starts_with("0x")
        || !value[2..]
            .bytes()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    {
        return Err(ChannelError::InvalidField(field));
    }
    Ok(())
}

pub(super) fn validate_evm_hash(field: &'static str, value: &str) -> Result<(), ChannelError> {
    if value.len() != 66
        || !value.starts_with("0x")
        || !value[2..]
            .bytes()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    {
        return Err(ChannelError::InvalidField(field));
    }
    Ok(())
}

pub(super) fn parse_base_units(value: &str) -> Result<u128, ChannelError> {
    if value.is_empty()
        || value.len() > 39
        || value.len() > 1 && value.starts_with('0')
        || !value.bytes().all(|byte| byte.is_ascii_digit())
    {
        return Err(ChannelError::InvalidField("token_base_units"));
    }
    value
        .parse::<u128>()
        .map_err(|_| ChannelError::InvalidField("token_base_units"))
}