rostrum 14.0.1

An efficient implementation of Electrum Server with token support
Documentation
use anyhow::{Context, Result};
use bitcoincash::hashes::hex::FromHex;
use bitcoincash::hashes::Hash;
use serde_json::Value;

use crate::chaindef::TokenID;
use crate::errors::rpc_invalid_params;

pub fn bool_from_value(val: Option<&Value>, name: &str) -> Result<bool> {
    let val = val.context(rpc_invalid_params(format!("missing {}", name)))?;
    let val = val
        .as_bool()
        .context(rpc_invalid_params(format!("not a bool {}", name)))?;
    Ok(val)
}

pub fn bool_from_value_or(val: Option<&Value>, name: &str, default: bool) -> Result<bool> {
    if val.is_none() {
        return Ok(default);
    }
    bool_from_value(val, name)
}

pub fn hash_from_value<T: Hash>(val: Option<&Value>) -> Result<T> {
    let hash = val.context(rpc_invalid_params("missing hash".to_string()))?;
    let hash = hash.as_str().context(rpc_invalid_params(
        "expected hash argument to be a string".to_string(),
    ))?;
    let hash = T::from_hex(hash).context(rpc_invalid_params(
        "expected hash argument to be a hex string".to_string(),
    ))?;
    Ok(hash)
}

pub fn str_from_value(val: Option<&Value>, name: &str) -> Result<String> {
    let string = val.context(rpc_invalid_params(format!("missing {}", name)))?;
    let string = string
        .as_str()
        .context(rpc_invalid_params(format!("{} is not a string", name)))?;
    Ok(string.into())
}

pub fn str_from_value_or_none(val: Option<&Value>, name: &str) -> Result<Option<String>> {
    let string = match val {
        Some(v) => v,
        None => return Ok(None),
    };
    let string = string
        .as_str()
        .context(rpc_invalid_params(format!("{} is not a string", name)))?;
    Ok(Some(string.into()))
}

pub fn hex_from_value(val: Option<&Value>, name: &str) -> Result<Vec<u8>> {
    let string = val.context(rpc_invalid_params(format!("missing {}", name)))?;
    let string = string
        .as_str()
        .context(rpc_invalid_params(format!("{} is not a string", name)))?;

    let hex = hex::decode(string).context(rpc_invalid_params(format!(
        "{} is not a valid hex string",
        name
    )))?;
    Ok(hex)
}

pub fn usize_from_value(val: Option<&Value>, name: &str) -> Result<usize> {
    let val = val.context(rpc_invalid_params(format!("missing {}", name)))?;
    let val = val
        .as_u64()
        .context(rpc_invalid_params(format!("non-integer {}", name)))?;
    Ok(val as usize)
}

pub fn usize_from_value_or(val: Option<&Value>, name: &str, default: usize) -> Result<usize> {
    if val.is_none() {
        return Ok(default);
    }
    usize_from_value(val, name)
}

pub fn tokenid_from_value(val: Option<&Value>) -> Result<Option<TokenID>> {
    if let Some(v) = val {
        if v.is_null() {
            return Ok(None);
        }

        let v = v.as_str().context("token_id is not a string")?;

        // Try to decode as hex
        if let Ok(id) = TokenID::from_hex(v) {
            return Ok(Some(id));
        };

        // Try to decode as cashaddr encoding
        #[cfg(nexa)]
        {
            if let Ok((payload, version_type, _)) = crate::nexa::cashaddr::decode(v) {
                if version_type != crate::nexa::cashaddr::version_byte_flags::TYPE_GROUP {
                    bail!(rpc_invalid_params(
                        "The `token_id` parameter had invalid cashaddr version flag (not a token)"
                            .to_string()
                    ));
                }
                return Ok(Some(TokenID::from_vec(payload)?));
            }
        }

        bail!(rpc_invalid_params(
            "The `token_id` parameter is not a valid identifier string".to_string()
        ));
    } else {
        Ok(None)
    }
}

pub fn cursor_from_value(val: Option<&Value>) -> Result<Option<String>> {
    match val {
        Some(inner_val) => {
            if inner_val.is_null() {
                Ok(None) // explicitly set to no cursor
            } else {
                // We don't support cursor yet, so if the user passed some other
                // value than null, we know it to be invalid.
                Err(rpc_invalid_params("Unknown 'cursor' provided".to_string()))
            }
        }
        None => Ok(None), // implicitly set to no cursor
    }
}

pub fn commitment_from_value(val: Option<&Value>) -> Result<Option<Vec<u8>>> {
    if let Some(v) = val {
        let v = v
            .as_str()
            .context(rpc_invalid_params("commitment is not a string".to_string()))?;

        // Try to decode as hex
        let decoded = hex::decode(v).context(rpc_invalid_params(
            "commitment is not a valid hex string".to_string(),
        ))?;

        if decoded.len() <= 40 {
            Ok(Some(decoded))
        } else {
            Err(rpc_invalid_params(
                "commitment is not a valid hex string or longer than 40 bytes".to_string(),
            ))
        }
    } else {
        Ok(None)
    }
}

/**
 * Nexa RPC uses little endian for block hash, so cannot use the
 * generic `hash_from_value` implementation.
 */
#[cfg(nexa)]
pub fn param_to_outpointhash(value: Option<&Value>) -> Result<crate::chaindef::OutPointHash> {
    use crate::chaindef::OutPointHash;
    let value = match value {
        Some(v) => v,
        None => {
            return Err(rpc_invalid_params(
                "missing argument outpointhash".to_string(),
            ))
        }
    };
    let mut hash = hex::decode(
        value
            .as_str()
            .context(format!("non-string outpointhash value: {}", value))?,
    )
    .context(format!("non-hex outpointhash value: {}", value))?;
    hash.reverse();
    OutPointHash::from_slice(&hash).context(format!("invalid blockhash {}", value))
}
pub fn parse_version_str(version: &str) -> Option<String> {
    version_compare::Version::from(version).map(|v| v.to_string())
}

#[cfg(test)]
mod tests {

    #[cfg(bch)]
    use bitcoin_hashes::hex::ToHex;

    use super::parse_version_str;
    use super::tokenid_from_value;

    #[test]
    fn test_tokenid_from_hex_value() {
        let hex_encoded = json!("27c21ff3552e283180422900c9671e60e6f6922691750aa1b2ed3b3347b00000");

        let hex_decoded = tokenid_from_value(Some(&hex_encoded)).unwrap().unwrap();
        assert_eq!(
            "27c21ff3552e283180422900c9671e60e6f6922691750aa1b2ed3b3347b00000",
            hex_decoded.to_hex()
        )
    }

    #[cfg(nexa)]
    #[test]
    fn test_tokenid_from_cashaddr_value() {
        let cashaddr_encoded =
            json!("nexareg:tqnuy8ln25hzsvvqgg5spjt8reswda5jy6gh2z4pktknkv68kqqqqvcwuh9hk");

        let cashaddr_decoded = tokenid_from_value(Some(&cashaddr_encoded))
            .unwrap()
            .unwrap();

        assert_eq!(
            "27c21ff3552e283180422900c9671e60e6f6922691750aa1b2ed3b3347b00000",
            cashaddr_decoded.to_hex()
        );
    }

    #[test]
    fn test_parse_version_str() {
        assert!(parse_version_str("1.4").is_some());
        assert!(parse_version_str("garbage").is_none());
    }
}