hive-rs 0.1.0

A Rust client library for the Hive blockchain with 1:1 dhive API parity
Documentation
use std::collections::BTreeMap;
use std::fmt;

use serde::de::Error as _;
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum NumberLike {
    I64(i64),
    U64(u64),
    String(String),
}

impl NumberLike {
    fn to_i64(&self) -> std::result::Result<i64, String> {
        match self {
            Self::I64(value) => Ok(*value),
            Self::U64(value) => {
                i64::try_from(*value).map_err(|_| format!("value '{value}' exceeds i64 range"))
            }
            Self::String(value) => value
                .parse::<i64>()
                .map_err(|err| format!("invalid integer string '{value}': {err}")),
        }
    }

    fn to_u64(&self) -> std::result::Result<u64, String> {
        match self {
            Self::I64(value) => u64::try_from(*value)
                .map_err(|_| format!("value '{value}' cannot be represented as u64")),
            Self::U64(value) => Ok(*value),
            Self::String(value) => value
                .parse::<u64>()
                .map_err(|err| format!("invalid unsigned integer string '{value}': {err}")),
        }
    }

    fn to_u128(&self) -> std::result::Result<u128, String> {
        match self {
            Self::I64(value) => u128::try_from(*value)
                .map_err(|_| format!("value '{value}' cannot be represented as u128")),
            Self::U64(value) => Ok((*value).into()),
            Self::String(value) => value
                .parse::<u128>()
                .map_err(|err| format!("invalid unsigned integer string '{value}': {err}")),
        }
    }
}

fn deserialize_i64<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    NumberLike::deserialize(deserializer)?
        .to_i64()
        .map_err(D::Error::custom)
}

fn deserialize_u64<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    NumberLike::deserialize(deserializer)?
        .to_u64()
        .map_err(D::Error::custom)
}

fn deserialize_u128<'de, D>(deserializer: D) -> std::result::Result<u128, D::Error>
where
    D: serde::Deserializer<'de>,
{
    NumberLike::deserialize(deserializer)?
        .to_u128()
        .map_err(D::Error::custom)
}

fn deserialize_opt_i64<'de, D>(deserializer: D) -> std::result::Result<Option<i64>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    Option::<NumberLike>::deserialize(deserializer)?
        .map(|value| value.to_i64())
        .transpose()
        .map_err(D::Error::custom)
}

fn deserialize_i64_map<'de, D>(
    deserializer: D,
) -> std::result::Result<BTreeMap<String, i64>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let raw = BTreeMap::<String, NumberLike>::deserialize(deserializer)?;
    raw.into_iter()
        .map(|(key, value)| value.to_i64().map(|parsed| (key, parsed)))
        .collect::<std::result::Result<BTreeMap<_, _>, _>>()
        .map_err(D::Error::custom)
}

