From d3e73c43a96c35ee227a403efd451f05409f85f2 Mon Sep 17 00:00:00 2001 From: Marquis Shanahan <29431502+9547@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:41:10 +0800 Subject: [PATCH] feat(dyn-abi): support parse scientific number (#835) * feat: support parse scientific number Signed-off-by: 9547 <29431502+9547@users.noreply.github.com> * add test case Signed-off-by: 9547 <29431502+9547@users.noreply.github.com> * style Signed-off-by: 9547 <29431502+9547@users.noreply.github.com> * move error into inner Signed-off-by: 9547 <29431502+9547@users.noreply.github.com> * check sign first Signed-off-by: 9547 <29431502+9547@users.noreply.github.com> * chore: touchups --------- Signed-off-by: 9547 <29431502+9547@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- crates/dyn-abi/src/coerce.rs | 145 +++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 41 deletions(-) diff --git a/crates/dyn-abi/src/coerce.rs b/crates/dyn-abi/src/coerce.rs index 4f9bd781c..914cd6032 100644 --- a/crates/dyn-abi/src/coerce.rs +++ b/crates/dyn-abi/src/coerce.rs @@ -13,7 +13,7 @@ use winnow::{ ascii::{alpha0, alpha1, digit1, hex_digit0, hex_digit1, space0}, combinator::{cut_err, dispatch, empty, fail, opt, preceded, trace}, error::{ - AddContext, ContextError, ErrMode, ErrorKind, FromExternalError, StrContext, + AddContext, ContextError, ErrMode, ErrorKind, FromExternalError, ParserError, StrContext, StrContextValue, }, stream::Stream, @@ -250,6 +250,7 @@ impl<'a> ValueParser<'a> { enum Error { IntOverflow, FractionalNotAllowed(U256), + NegativeUnits, TooManyDecimals(usize, usize), InvalidFixedBytesLength(usize), FixedArrayLengthMismatch(usize, usize), @@ -265,18 +266,17 @@ impl fmt::Display for Error { Self::TooManyDecimals(expected, actual) => { write!(f, "expected at most {expected} decimals, got {actual}") } - Self::FractionalNotAllowed(n) => write!( - f, - "non-zero fraction 0.{n} not allowed without specifying non-wei units (gwei, ether, etc.)" - ), + Self::FractionalNotAllowed(n) => write!(f, "non-zero fraction .{n} not allowed"), + Self::NegativeUnits => f.write_str("negative units not allowed"), Self::InvalidFixedBytesLength(len) => { write!(f, "fixed bytes length {len} greater than 32") } - Self::FixedArrayLengthMismatch(expected, actual) => write!( - f, - "fixed array length mismatch: expected {expected} elements, got {actual}" - ), - Self::EmptyHexStringWithoutPrefix => f.write_str("expected hex digits or the `0x` prefix for an empty hex string"), + Self::FixedArrayLengthMismatch(expected, actual) => { + write!(f, "fixed array length mismatch: expected {expected} elements, got {actual}") + } + Self::EmptyHexStringWithoutPrefix => { + f.write_str("expected hex digits or the `0x` prefix for an empty hex string") + } } } } @@ -335,20 +335,34 @@ fn uint<'i>(len: usize) -> impl Parser, U256, ContextError> { #[cfg(not(feature = "debug"))] let name = "uint"; trace(name, move |input: &mut Input<'_>| { - let (s, (intpart, fract)) = spanned(( - prefixed_int, + let intpart = prefixed_int(input)?; + let fract = opt(preceded( '.', cut_err(digit1.context(StrContext::Expected(StrContextValue::Description( "at least one digit", )))), - )), - )) - .parse_next(input)?; + )) + .parse_next(input)?; + + let intpart = intpart + .parse::() + .map_err(|e| ErrMode::from_external_error(input, ErrorKind::Verify, e))?; + let e = opt(scientific_notation).parse_next(input)?.unwrap_or(0); let _ = space0(input)?; let units = int_units(input)?; + let units = units as isize + e; + if units < 0 { + return Err(ErrMode::from_external_error( + input, + ErrorKind::Verify, + Error::NegativeUnits, + )); + } + let units = units as usize; + let uint = if let Some(fract) = fract { let fract_uint = U256::from_str_radix(fract, 10) .map_err(|e| ErrMode::from_external_error(input, ErrorKind::Verify, e))?; @@ -370,21 +384,19 @@ fn uint<'i>(len: usize) -> impl Parser, U256, ContextError> { } // (intpart * 10^fract.len() + fract) * 10^(units-fract.len()) - U256::from_str_radix(intpart, 10) - .map_err(|e| ErrMode::from_external_error(input, ErrorKind::Verify, e))? + intpart .checked_mul(U256::from(10usize.pow(fract.len() as u32))) .and_then(|u| u.checked_add(fract_uint)) .and_then(|u| u.checked_mul(U256::from(10usize.pow((units - fract.len()) as u32)))) .ok_or_else(|| { ErrMode::from_external_error(input, ErrorKind::Verify, Error::IntOverflow) }) + } else if units > 0 { + intpart.checked_mul(U256::from(10usize.pow(units as u32))).ok_or_else(|| { + ErrMode::from_external_error(input, ErrorKind::Verify, Error::IntOverflow) + }) } else { - s.parse::() - .map_err(|e| ErrMode::from_external_error(input, ErrorKind::Verify, e))? - .checked_mul(U256::from(10usize.pow(units as u32))) - .ok_or_else(|| { - ErrMode::from_external_error(input, ErrorKind::Verify, Error::IntOverflow) - }) + Ok(intpart) }?; if uint.bit_len() > len { @@ -397,25 +409,30 @@ fn uint<'i>(len: usize) -> impl Parser, U256, ContextError> { #[inline] fn prefixed_int<'i>(input: &mut Input<'i>) -> PResult<&'i str> { - trace("prefixed_int", |input: &mut Input<'i>| { - let has_prefix = matches!(input.get(..2), Some("0b" | "0B" | "0o" | "0O" | "0x" | "0X")); - let checkpoint = input.checkpoint(); - if has_prefix { - let _ = input.next_slice(2); - // parse hex since it's the most general - hex_digit1(input) - } else { - digit1(input) - } - .map_err(|e| { - e.add_context( - input, - &checkpoint, - StrContext::Expected(StrContextValue::Description("at least one digit")), - ) - }) - }) + trace( + "prefixed_int", + spanned(|input: &mut Input<'i>| { + let has_prefix = + matches!(input.get(..2), Some("0b" | "0B" | "0o" | "0O" | "0x" | "0X")); + let checkpoint = input.checkpoint(); + if has_prefix { + let _ = input.next_slice(2); + // parse hex since it's the most general + hex_digit1(input) + } else { + digit1(input) + } + .map_err(|e| { + e.add_context( + input, + &checkpoint, + StrContext::Expected(StrContextValue::Description("at least one digit")), + ) + }) + }), + ) .parse_next(input) + .map(|(s, _)| s) } #[inline] @@ -432,6 +449,16 @@ fn int_units(input: &mut Input<'_>) -> PResult { .parse_next(input) } +#[inline] +fn scientific_notation(input: &mut Input<'_>) -> PResult { + // Check if we have 'e' or 'E' followed by an optional sign and digits + if !matches!(input.chars().next(), Some('e' | 'E')) { + return Err(ErrMode::from_error_kind(input, ErrorKind::Fail)); + }; + let _ = input.next_token(); + winnow::ascii::dec_int(input) +} + #[inline] fn fixed_bytes<'i>(len: usize) -> impl Parser, Word, ContextError> { #[cfg(feature = "debug")] @@ -1283,4 +1310,40 @@ mod tests { } assert_eq!(value, DynSolValue::Bool(true)); } + + #[test] + fn coerce_uint_scientific() { + assert_eq!( + DynSolType::Uint(256).coerce_str("1e18").unwrap(), + DynSolValue::Uint(U256::from_str("1000000000000000000").unwrap(), 256) + ); + + assert_eq!( + DynSolType::Uint(256).coerce_str("74258.225772486694040708e18").unwrap(), + DynSolValue::Uint(U256::from_str("74258225772486694040708").unwrap(), 256) + ); + + assert_eq!( + DynSolType::Uint(256).coerce_str("1.5e18").unwrap(), + DynSolValue::Uint(U256::from_str("1500000000000000000").unwrap(), 256) + ); + + assert_eq!( + DynSolType::Uint(256).coerce_str("1e-3 ether").unwrap(), + DynSolValue::Uint(U256::from_str("1000000000000000").unwrap(), 256) + ); + assert_eq!( + DynSolType::Uint(256).coerce_str("1.0e-3 ether").unwrap(), + DynSolValue::Uint(U256::from_str("1000000000000000").unwrap(), 256) + ); + assert_eq!( + DynSolType::Uint(256).coerce_str("1.1e-3 ether").unwrap(), + DynSolValue::Uint(U256::from_str("1100000000000000").unwrap(), 256) + ); + + assert!(DynSolType::Uint(256).coerce_str("1e-18").is_err()); + assert!(DynSolType::Uint(256).coerce_str("1 e18").is_err()); + assert!(DynSolType::Uint(256).coerce_str("1ex").is_err()); + assert!(DynSolType::Uint(256).coerce_str("1e").is_err()); + } }