chio_settle/channel/
amount.rs1use 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}