use crate::error::{Error, Result};
use sha2::{Digest, Sha256};
use sha3::Keccak256;
pub fn sha256(data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
pub fn keccak256(data: &[u8]) -> Vec<u8> {
Keccak256::digest(data).to_vec()
}
pub fn ripemd160(data: &[u8]) -> Vec<u8> {
use ripemd::Ripemd160;
Ripemd160::digest(data).to_vec()
}
pub fn hash160(data: &[u8]) -> Vec<u8> {
ripemd160(&sha256(data))
}
pub fn parse_units(amount: &str, decimals: u32) -> Result<String> {
let body = amount.trim();
if let Some(stripped) = body.strip_prefix('-') {
let _ = stripped;
return Err(Error::Denom(format!(
"negative amounts are not supported: {amount}"
)));
}
let body = body.strip_prefix('+').unwrap_or(body);
if body.is_empty() {
return Err(Error::Denom("empty amount".into()));
}
let (int_part, frac_part) = match body.split_once('.') {
Some((i, f)) => (i, f),
None => (body, ""),
};
let int_part = if int_part.is_empty() { "0" } else { int_part };
if !is_digits(int_part) || (!frac_part.is_empty() && !is_digits(frac_part)) {
return Err(Error::Denom(format!("invalid decimal amount: {amount}")));
}
let decimals = decimals as usize;
if frac_part.len() > decimals {
return Err(Error::Denom(format!(
"too many decimal places in {amount}: {} > decimals {decimals}",
frac_part.len()
)));
}
let mut combined = String::with_capacity(int_part.len() + decimals);
combined.push_str(int_part);
combined.push_str(frac_part);
for _ in 0..(decimals - frac_part.len()) {
combined.push('0');
}
let trimmed = combined.trim_start_matches('0');
Ok(if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
})
}
pub fn format_units(base: &str, decimals: u32) -> Result<String> {
let trimmed = base.trim();
if trimmed.starts_with('-') {
return Err(Error::Denom(format!(
"negative amounts are not supported: {base}"
)));
}
if !is_digits(trimmed) {
return Err(Error::Denom(format!("invalid base amount: {base}")));
}
let decimals = decimals as usize;
let digits_owned = {
let t = trimmed.trim_start_matches('0');
if t.is_empty() {
"0".to_string()
} else {
t.to_string()
}
};
if decimals == 0 {
return Ok(digits_owned);
}
let digits = if digits_owned.len() <= decimals {
let pad = decimals + 1 - digits_owned.len();
format!("{}{}", "0".repeat(pad), digits_owned)
} else {
digits_owned
};
let split = digits.len() - decimals;
let int_part = &digits[..split];
let frac_part = &digits[split..];
let trimmed_frac = frac_part.trim_end_matches('0');
if trimmed_frac.is_empty() {
Ok(int_part.to_string())
} else {
Ok(format!("{int_part}.{trimmed_frac}"))
}
}
pub fn is_valid_evm_address(s: &str) -> bool {
if s.len() != 42 {
return false;
}
let bytes = s.as_bytes();
if bytes[0] != b'0' || (bytes[1] != b'x' && bytes[1] != b'X') {
return false;
}
s[2..].bytes().all(|b| b.is_ascii_hexdigit())
}
pub fn is_valid_svm_address(s: &str) -> bool {
match bs58::decode(s).into_vec() {
Ok(decoded) => decoded.len() == 32,
Err(_) => false,
}
}
pub fn to_checksum_address(s: &str) -> Result<String> {
if !is_valid_evm_address(s) {
return Err(Error::Address(format!("invalid EVM address: {s}")));
}
let lower = s[2..].to_ascii_lowercase();
let hash = keccak256(lower.as_bytes());
let mut out = String::with_capacity(42);
out.push_str("0x");
for (i, c) in lower.bytes().enumerate() {
if c.is_ascii_digit() {
out.push(c as char);
continue;
}
let mut nibble = hash[i / 2];
if i % 2 == 0 {
nibble >>= 4;
}
nibble &= 0x0f;
if nibble >= 8 {
out.push((c as char).to_ascii_uppercase());
} else {
out.push(c as char);
}
}
Ok(out)
}
fn is_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}