use std::fmt;
use crate::client::evm::abi::{AbiError, LONGEST, WORD};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Kind {
Uint(u16),
Int(u16),
Address,
Bool,
String,
Bytes,
Fixed(usize),
Array(Box<Kind>),
Sized(Box<Kind>, usize),
Tuple(Vec<Param>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Option<String>,
pub kind: Kind,
pub indexed: bool,
}
const DEEPEST: usize = 32;
const MODIFIERS: [&str; 4] = ["memory", "calldata", "storage", "payable"];
impl Kind {
pub fn parse(text: &str) -> Result<Kind, AbiError> {
kind_of(text, 0).ok_or_else(|| AbiError::Type(text.trim().to_owned()))
}
#[must_use]
pub fn is_dynamic(&self) -> bool {
match self {
Kind::String | Kind::Bytes | Kind::Array(_) => true,
Kind::Sized(inner, _) => inner.is_dynamic(),
Kind::Tuple(params) => params.iter().any(|param| param.kind.is_dynamic()),
Kind::Uint(_) | Kind::Int(_) | Kind::Address | Kind::Bool | Kind::Fixed(_) => false,
}
}
pub(super) fn head(&self) -> usize {
if self.is_dynamic() {
return WORD;
}
match self {
Kind::Sized(inner, count) => inner.head().saturating_mul(*count),
Kind::Tuple(params) => params.iter().map(|param| param.kind.head()).sum(),
_ => WORD,
}
}
#[must_use]
pub fn canonical(&self) -> String {
match self {
Kind::Uint(bits) => format!("uint{bits}"),
Kind::Int(bits) => format!("int{bits}"),
Kind::Address => "address".to_owned(),
Kind::Bool => "bool".to_owned(),
Kind::String => "string".to_owned(),
Kind::Bytes => "bytes".to_owned(),
Kind::Fixed(size) => format!("bytes{size}"),
Kind::Array(inner) => format!("{}[]", inner.canonical()),
Kind::Sized(inner, count) => format!("{}[{count}]", inner.canonical()),
Kind::Tuple(params) => tuple_type(params),
}
}
pub(super) fn wants(&self) -> String {
match self {
Kind::Uint(bits) => format!("a whole number that fits uint{bits}"),
Kind::Int(bits) => format!("a whole number that fits int{bits}"),
Kind::Address => "a 20-byte address".to_owned(),
Kind::Bool => "true or false".to_owned(),
Kind::String => "text".to_owned(),
Kind::Bytes => "hex bytes".to_owned(),
Kind::Fixed(size) => format!("{size} hex bytes"),
Kind::Array(_) | Kind::Sized(..) => "a list in brackets".to_owned(),
Kind::Tuple(_) => "a tuple in parentheses".to_owned(),
}
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.canonical())
}
}
#[must_use]
pub fn tuple_type(params: &[Param]) -> String {
let kinds: Vec<String> = params.iter().map(|param| param.kind.canonical()).collect();
format!("({})", kinds.join(","))
}
pub fn params(text: &str) -> Result<Vec<Param>, AbiError> {
params_at(text, 0).ok_or_else(|| AbiError::Type(text.trim().to_owned()))
}
fn params_at(text: &str, depth: usize) -> Option<Vec<Param>> {
let text = text.trim();
if text.is_empty() {
return Some(Vec::new());
}
split(text).into_iter().map(|one| param(one.trim(), depth)).collect()
}
fn param(text: &str, depth: usize) -> Option<Param> {
let end = type_end(text);
let kind = kind_of(&text[..end], depth)?;
let mut name = None;
let mut indexed = false;
for word in text[end..].split_whitespace() {
match word {
"indexed" => indexed = true,
_ if MODIFIERS.contains(&word) => {}
_ if name.is_none() && is_name(word) => name = Some(word.to_owned()),
_ => return None,
}
}
Some(Param { name, kind, indexed })
}
pub(super) fn is_name(word: &str) -> bool {
word.chars().all(|letter| letter.is_ascii_alphanumeric() || letter == '_' || letter == '$')
}
fn type_end(text: &str) -> usize {
let mut depth = 0i32;
for (at, letter) in text.char_indices() {
match letter {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
_ if letter.is_whitespace() && depth == 0 => return at,
_ => {}
}
}
text.len()
}
fn depth_at(text: &str, at: usize) -> i32 {
text[..at].chars().fold(0, |depth, letter| match letter {
'(' | '[' => depth + 1,
')' | ']' => depth - 1,
_ => depth,
})
}
fn kind_of(text: &str, depth: usize) -> Option<Kind> {
if depth > DEEPEST {
return None;
}
let text = text.trim();
if let Some(open) = text.rfind('[')
&& text.ends_with(']')
&& depth_at(text, open) == 0
{
let inner = kind_of(&text[..open], depth + 1)?;
let count = &text[open + 1..text.len() - 1];
return Some(match count.is_empty() {
true => Kind::Array(Box::new(inner)),
false => Kind::Sized(Box::new(inner), count.parse().ok().filter(|count| *count <= LONGEST)?),
});
}
if let Some(inside) = text.strip_prefix('(').and_then(|rest| rest.strip_suffix(')')) {
return params_at(inside, depth + 1).map(Kind::Tuple);
}
let bits = |digits: &str| -> Option<u16> {
match digits.is_empty() {
true => Some(256),
false => digits.parse().ok().filter(|bits| *bits > 0 && *bits <= 256 && bits % 8 == 0),
}
};
Some(match text {
"address" => Kind::Address,
"bool" => Kind::Bool,
"string" => Kind::String,
"bytes" => Kind::Bytes,
_ if text.starts_with("uint") => Kind::Uint(bits(&text[4..])?),
_ if text.starts_with("int") => Kind::Int(bits(&text[3..])?),
_ if text.starts_with("bytes") => {
let size: usize = text[5..].parse().ok()?;
(1..=32).contains(&size).then_some(Kind::Fixed(size))?
}
_ => return None,
})
}
pub(super) fn split(text: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut start = 0;
for (at, letter) in text.char_indices() {
match letter {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
',' if depth == 0 => {
parts.push(&text[start..at]);
start = at + 1;
}
_ => {}
}
}
parts.push(&text[start..]);
parts
}
#[cfg(test)]
#[path = "tests/kind.rs"]
mod tests;