chio-web3 0.1.2

Chio web3 settlement, anchoring, and contract-surface artifacts
Documentation
use std::collections::HashSet;

use crate::capability::scope::MonetaryAmount;
use crate::error::Web3ContractError;

pub(crate) fn ensure_non_empty(value: &str, field: &'static str) -> Result<(), Web3ContractError> {
    if value.trim().is_empty() {
        return Err(Web3ContractError::MissingField(field));
    }
    if value.trim() != value {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must not contain surrounding whitespace"
        )));
    }
    Ok(())
}

pub(crate) fn ensure_b256_hex(value: &str, field: &'static str) -> Result<(), Web3ContractError> {
    let hex = value.strip_prefix("0x").ok_or_else(|| {
        Web3ContractError::InvalidBinding(format!(
            "{field} must be a 0x-prefixed 32-byte hex value"
        ))
    })?;
    if hex.len() != 64 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(Web3ContractError::InvalidBinding(format!(
            "{field} must be a 0x-prefixed 32-byte hex value"
        )));
    }
    Ok(())
}

pub(crate) fn evm_addresses_match(left: &str, right: &str) -> Result<bool, Web3ContractError> {
    let left_hex = evm_address_hex(left).ok_or_else(invalid_evm_address)?;
    let right_hex = evm_address_hex(right).ok_or_else(invalid_evm_address)?;
    Ok(left_hex.eq_ignore_ascii_case(right_hex))
}

pub(crate) fn ensure_evm_address(
    value: &str,
    field: &'static str,
) -> Result<(), Web3ContractError> {
    evm_address_hex(value)
        .ok_or_else(|| {
            Web3ContractError::invalid_settlement(format!(
                "{field} must be a 0x-prefixed 20-byte hex EVM address"
            ))
        })
        .map(|_| ())
}

fn evm_address_hex(value: &str) -> Option<&str> {
    let hex = value.strip_prefix("0x")?;
    if hex.len() == 40 && hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        Some(hex)
    } else {
        None
    }
}

fn invalid_evm_address() -> Web3ContractError {
    Web3ContractError::invalid_settlement(
        "EVM address fields must be 0x-prefixed 20-byte hex values",
    )
}

pub(crate) fn ensure_unique_strings(
    values: &[String],
    field: &'static str,
) -> Result<(), Web3ContractError> {
    let mut seen = HashSet::new();
    for value in values {
        ensure_non_empty(value, field)?;
        if !seen.insert(value.as_str()) {
            return Err(Web3ContractError::DuplicateValue(value.clone()));
        }
    }
    Ok(())
}

pub(crate) fn ensure_unique_copy_values<T>(
    values: &[T],
    field: &'static str,
) -> Result<(), Web3ContractError>
where
    T: Eq + std::hash::Hash + Copy + std::fmt::Debug,
{
    let mut seen = HashSet::new();
    for value in values {
        if !seen.insert(*value) {
            return Err(Web3ContractError::DuplicateValue(format!(
                "{field}:{value:?}"
            )));
        }
    }
    Ok(())
}

pub(crate) fn ensure_money(
    amount: &MonetaryAmount,
    field: &'static str,
) -> Result<(), Web3ContractError> {
    if amount.units == 0 {
        return Err(Web3ContractError::invalid_settlement(format!(
            "{field} must be non-zero"
        )));
    }
    if amount.currency.trim().is_empty() {
        return Err(Web3ContractError::invalid_settlement(format!(
            "{field} currency is required"
        )));
    }
    if amount.currency.len() != 3
        || !amount
            .currency
            .chars()
            .all(|character| character.is_ascii_uppercase())
    {
        return Err(Web3ContractError::invalid_settlement(format!(
            "{field} currency must be a 3-letter uppercase code"
        )));
    }
    Ok(())
}