use alloc::string::ToString;
use bigdecimal::BigDecimal;
use core::str::FromStr;
use crate::models::{Amount, XRPLModelException, XRPLModelResult};
pub(crate) const VAULT_ID_HEX_LEN: usize = 64;
pub(crate) fn validate_hash256(field: &'static str, value: &str) -> XRPLModelResult<()> {
if value.len() != VAULT_ID_HEX_LEN {
return Err(XRPLModelException::InvalidValueFormat {
field: field.to_string(),
format: "64 hex characters (256-bit hash)".to_string(),
found: value.to_string(),
});
}
if !value.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(XRPLModelException::InvalidValueFormat {
field: field.to_string(),
format: "ASCII hexadecimal".to_string(),
found: value.to_string(),
});
}
Ok(())
}
pub(crate) fn validate_vault_id(vault_id: &str) -> XRPLModelResult<()> {
if vault_id.len() != VAULT_ID_HEX_LEN {
return Err(XRPLModelException::InvalidValueFormat {
field: "vault_id".to_string(),
format: "64 hex characters (256-bit hash)".to_string(),
found: vault_id.to_string(),
});
}
validate_hash256("vault_id", vault_id)?;
if vault_id.bytes().all(|b| b == b'0') {
return Err(XRPLModelException::InvalidValue {
field: "vault_id".to_string(),
expected: "nonzero 256-bit hash".to_string(),
found: vault_id.to_string(),
});
}
Ok(())
}
pub(crate) fn validate_hex_blob(
field: &'static str,
value: &str,
max_hex_chars: usize,
) -> XRPLModelResult<()> {
if value.is_empty() || !value.len().is_multiple_of(2) {
return Err(XRPLModelException::InvalidValueFormat {
field: field.to_string(),
format: "non-empty even-length ASCII hexadecimal".to_string(),
found: value.to_string(),
});
}
if !value.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(XRPLModelException::InvalidValueFormat {
field: field.to_string(),
format: "ASCII hexadecimal".to_string(),
found: value.to_string(),
});
}
if value.len() > max_hex_chars {
return Err(XRPLModelException::ValueTooLong {
field: field.to_string(),
max: max_hex_chars,
found: value.len(),
});
}
Ok(())
}
pub(crate) fn validate_nonnegative_number(field: &'static str, value: &str) -> XRPLModelResult<()> {
let parsed =
BigDecimal::from_str(value).map_err(|_| XRPLModelException::InvalidValueFormat {
field: field.to_string(),
format: "a valid decimal number".to_string(),
found: value.to_string(),
})?;
if parsed < 0 {
return Err(XRPLModelException::InvalidValue {
field: field.to_string(),
expected: "a nonnegative number".to_string(),
found: value.to_string(),
});
}
Ok(())
}
pub(crate) fn validate_positive_amount(
field: &'static str,
amount: &Amount<'_>,
) -> XRPLModelResult<()> {
let value = match amount {
Amount::MPTAmount(amount) => amount.value.as_ref(),
Amount::IssuedCurrencyAmount(amount) => amount.value.as_ref(),
Amount::XRPAmount(amount) => amount.0.as_ref(),
};
let parsed =
BigDecimal::from_str(value).map_err(|_| XRPLModelException::InvalidValueFormat {
field: field.to_string(),
format: "a valid decimal number".to_string(),
found: value.to_string(),
})?;
if parsed <= 0 {
return Err(XRPLModelException::InvalidValue {
field: field.to_string(),
expected: "a positive amount".to_string(),
found: value.to_string(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_vault_id_accepts_valid_id() {
let id = "A0000000000000000000000000000000000000000000000000000000DEADBEEF";
assert!(validate_vault_id(id).is_ok());
}
#[test]
fn test_validate_vault_id_rejects_wrong_length() {
assert!(validate_vault_id("DEADBEEF").is_err());
let too_long = "A".repeat(65);
assert!(validate_vault_id(&too_long).is_err());
}
#[test]
fn test_validate_vault_id_rejects_non_hex() {
let id = "Z0000000000000000000000000000000000000000000000000000000DEADBEEF";
assert!(validate_vault_id(id).is_err());
}
#[test]
fn test_validate_hex_blob_accepts_valid() {
assert!(validate_hex_blob("data", "48656C6C6F", 512).is_ok());
}
#[test]
fn test_validate_hex_blob_rejects_too_long() {
let long = "A".repeat(513);
assert!(validate_hex_blob("data", &long, 512).is_err());
}
#[test]
fn test_validate_hex_blob_rejects_non_hex() {
assert!(validate_hex_blob("data", "XYZ", 512).is_err());
}
#[test]
fn test_validate_hex_blob_rejects_empty_or_odd_length() {
assert!(validate_hex_blob("data", "", 512).is_err());
assert!(validate_hex_blob("data", "ABC", 512).is_err());
}
#[test]
fn test_validate_nonnegative_number_accepts_valid() {
assert!(validate_nonnegative_number("f", "0").is_ok());
assert!(validate_nonnegative_number("f", "1000000000").is_ok());
assert!(validate_nonnegative_number("f", "100.5").is_ok());
assert!(validate_nonnegative_number("f", "1e6").is_ok());
}
#[test]
fn test_validate_nonnegative_number_rejects_empty_negative_and_nonnumeric() {
assert!(validate_nonnegative_number("f", "").is_err());
assert!(validate_nonnegative_number("f", "abc").is_err());
assert!(validate_nonnegative_number("f", "-5").is_err());
}
}