chio-settle 0.1.2

Settlement runtime for Chio web3 escrow and bond execution
Documentation
use chio_core::capability::scope::MonetaryAmount;
use serde::{Deserialize, Serialize};

use super::validation::{
    digest, parse_base_units, validate_chain_id, validate_currency, validate_digest,
    validate_evm_address, validate_text, I_JSON_MAX_SAFE_INTEGER,
};
use super::ChannelError;

pub const CHANNEL_ASSET_BINDING_SCHEMA: &str = "chio.channel.asset-binding.v1";

const ASSET_BINDING_DIGEST_DOMAIN: &[u8] = b"chio.channel.asset-binding.digest.v1\0";
const MAX_DECIMALS: u8 = 38;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChannelAssetBindingV1 {
    pub schema: String,
    pub currency: String,
    pub protocol_minor_unit_decimals: u8,
    pub chain_id: String,
    pub token_address: String,
    pub token_symbol: String,
    pub token_decimals: u8,
    pub settlement_policy_digest: String,
}

impl ChannelAssetBindingV1 {
    pub fn validate(&self) -> Result<(), ChannelError> {
        if self.schema != CHANNEL_ASSET_BINDING_SCHEMA {
            return Err(ChannelError::InvalidField("asset_binding_schema"));
        }
        validate_currency(&self.currency)?;
        validate_chain_id(&self.chain_id)?;
        validate_evm_address("token_address", &self.token_address)?;
        validate_text("token_symbol", &self.token_symbol)?;
        validate_digest("settlement_policy_digest", &self.settlement_policy_digest)?;
        if self.protocol_minor_unit_decimals > MAX_DECIMALS || self.token_decimals > MAX_DECIMALS {
            return Err(ChannelError::InvalidField("asset_decimals"));
        }
        Ok(())
    }

    pub fn digest(&self) -> Result<String, ChannelError> {
        self.validate()?;
        digest(ASSET_BINDING_DIGEST_DOMAIN, self)
    }

    pub fn token_base_units(&self, amount: &MonetaryAmount) -> Result<String, ChannelError> {
        self.validate()?;
        validate_currency(&amount.currency)?;
        if amount.currency != self.currency {
            return Err(ChannelError::InvalidField("amount_currency"));
        }
        if amount.units > I_JSON_MAX_SAFE_INTEGER {
            return Err(ChannelError::InvalidField("amount_units"));
        }
        let units = scale_units(
            u128::from(amount.units),
            self.protocol_minor_unit_decimals,
            self.token_decimals,
        )?;
        Ok(units.to_string())
    }

    pub fn monetary_amount(&self, token_base_units: &str) -> Result<MonetaryAmount, ChannelError> {
        self.validate()?;
        let token_units = parse_base_units(token_base_units)?;
        let units = scale_units(
            token_units,
            self.token_decimals,
            self.protocol_minor_unit_decimals,
        )?;
        let units = u64::try_from(units)
            .ok()
            .filter(|units| *units <= I_JSON_MAX_SAFE_INTEGER)
            .ok_or(ChannelError::ArithmeticOverflow)?;
        Ok(MonetaryAmount {
            units,
            currency: self.currency.clone(),
        })
    }

    pub fn verify_round_trip(
        &self,
        amount: &MonetaryAmount,
        token_base_units: &str,
    ) -> Result<(), ChannelError> {
        let expected = self.token_base_units(amount)?;
        if expected != token_base_units || self.monetary_amount(token_base_units)? != *amount {
            return Err(ChannelError::InexactAmount);
        }
        Ok(())
    }
}

fn scale_units(
    units: u128,
    source_decimals: u8,
    target_decimals: u8,
) -> Result<u128, ChannelError> {
    if target_decimals >= source_decimals {
        let scale = 10_u128
            .checked_pow(u32::from(target_decimals - source_decimals))
            .ok_or(ChannelError::ArithmeticOverflow)?;
        units
            .checked_mul(scale)
            .ok_or(ChannelError::ArithmeticOverflow)
    } else {
        let divisor = 10_u128
            .checked_pow(u32::from(source_decimals - target_decimals))
            .ok_or(ChannelError::ArithmeticOverflow)?;
        if !units.is_multiple_of(divisor) {
            return Err(ChannelError::InexactAmount);
        }
        Ok(units / divisor)
    }
}