Skip to main content

chio_settle/channel/
amount.rs

1use chio_core::capability::scope::MonetaryAmount;
2use serde::{Deserialize, Serialize};
3
4use super::validation::{
5    digest, parse_base_units, validate_chain_id, validate_currency, validate_digest,
6    validate_evm_address, validate_text, I_JSON_MAX_SAFE_INTEGER,
7};
8use super::ChannelError;
9
10pub const CHANNEL_ASSET_BINDING_SCHEMA: &str = "chio.channel.asset-binding.v1";
11
12const ASSET_BINDING_DIGEST_DOMAIN: &[u8] = b"chio.channel.asset-binding.digest.v1\0";
13const MAX_DECIMALS: u8 = 38;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase", deny_unknown_fields)]
17pub struct ChannelAssetBindingV1 {
18    pub schema: String,
19    pub currency: String,
20    pub protocol_minor_unit_decimals: u8,
21    pub chain_id: String,
22    pub token_address: String,
23    pub token_symbol: String,
24    pub token_decimals: u8,
25    pub settlement_policy_digest: String,
26}
27
28impl ChannelAssetBindingV1 {
29    pub fn validate(&self) -> Result<(), ChannelError> {
30        if self.schema != CHANNEL_ASSET_BINDING_SCHEMA {
31            return Err(ChannelError::InvalidField("asset_binding_schema"));
32        }
33        validate_currency(&self.currency)?;
34        validate_chain_id(&self.chain_id)?;
35        validate_evm_address("token_address", &self.token_address)?;
36        validate_text("token_symbol", &self.token_symbol)?;
37        validate_digest("settlement_policy_digest", &self.settlement_policy_digest)?;
38        if self.protocol_minor_unit_decimals > MAX_DECIMALS || self.token_decimals > MAX_DECIMALS {
39            return Err(ChannelError::InvalidField("asset_decimals"));
40        }
41        Ok(())
42    }
43
44    pub fn digest(&self) -> Result<String, ChannelError> {
45        self.validate()?;
46        digest(ASSET_BINDING_DIGEST_DOMAIN, self)
47    }
48
49    pub fn token_base_units(&self, amount: &MonetaryAmount) -> Result<String, ChannelError> {
50        self.validate()?;
51        validate_currency(&amount.currency)?;
52        if amount.currency != self.currency {
53            return Err(ChannelError::InvalidField("amount_currency"));
54        }
55        if amount.units > I_JSON_MAX_SAFE_INTEGER {
56            return Err(ChannelError::InvalidField("amount_units"));
57        }
58        let units = scale_units(
59            u128::from(amount.units),
60            self.protocol_minor_unit_decimals,
61            self.token_decimals,
62        )?;
63        Ok(units.to_string())
64    }
65
66    pub fn monetary_amount(&self, token_base_units: &str) -> Result<MonetaryAmount, ChannelError> {
67        self.validate()?;
68        let token_units = parse_base_units(token_base_units)?;
69        let units = scale_units(
70            token_units,
71            self.token_decimals,
72            self.protocol_minor_unit_decimals,
73        )?;
74        let units = u64::try_from(units)
75            .ok()
76            .filter(|units| *units <= I_JSON_MAX_SAFE_INTEGER)
77            .ok_or(ChannelError::ArithmeticOverflow)?;
78        Ok(MonetaryAmount {
79            units,
80            currency: self.currency.clone(),
81        })
82    }
83
84    pub fn verify_round_trip(
85        &self,
86        amount: &MonetaryAmount,
87        token_base_units: &str,
88    ) -> Result<(), ChannelError> {
89        let expected = self.token_base_units(amount)?;
90        if expected != token_base_units || self.monetary_amount(token_base_units)? != *amount {
91            return Err(ChannelError::InexactAmount);
92        }
93        Ok(())
94    }
95}
96
97fn scale_units(
98    units: u128,
99    source_decimals: u8,
100    target_decimals: u8,
101) -> Result<u128, ChannelError> {
102    if target_decimals >= source_decimals {
103        let scale = 10_u128
104            .checked_pow(u32::from(target_decimals - source_decimals))
105            .ok_or(ChannelError::ArithmeticOverflow)?;
106        units
107            .checked_mul(scale)
108            .ok_or(ChannelError::ArithmeticOverflow)
109    } else {
110        let divisor = 10_u128
111            .checked_pow(u32::from(source_decimals - target_decimals))
112            .ok_or(ChannelError::ArithmeticOverflow)?;
113        if !units.is_multiple_of(divisor) {
114            return Err(ChannelError::InexactAmount);
115        }
116        Ok(units / divisor)
117    }
118}