fn deserialize_i64_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<i64>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let raw = Vec::<NumberLike>::deserialize(deserializer)?;
    raw.into_iter()
        .map(|value| value.to_i64())
        .collect::<std::result::Result<Vec<_>, _>>()
        .map_err(D::Error::custom)
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Manabar {
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub current_mana: i64,
    #[serde(default, deserialize_with = "deserialize_u64")]
    pub last_update_time: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RCAccount {
    pub account: String,
    #[serde(default, deserialize_with = "deserialize_opt_i64")]
    pub delegated_rc: Option<i64>,
    #[serde(default, deserialize_with = "deserialize_opt_i64")]
    pub max_rc: Option<i64>,
    #[serde(default)]
    pub rc_manabar: Option<Manabar>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RCPriceCurveParams {
    #[serde(default, deserialize_with = "deserialize_u128")]
    pub coeff_a: u128,
    #[serde(default, deserialize_with = "deserialize_u128")]
    pub coeff_b: u128,
    #[serde(default)]
    pub shift: u8,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RCDecayParams {
    #[serde(default, deserialize_with = "deserialize_u64")]
    pub decay_per_time_unit: u64,
    #[serde(default)]
    pub decay_per_time_unit_denom_shift: u8,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RCDynamicsParams {
    #[serde(default, deserialize_with = "deserialize_u64")]
    pub resource_unit: u64,
    #[serde(default, deserialize_with = "deserialize_u64")]
    pub budget_per_time_unit: u64,
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub pool_eq: i64,
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub max_pool_size: i64,
    #[serde(default)]
    pub decay_params: RCDecayParams,
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub min_decay: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RCResourceParam {
    #[serde(default)]
    pub resource_dynamics_params: RCDynamicsParams,
    #[serde(default)]
    pub price_curve_params: RCPriceCurveParams,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RCSizeInfo {
    #[serde(default, deserialize_with = "deserialize_i64_map")]
    pub resource_execution_time: BTreeMap<String, i64>,
    #[serde(default, deserialize_with = "deserialize_i64_map")]
    pub resource_state_bytes: BTreeMap<String, i64>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RCParams {
    #[serde(default)]
    pub resource_names: Vec<String>,
    #[serde(default)]
    pub resource_params: BTreeMap<String, RCResourceParam>,
    #[serde(default)]
    pub size_info: RCSizeInfo,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RCPoolResource {
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub pool: i64,
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub fill_level: i64,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RCPool {
    #[serde(default)]
    pub resource_pool: BTreeMap<String, RCPoolResource>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RcStats {
    #[serde(default, deserialize_with = "deserialize_i64")]
    pub regen: i64,
    #[serde(default, deserialize_with = "deserialize_i64_vec")]
    pub share: Vec<i64>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

impl fmt::Display for RcStats {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "regen={}, share={:?}", self.regen, self.share)
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::types::{RCAccount, RCParams, RCPool, RcStats};

    #[test]
    fn rc_account_parses_mixed_numeric_encodings() {
        let account: RCAccount = serde_json::from_value(json!({
            "account": "alice",
            "delegated_rc": 0,
            "max_rc": "135630143570",
            "rc_manabar": {
                "current_mana": "135375191366",
                "last_update_time": 1550731380
            }
        }))
        .expect("account should parse");

        assert_eq!(account.delegated_rc, Some(0));
        assert_eq!(account.max_rc, Some(135_630_143_570));
        assert_eq!(
            account.rc_manabar.as_ref().expect("manabar").current_mana,
            135_375_191_366
        );
    }

    #[test]
    fn rc_params_parse_with_resource_maps_and_size_info() {
        let params: RCParams = serde_json::from_value(json!({
            "resource_names": ["resource_history_bytes"],
            "resource_params": {
                "resource_history_bytes": {
                    "price_curve_params": {
                        "coeff_a": "10525659774662010880",
                        "coeff_b": 211332338,
                        "shift": 50
                    },
                    "resource_dynamics_params": {
                        "resource_unit": 1,
                        "budget_per_time_unit": 43403,
                        "pool_eq": 27050539251_i64,
                        "max_pool_size": "54101078501",
                        "decay_params": {
                            "decay_per_time_unit": 3613026481_u64,
                            "decay_per_time_unit_denom_shift": 51
                        },
                        "min_decay": 0
                    }
                }
            },
            "size_info": {
                "resource_execution_time": {
                    "transaction_time": 6622
                },
                "resource_state_bytes": {
                    "transaction_base_size": "128"
                }
            }
        }))
        .expect("params should parse");

        let history = params
            .resource_params
            .get("resource_history_bytes")
            .expect("history params");
        assert_eq!(
            history.price_curve_params.coeff_a,
            10_525_659_774_662_010_880
        );
        assert_eq!(
            history.resource_dynamics_params.max_pool_size,
            54_101_078_501
        );
        assert_eq!(
            params.size_info.resource_execution_time["transaction_time"],
            6622
        );
        assert_eq!(
            params.size_info.resource_state_bytes["transaction_base_size"],
            128
        );
    }

    #[test]
    fn rc_pool_and_stats_parse() {
        let pool: RCPool = serde_json::from_value(json!({
            "resource_pool": {
                "resource_execution_time": {
                    "pool": 66199826375_i64,
                    "fill_level": "9558"
                }
            }
        }))
        .expect("pool should parse");
        assert_eq!(
            pool.resource_pool["resource_execution_time"].fill_level,
            9558
        );

        let stats: RcStats = serde_json::from_value(json!({
            "regen": "2298172681338",
            "share": [5028, "10000", 436, 2467, 2068]
        }))
        .expect("stats should parse");
        assert_eq!(stats.regen, 2_298_172_681_338);
        assert_eq!(stats.share[1], 10_000);
    }
}