use crate::types::{
entries::{global::lanes::UnitIssuerMap, GlobalDefinitionExt, LaneDefinition, UnitDefinition},
Ledger, TxStateManager,
};
use hdi::prelude::{
trace, wasm_error, ActionHash, ActionHashB64, AgentPubKeyB64, Deserialize, ExternResult,
Serialize,
};
use rhai::Map;
use serde::Deserializer;
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
use zfuel::fuel::ZFuel;
pub type UnitIndex = u8;
#[derive(Debug, Serialize, Clone, PartialEq, Eq, Default)]
pub struct UnitIndexMap(pub BTreeMap<String, ActionHashB64>);
impl UnitIndexMap {
pub fn new() -> Self {
Self(BTreeMap::new())
}
pub fn get_unit_indexes(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
}
impl<'de> Deserialize<'de> for UnitIndexMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map: BTreeMap<String, ActionHashB64> = BTreeMap::deserialize(deserializer)?;
for key in map.keys() {
key.parse::<UnitIndex>().map_err(serde::de::Error::custom)?;
}
Ok(Self(map))
}
}
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
pub struct UnitMap(BTreeMap<String, ZFuel>);
impl<'de> Deserialize<'de> for UnitMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map: BTreeMap<String, ZFuel> = BTreeMap::deserialize(deserializer)?;
for key in map.keys() {
key.parse::<UnitIndex>().map_err(serde::de::Error::custom)?;
}
Ok(Self(map))
}
}
impl TryFrom<Value> for UnitMap {
type Error = String;
fn try_from(value: Value) -> Result<Self, Self::Error> {
serde_json::from_value(value).map_err(|e| e.to_string())
}
}
impl Default for UnitMap {
fn default() -> Self {
Self::new()
}
}
impl From<Vec<(u32, &str)>> for UnitMap {
fn from(amounts: Vec<(u32, &str)>) -> Self {
let mut map = std::collections::BTreeMap::new();
for (k, v) in amounts {
map.insert(k.to_string(), v.parse().expect("Failed to parse amount"));
}
Self(map)
}
}
impl UnitMap {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn remove_zero_amounts(&mut self, skip_base_unit: bool) {
self.0.retain(|key, amount| {
if skip_base_unit && key == "0" {
return true;
}
amount != &ZFuel::zero()
});
}
pub fn retain_positive_only(&mut self) {
self.0.retain(|_, amount| *amount > ZFuel::zero());
}
pub fn invert_signs(
&self,
global_definition: &GlobalDefinitionExt,
lane_definitions: &[ActionHash],
) -> ExternResult<Self> {
let unit_def_map = self.get_unit_definition(global_definition, lane_definitions)?;
let mut new_amount = Self::new();
for (key, amount) in self.0.iter() {
let unit_def = unit_def_map
.get(key)
.ok_or(wasm_error!("Unit definition not found for index: {}", key))?;
if unit_def.unit_type.should_invert_sign() {
new_amount.0.insert(key.clone(), (ZFuel::zero() - amount)?);
} else {
new_amount.0.insert(key.clone(), *amount);
}
}
Ok(new_amount)
}
pub fn differing_unit_indexes(&self, other: &Option<Self>) -> ExternResult<Vec<String>> {
trace!("comparing self: {:?} with other: {:?}", self, other);
let empty = Self::new();
let other = other.as_ref().unwrap_or(&empty);
let all_keys: BTreeSet<String> = self.0.keys().chain(other.0.keys()).cloned().collect();
Ok(all_keys
.into_iter()
.filter(|key| self.get_safe(key) != other.get_safe(key))
.collect())
}
pub fn should_run_credit_check(
&self,
expected_ledger: &Ledger,
previous_ledger: &Ledger,
unit_def_map: &BTreeMap<String, UnitDefinition>,
) -> bool {
for key in self.0.keys() {
let expected_value = expected_ledger.balance.get_safe(key);
let expected_proposed = expected_ledger.proposed_balance.get_safe(key);
let previous_value = previous_ledger.balance.get_safe(key);
let previous_proposed = previous_ledger.proposed_balance.get_safe(key);
let expected_net = match expected_value + expected_proposed {
Ok(value) => value,
Err(_) => continue,
};
let previous_net = match previous_value + previous_proposed {
Ok(value) => value,
Err(_) => continue,
};
if expected_net < previous_net && expected_net < ZFuel::zero() {
if let Some(unit_def) = unit_def_map.get(key) {
if unit_def.unit_type.first_law_of_thermodynamics() {
return true;
}
}
}
}
false
}
pub fn summarize(
&mut self,
handle: &TxStateManager,
new_amount: Self,
unit_def_map: &BTreeMap<String, UnitDefinition>,
) -> ExternResult<()> {
for (key, unit) in new_amount.0.iter() {
let unit_def = unit_def_map
.get(key)
.ok_or(wasm_error!("Unit definition not found for index: {}", key))?;
if let Some(updated_balance) = unit_def
.unit_type
.summarize(handle, key, self.0.get(key).unwrap_or(&ZFuel::zero()), unit)
.unwrap_or_else(|_| panic!("Failed to summarize {handle:?}"))
{
self.0.insert(key.clone(), updated_balance);
}
}
Ok(())
}
fn check_validity(&self, unit_def_map: &BTreeMap<String, UnitDefinition>) -> ExternResult<()> {
for index in self.get_unit_indexes() {
if self.0.get(&index).unwrap_or(&ZFuel::zero()) == &ZFuel::zero() {
let unit_def = unit_def_map.get(&index).ok_or(wasm_error!(
"Unit definition not found for index: {}",
index
))?;
if !unit_def.is_valid_for_zero() {
return Err(wasm_error!(
"unit index {} ({}) is not valid for zero",
index,
unit_def.unit_symbol
));
}
}
}
Ok(())
}
pub fn normalize_precision(
&mut self,
global_definition: &GlobalDefinitionExt,
lane_definitions: &[ActionHash],
) -> ExternResult<BTreeMap<String, UnitDefinition>> {
let unit_def_map = self.get_unit_definition(global_definition, lane_definitions)?;
for (index, amount) in self.0.iter_mut() {
let unit_def = unit_def_map.get(index).ok_or(wasm_error!(
"Unit definition not found for index: {}",
index
))?;
unit_def.unit_type.normalize_precision(amount)?;
}
self.check_validity(&unit_def_map)?;
Ok(unit_def_map)
}
pub fn validate_units(
&self,
unit_def_map: &BTreeMap<String, UnitDefinition>,
) -> ExternResult<()> {
for (index, amount) in self.0.iter() {
let unit_def = unit_def_map.get(index).ok_or(wasm_error!(
"Unit definition not found for index: {}",
index
))?;
unit_def.unit_type.validate_unit(amount)?
}
Ok(())
}
pub fn disallow_rated_and_nameable_units(
&self,
unit_def_map: &BTreeMap<String, UnitDefinition>,
) -> ExternResult<()> {
for (index, amount) in self.0.iter() {
if amount == &ZFuel::zero() {
continue;
}
let unit_def = unit_def_map.get(index).ok_or(wasm_error!(
"Unit definition not found for index: {}",
index
))?;
if matches!(
unit_def.unit_type,
crate::types::entries::UnytType::RatedUnit(_)
| crate::types::entries::UnytType::NameableUnit(_)
) {
return Err(wasm_error!(
"Rated and Nameable units may only be transferred via agreements (parked spend and receipt), not in proposal/commitment/accept flow"
));
}
}
Ok(())
}
pub fn validate_unit_issuers(
&self,
author: &AgentPubKeyB64,
unit_issuers: &UnitIssuerMap,
unit_def_map: &BTreeMap<String, UnitDefinition>,
) -> ExternResult<()> {
for (index, amount) in self.0.iter() {
if amount <= &ZFuel::zero() {
continue;
}
let unit_def = unit_def_map.get(index).ok_or(wasm_error!(
"Unit definition not found for index: {}",
index
))?;
if !matches!(
unit_def.unit_type,
crate::types::entries::UnytType::RatedUnit(_)
| crate::types::entries::UnytType::NameableUnit(_)
) {
continue;
}
if let Some(allowed_issuers) = unit_issuers.get(index) {
if !allowed_issuers.contains(author) {
return Err(wasm_error!(format!(
"Agent {} is not allowed to issue unit index {}",
author, index
)));
}
}
}
Ok(())
}
pub fn get_unit_definition(
&self,
global_definition: &GlobalDefinitionExt,
lane_id: &[ActionHash],
) -> ExternResult<BTreeMap<String, UnitDefinition>> {
Self::get_unit_definitions(&self.get_unit_indexes(), global_definition, lane_id)
}
pub fn get_unit_definitions(
indexes: &[String],
global_definition: &GlobalDefinitionExt,
lane_id: &[ActionHash],
) -> ExternResult<BTreeMap<String, UnitDefinition>> {
let mut lanes = vec![];
for hash in lane_id {
lanes.push(LaneDefinition::must_get(hash)?);
}
let mut result = BTreeMap::new();
for index in indexes {
let service_unit_hash = global_definition
.lane_def
.service_units
.0
.get(index)
.or_else(|| lanes.iter().find_map(|l| l.service_units.0.get(index)))
.ok_or(wasm_error!(
"unable to find the service unit definition for the index: {}",
index
))?;
result.insert(
index.clone(),
UnitDefinition::must_get(&service_unit_hash.clone().into())?,
);
}
Ok(result)
}
pub fn new() -> Self {
Self(BTreeMap::new())
}
pub fn get(&self, key: &str) -> Option<ZFuel> {
self.0.get(key).cloned()
}
pub fn get_safe(&self, key: &String) -> ZFuel {
self.0.get(key).cloned().unwrap_or(ZFuel::zero())
}
pub fn get_unit_indexes(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
pub fn into_iter(&self) -> BTreeMap<String, ZFuel> {
self.0.clone()
}
pub fn load(map: BTreeMap<String, ZFuel>) -> Self {
Self(map)
}
pub fn add(&mut self, amount: Self) -> ExternResult<()> {
for (key, amount) in amount.0.iter() {
if self.0.contains_key(key) {
self.0.insert(
key.clone(),
(self.0.get(key).unwrap_or(&ZFuel::zero()) + amount)?,
);
} else {
self.0.insert(key.clone(), *amount);
}
}
Ok(())
}
pub fn sub(&mut self, amount: Self) -> ExternResult<()> {
for (key, amount) in amount.0.iter() {
if self.0.contains_key(key) {
self.0.insert(
key.clone(),
(self.0.get(key).unwrap_or(&ZFuel::zero()) - amount)?,
);
} else {
let new_amount = (ZFuel::zero() - amount)?;
self.0.insert(key.clone(), new_amount);
}
}
Ok(())
}
pub fn to_value(&self) -> Value {
let json_map: serde_json::Map<String, Value> = self
.0
.iter()
.map(|(k, v)| (k.clone(), v.to_string().into()))
.collect();
json_map.into()
}
pub fn to_map(&self) -> Map {
let mut map = Map::new();
for (k, v) in self.0.iter() {
map.insert(k.clone().into(), v.to_string().into());
}
map
}
pub fn is_zero(&self) -> bool {
if self.0.is_empty() {
return true;
}
self.0.iter().all(|(_, value)| value == &ZFuel::zero())
}
pub fn is_negative(&self) -> bool {
if self.0.is_empty() {
return false;
}
self.0.iter().any(|(_, value)| value < &ZFuel::zero())
}
pub fn negative_unit_indexes(&self) -> Vec<String> {
self.0
.iter()
.filter(|(_, value)| *value < &ZFuel::zero())
.map(|(key, _)| key.clone())
.collect()
}
pub fn sum_vec(units: Vec<UnitMap>) -> ExternResult<UnitMap> {
let mut sum = Self::new();
for unit in units {
sum.add(unit)?;
}
Ok(sum)
}
pub fn push_base_unit(&mut self, amount: ZFuel) {
self.0.insert("0".to_string(), amount);
}
pub fn get_base_unyt(&self) -> ZFuel {
match self.0.get("0") {
Some(amount) => *amount,
None => ZFuel::zero(),
}
}
pub fn less_than(&self, other: &Self) -> bool {
for (key, amount) in self.0.iter() {
if amount < other.0.get(key).unwrap_or(&ZFuel::zero()) {
return true;
}
}
false
}
pub fn contains_base_unit(&self) -> bool {
self.0.contains_key("0")
}
pub fn get_service_units(&self) -> Vec<String> {
self.0.keys().filter(|k| k != &"0").cloned().collect()
}
}
#[cfg(test)]
mod unit_map_tests {
use super::*;
#[test]
fn is_zero_true_for_empty() {
assert!(UnitMap::new().is_zero());
}
#[test]
fn is_zero_true_for_all_zero_values() {
let m = UnitMap::from(vec![(0u32, "0"), (1u32, "0")]);
assert!(m.is_zero());
}
#[test]
fn is_zero_false_when_any_nonzero() {
let m = UnitMap::from(vec![(0u32, "0"), (1u32, "10")]);
assert!(!m.is_zero());
}
#[test]
fn is_negative_false_for_empty() {
assert!(!UnitMap::new().is_negative());
}
#[test]
fn is_negative_false_for_all_positive() {
let m = UnitMap::from(vec![(0u32, "5"), (1u32, "10")]);
assert!(!m.is_negative());
}
#[test]
fn is_negative_true_when_any_negative() {
let m = UnitMap::from(vec![(0u32, "5"), (1u32, "-1")]);
assert!(m.is_negative());
}
#[test]
fn is_negative_false_when_all_zero() {
let m = UnitMap::from(vec![(0u32, "0"), (1u32, "0")]);
assert!(!m.is_negative());
}
#[test]
fn differing_indexes_reports_value_mismatch() {
let a = UnitMap::from(vec![(0u32, "5"), (1u32, "3")]);
let b = UnitMap::from(vec![(0u32, "5"), (1u32, "4")]);
assert_eq!(
a.differing_unit_indexes(&Some(b)).unwrap(),
vec!["1".to_string()]
);
}
#[test]
fn differing_indexes_reports_keys_only_in_other() {
let a = UnitMap::from(vec![(0u32, "5")]);
let b = UnitMap::from(vec![(0u32, "5"), (2u32, "7")]);
assert_eq!(
a.differing_unit_indexes(&Some(b)).unwrap(),
vec!["2".to_string()]
);
}
#[test]
fn differing_indexes_treats_missing_as_zero() {
let a = UnitMap::from(vec![(0u32, "5"), (1u32, "0")]);
let b = UnitMap::from(vec![(0u32, "5")]);
assert!(a.differing_unit_indexes(&Some(b)).unwrap().is_empty());
}
#[test]
fn differing_indexes_none_other_returns_nonzero_keys() {
let a = UnitMap::from(vec![(0u32, "5"), (1u32, "0")]);
assert_eq!(
a.differing_unit_indexes(&None).unwrap(),
vec!["0".to_string()]
);
}
}