Repository: gakonst/ethers-rs
Pull request #1864 (2022-11-16 03:59:58)

Rust already exists on the platform - update it instead
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
Windows build failures on main line.

## Solution
Rust seems to already be installed on the platform, therefore rustup just exits. Moved to rustup update instead.

## PR Checklist

-   [-] Added Tests
-   [-] Added Documentation
-   [-] Updated the changelog


dbelv: No sre why the tests are stuck, assuming this happens from time to time?
mattsse: > No sure why the tests are stuck, assuming this happens from time to time?

flaky test against testnet, unrelated.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 91d0133d..14281f96 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -284,13 +284,11 @@ jobs:
               uses: actions/checkout@v3
 
               # Run build
-            - name: Install Rustup using win.rustup.rs
+            - name: Update Rustup
               run: |
                   # disable download progress bar
                   $ProgressPreference = "SilentlyContinue"
-                  Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
-                  .\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --default-toolchain=none
-                  del rustup-init.exe
+                  rustup update
                   rustup target add ${{ matrix.target }}
               shell: powershell
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9676bad..992be8ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,9 +94,11 @@
 -   [#1632](https://github.com/gakonst/ethers-rs/pull/1632) Re-export `H32` from `ethabi`.
 -   [#1634](https://github.com/gakonst/ethers-rs/pull/1634) Derive missing `Clone`, `Copy` and `Debug` impls in ethers-etherscan.
 -   Bytes debug format now displays hex literals [#1658](https://github.com/gakonst/ethers-rs/pull/1658)
--   [#1451](https://github.com/gakonst/ethers-rs/issues/1451) Add Arithemtic Shift Left operation for I256
--   [#1860](https://github.com/gakonst/ethers-rs/pull/1860)Update I256 type documentation calling out the inconsistency 
+-   [#1451](https://github.com/gakonst/ethers-rs/issues/1451) Add Arithmetic Shift Left operation for I256
+-   [#1860](https://github.com/gakonst/ethers-rs/pull/1860) Update I256 type documentation calling out the inconsistency 
     between its right shift operator and standard library numeric types.
+-   [#842](https://github.com/gakonst/ethers-rs/issues/842) Add support for I256 types in `parse_units` and `format_units`. 
+    Added `twos_complement` function for I256.
 
 ## ethers-contract-abigen
 
diff --git a/ethers-core/src/types/i256.rs b/ethers-core/src/types/i256.rs
index b3bc4ae5..28660e13 100644
--- a/ethers-core/src/types/i256.rs
+++ b/ethers-core/src/types/i256.rs
@@ -4,6 +4,7 @@
 use crate::{
     abi::{InvalidOutputType, Token, Tokenizable},
     types::U256,
+    utils::ParseUnits,
 };
 use ethabi::ethereum_types::FromDecStrErr;
 use serde::{Deserialize, Serialize};
@@ -977,13 +978,21 @@ impl I256 {
         } else {
             let result = self << shift;
             if result.sign() != self.sign() {
-                // Overflow occured
+                // Overflow occurred
                 None
             } else {
                 Some(result)
             }
         }
     }
+
+    /// Compute the twos complement of the I256
+    pub fn twos_complement(self) -> U256 {
+        match self.sign() {
+            Sign::Positive => self.into_raw(),
+            Sign::Negative => twos_complement(self.into_raw()),
+        }
+    }
 }
 
 macro_rules! impl_from {
@@ -1042,6 +1051,15 @@ impl TryFrom<I256> for U256 {
     }
 }
 
+impl From<ParseUnits> for I256 {
+    fn from(n: ParseUnits) -> Self {
+        match n {
+            ParseUnits::U256(n) => Self::from_raw(n),
+            ParseUnits::I256(n) => n,
+        }
+    }
+}
+
 impl str::FromStr for I256 {
     type Err = ParseI256Error;
 
@@ -1828,4 +1846,37 @@ mod tests {
         assert_eq!(I256::from_token(42i32.into_token()).unwrap(), I256::from(42),);
         assert_eq!(I256::from_token(U256::MAX.into_token()).unwrap(), I256::minus_one(),);
     }
+
+    #[test]
+    fn twos_complement() {
+        macro_rules! assert_twos_complement {
+            ($signed:ty, $unsigned:ty) => {
+                assert_eq!(
+                    I256::from(<$signed>::MAX).twos_complement(),
+                    U256::from(<$signed>::MAX)
+                );
+                assert_eq!(
+                    I256::from(<$signed>::MIN).twos_complement(),
+                    U256::from(<$signed>::MIN.unsigned_abs())
+                );
+                assert_eq!(I256::from(0 as $signed).twos_complement(), U256::from(0 as $signed));
+
+                assert_eq!(
+                    I256::from(<$unsigned>::MAX).twos_complement(),
+                    U256::from(<$unsigned>::MAX)
+                );
+                assert_eq!(
+                    I256::from(0 as $unsigned).twos_complement(),
+                    U256::from(0 as $unsigned)
+                );
+            };
+        }
+
+        assert_twos_complement!(i8, u8);
+        assert_twos_complement!(i16, u16);
+        assert_twos_complement!(i32, u32);
+        assert_twos_complement!(i64, u64);
+        assert_twos_complement!(i128, u128);
+        assert_twos_complement!(isize, usize);
+    }
 }
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index 335f8540..99eeadb5 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -30,7 +30,7 @@ mod uint8;
 pub use uint8::*;
 
 mod i256;
-pub use i256::{Sign, I256};
+pub use i256::{ParseI256Error, Sign, I256};
 
 mod bytes;
 pub use self::bytes::{deserialize_bytes, serialize_bytes, Bytes, ParseBytesError};
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index e4c347af..32aa1f84 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -31,7 +31,7 @@ pub use rlp;
 /// Re-export hex
 pub use hex;
 
-use crate::types::{Address, Bytes, I256, U256};
+use crate::types::{Address, Bytes, ParseI256Error, I256, U256};
 use elliptic_curve::sec1::ToEncodedPoint;
 use ethabi::ethereum_types::FromDecStrErr;
 use k256::{ecdsa::SigningKey, PublicKey as K256PublicKey};
@@ -58,6 +58,8 @@ pub enum ConversionError {
     FromDecStrError(#[from] FromDecStrErr),
     #[error("Overflow parsing string")]
     ParseOverflow,
+    #[error(transparent)]
+    ParseI256Error(#[from] ParseI256Error),
 }
 
 /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei
@@ -76,6 +78,41 @@ pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000;
 /// under it.
 pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200;
 
+/// This enum holds the numeric types that a possible to be returned by `parse_units` and
+/// that are taken by `format_units`.
+#[derive(Copy, Clone)]
+pub enum ParseUnits {
+    U256(U256),
+    I256(I256),
+}
+
+impl From<ParseUnits> for U256 {
+    fn from(n: ParseUnits) -> Self {
+        match n {
+            ParseUnits::U256(n) => n,
+            ParseUnits::I256(n) => n.into_raw(),
+        }
+    }
+}
+
+macro_rules! construct_format_units_from {
+    ($( $t:ty[$convert:ident] ),*) => {
+        $(
+            impl From<$t> for ParseUnits {
+                fn from(num: $t) -> Self {
+                    Self::$convert(num.into())
+                }
+            }
+        )*
+    }
+}
+
+// Generate the From<T> code for the given numeric types below.
+construct_format_units_from! {
+    u8[U256], u16[U256], u32[U256], u64[U256], u128[U256], U256[U256], usize[U256],
+    i8[I256], i16[I256], i32[I256], i64[I256], i128[I256], I256[I256], isize[I256]
+}
+
 /// Format the output for the user which prefer to see values
 /// in ether (instead of wei)
 ///
@@ -97,22 +134,43 @@ pub fn format_ether<T: Into<U256>>(amount: T) -> U256 {
 ///
 /// let eth = format_units(U256::from_dec_str("1395633240123456789").unwrap(), "ether").unwrap();
 /// assert_eq!(eth, "1.395633240123456789");
+///
+/// let eth = format_units(i64::MIN, "gwei").unwrap();
+/// assert_eq!(eth, "-9223372036.854775808");
+///
+/// let eth = format_units(i128::MIN, 36).unwrap();
+/// assert_eq!(eth, "-170.141183460469231731687303715884105728");
 /// ```
 pub fn format_units<T, K>(amount: T, units: K) -> Result<String, ConversionError>
 where
-    T: Into<U256>,
+    T: Into<ParseUnits>,
     K: TryInto<Units, Error = ConversionError>,
 {
     let units = units.try_into()?;
-    let amount = amount.into();
-    let amount_decimals = amount % U256::from(10_u128.pow(units.as_num()));
-    let amount_integer = amount / U256::from(10_u128.pow(units.as_num()));
-    Ok(format!(
-        "{}.{:0width$}",
-        amount_integer,
-        amount_decimals.as_u128(),
-        width = units.as_num() as usize
-    ))
+    match amount.into() {
+        ParseUnits::U256(amount) => {
+            let amount_decimals = amount % U256::from(10_u128.pow(units.as_num()));
+            let amount_integer = amount / U256::from(10_u128.pow(units.as_num()));
+            Ok(format!(
+                "{}.{:0width$}",
+                amount_integer,
+                amount_decimals.as_u128(),
+                width = units.as_num() as usize
+            ))
+        }
+        ParseUnits::I256(amount) => {
+            let sign = if amount.is_negative() { "-" } else { "" };
+            let amount_decimals = amount % I256::from(10_u128.pow(units.as_num()));
+            let amount_integer = amount / I256::from(10_u128.pow(units.as_num()));
+            Ok(format!(
+                "{}{}.{:0width$}",
+                sign,
+                amount_integer.twos_complement(),
+                amount_decimals.twos_complement().as_u128(),
+                width = units.as_num() as usize
+            ))
+        }
+    }
 }
 
 /// Converts the input to a U256 and converts from Ether to Wei.
@@ -129,7 +187,7 @@ pub fn parse_ether<S>(eth: S) -> Result<U256, ConversionError>
 where
     S: ToString,
 {
-    parse_units(eth, "ether")
+    Ok(parse_units(eth, "ether")?.into())
 }
 
 /// Multiplies the provided amount with 10^{units} provided.
@@ -139,24 +197,25 @@ where
 /// let amount_in_eth = U256::from_dec_str("15230001000000000000").unwrap();
 /// let amount_in_gwei = U256::from_dec_str("15230001000").unwrap();
 /// let amount_in_wei = U256::from_dec_str("15230001000").unwrap();
-/// assert_eq!(amount_in_eth, parse_units("15.230001000000000000", "ether").unwrap());
-/// assert_eq!(amount_in_gwei, parse_units("15.230001000000000000", "gwei").unwrap());
-/// assert_eq!(amount_in_wei, parse_units("15230001000", "wei").unwrap());
+/// assert_eq!(amount_in_eth, parse_units("15.230001000000000000", "ether").unwrap().into());
+/// assert_eq!(amount_in_gwei, parse_units("15.230001000000000000", "gwei").unwrap().into());
+/// assert_eq!(amount_in_wei, parse_units("15230001000", "wei").unwrap().into());
 /// ```
 /// Example of trying to parse decimal WEI, which should fail, as WEI is the smallest
 /// ETH denominator. 1 ETH = 10^18 WEI.
 /// ```should_panic
 /// use ethers_core::{types::U256, utils::parse_units};
 /// let amount_in_wei = U256::from_dec_str("15230001000").unwrap();
-/// assert_eq!(amount_in_wei, parse_units("15.230001000000000000", "wei").unwrap());
+/// assert_eq!(amount_in_wei, parse_units("15.230001000000000000", "wei").unwrap().into());
 /// ```
-pub fn parse_units<K, S>(amount: S, units: K) -> Result<U256, ConversionError>
+pub fn parse_units<K, S>(amount: S, units: K) -> Result<ParseUnits, ConversionError>
 where
     S: ToString,
     K: TryInto<Units, Error = ConversionError> + Copy,
 {
     let exponent: u32 = units.try_into()?.as_num();
     let mut amount_str = amount.to_string().replace('_', "");
+    let negative = amount_str.chars().next().unwrap_or_default() == '-';
     let dec_len = if let Some(di) = amount_str.find('.') {
         amount_str.remove(di);
         amount_str[di..].len() as u32
@@ -167,16 +226,37 @@ where
     if dec_len > exponent {
         // Truncate the decimal part if it is longer than the exponent
         let amount_str = &amount_str[..(amount_str.len() - (dec_len - exponent) as usize)];
-        let a_uint = U256::from_dec_str(amount_str)?;
-        Ok(a_uint)
+        if negative {
+            // Edge case: We have removed the entire number and only the negative sign is left.
+            //            Return 0 as a I256 given the input was signed.
+            if amount_str == "-" {
+                Ok(ParseUnits::I256(I256::zero()))
+            } else {
+                Ok(ParseUnits::I256(I256::from_dec_str(amount_str)?))
+            }
+        } else {
+            Ok(ParseUnits::U256(U256::from_dec_str(amount_str)?))
+        }
+    } else if negative {
+        // Edge case: Only a negative sign was given, return 0 as a I256 given the input was signed.
+        if amount_str == "-" {
+            Ok(ParseUnits::I256(I256::zero()))
+        } else {
+            let mut n = I256::from_dec_str(&amount_str)?;
+            n *= I256::from(10)
+                .checked_pow(exponent - dec_len)
+                .ok_or(ConversionError::ParseOverflow)?;
+            Ok(ParseUnits::I256(n))
+        }
     } else {
         let mut a_uint = U256::from_dec_str(&amount_str)?;
         a_uint *= U256::from(10)
             .checked_pow(U256::from(exponent - dec_len))
             .ok_or(ConversionError::ParseOverflow)?;
-        Ok(a_uint)
+        Ok(ParseUnits::U256(a_uint))
     }
 }
+
 /// The address for an Ethereum contract is deterministically computed from the
 /// address of its creator (sender) and how many transactions the creator has
 /// sent (nonce). The sender and nonce are RLP encoded and then hashed with Keccak-256.
@@ -432,7 +512,7 @@ mod tests {
     }
 
     #[test]
-    fn test_format_units() {
+    fn test_format_units_unsigned() {
         let gwei_in_ether = format_units(WEI_IN_ETHER, 9).unwrap();
         assert_eq!(gwei_in_ether.parse::<f64>().unwrap() as u64, 1e9 as u64);
 
@@ -453,51 +533,173 @@ mod tests {
         let eth =
             format_units(U256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap();
         assert_eq!(eth, "1.005633240123456789");
+
+        let eth = format_units(255u8, 4).unwrap();
+        assert_eq!(eth, "0.0255");
+
+        let eth = format_units(u16::MAX, "ether").unwrap();
+        assert_eq!(eth, "0.000000000000065535");
+
+        // Note: This covers usize on 32 bit systems.
+        let eth = format_units(u32::MAX, 18).unwrap();
+        assert_eq!(eth, "0.000000004294967295");
+
+        // Note: This covers usize on 64 bit systems.
+        let eth = format_units(u64::MAX, "gwei").unwrap();
+        assert_eq!(eth, "18446744073.709551615");
+
+        let eth = format_units(u128::MAX, 36).unwrap();
+        assert_eq!(eth, "340.282366920938463463374607431768211455");
+    }
+
+    #[test]
+    fn test_format_units_signed() {
+        let eth =
+            format_units(I256::from_dec_str("-1395633240123456000").unwrap(), "ether").unwrap();
+        assert_eq!(eth.parse::<f64>().unwrap(), -1.395633240123456);
+
+        let eth =
+            format_units(I256::from_dec_str("-1395633240123456789").unwrap(), "ether").unwrap();
+        assert_eq!(eth, "-1.395633240123456789");
+
+        let eth =
+            format_units(I256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap();
+        assert_eq!(eth, "1.005633240123456789");
+
+        let eth = format_units(i8::MIN, 4).unwrap();
+        assert_eq!(eth, "-0.0128");
+        assert_eq!(eth.parse::<f64>().unwrap(), -0.0128_f64);
+
+        let eth = format_units(i8::MAX, 4).unwrap();
+        assert_eq!(eth, "0.0127");
+        assert_eq!(eth.parse::<f64>().unwrap(), 0.0127);
+
+        let eth = format_units(i16::MIN, "ether").unwrap();
+        assert_eq!(eth, "-0.000000000000032768");
+
+        // Note: This covers isize on 32 bit systems.
+        let eth = format_units(i32::MIN, 18).unwrap();
+        assert_eq!(eth, "-0.000000002147483648");
+
+        // Note: This covers isize on 64 bit systems.
+        let eth = format_units(i64::MIN, "gwei").unwrap();
+        assert_eq!(eth, "-9223372036.854775808");
+
+        let eth = format_units(i128::MIN, 36).unwrap();
+        assert_eq!(eth, "-170.141183460469231731687303715884105728");
     }
 
     #[test]
     fn parse_large_units() {
         let decimals = 27u32;
         let val = "10.55";
-        let unit = parse_units(val, decimals).unwrap();
-        assert_eq!(unit.to_string(), "10550000000000000000000000000");
+
+        let n: U256 = parse_units(val, decimals).unwrap().into();
+        assert_eq!(n.to_string(), "10550000000000000000000000000");
     }
 
     #[test]
     fn test_parse_units() {
-        let gwei = parse_units(1.5, 9).unwrap();
+        let gwei: U256 = parse_units(1.5, 9).unwrap().into();
         assert_eq!(gwei.as_u64(), 15e8 as u64);
 
-        let token = parse_units(1163.56926418, 8).unwrap();
+        let token: U256 = parse_units(1163.56926418, 8).unwrap().into();
         assert_eq!(token.as_u64(), 116356926418);
 
-        let eth_dec_float = parse_units(1.39563324, "ether").unwrap();
+        let eth_dec_float: U256 = parse_units(1.39563324, "ether").unwrap().into();
         assert_eq!(eth_dec_float, U256::from_dec_str("1395633240000000000").unwrap());
 
-        let eth_dec_string = parse_units("1.39563324", "ether").unwrap();
+        let eth_dec_string: U256 = parse_units("1.39563324", "ether").unwrap().into();
         assert_eq!(eth_dec_string, U256::from_dec_str("1395633240000000000").unwrap());
 
-        let eth = parse_units(1, "ether").unwrap();
+        let eth: U256 = parse_units(1, "ether").unwrap().into();
         assert_eq!(eth, WEI_IN_ETHER);
 
-        let val = parse_units("2.3", "ether").unwrap();
+        let val: U256 = parse_units("2.3", "ether").unwrap().into();
         assert_eq!(val, U256::from_dec_str("2300000000000000000").unwrap());
 
-        assert_eq!(parse_units(".2", 2).unwrap(), U256::from(20), "leading dot");
-        assert_eq!(parse_units("333.21", 2).unwrap(), U256::from(33321), "trailing dot");
-        assert_eq!(
-            parse_units("98766", 16).unwrap(),
-            U256::from_dec_str("987660000000000000000").unwrap(),
-            "no dot"
-        );
-        assert_eq!(parse_units("3_3_0", 3).unwrap(), U256::from(330000), "underscore");
-        assert_eq!(parse_units("330", 0).unwrap(), U256::from(330), "zero decimals");
-        assert_eq!(parse_units(".1234", 3).unwrap(), U256::from(123), "truncate too many decimals");
+        let n: U256 = parse_units(".2", 2).unwrap().into();
+        assert_eq!(n, U256::from(20), "leading dot");
+
+        let n: U256 = parse_units("333.21", 2).unwrap().into();
+        assert_eq!(n, U256::from(33321), "trailing dot");
+
+        let n: U256 = parse_units("98766", 16).unwrap().into();
+        assert_eq!(n, U256::from_dec_str("987660000000000000000").unwrap(), "no dot");
+
+        let n: U256 = parse_units("3_3_0", 3).unwrap().into();
+        assert_eq!(n, U256::from(330000), "underscore");
+
+        let n: U256 = parse_units("330", 0).unwrap().into();
+        assert_eq!(n, U256::from(330), "zero decimals");
+
+        let n: U256 = parse_units(".1234", 3).unwrap().into();
+        assert_eq!(n, U256::from(123), "truncate too many decimals");
+
         assert!(parse_units("1", 80).is_err(), "overflow");
         assert!(parse_units("1", -1).is_err(), "neg units");
+
         let two_e30 = U256::from(2) * U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0]);
-        assert_eq!(parse_units("2", 30).unwrap(), two_e30, "2e30");
-        assert_eq!(parse_units(".33_319_2", 0).unwrap(), U256::zero(), "mix");
+        let n: U256 = parse_units("2", 30).unwrap().into();
+        assert_eq!(n, two_e30, "2e30");
+
+        let n: U256 = parse_units(".33_319_2", 0).unwrap().into();
+        assert_eq!(n, U256::zero(), "mix");
+
+        let n: U256 = parse_units("", 3).unwrap().into();
+        assert_eq!(n, U256::zero(), "empty");
+    }
+
+    #[test]
+    fn test_signed_parse_units() {
+        let gwei: I256 = parse_units(-1.5, 9).unwrap().into();
+        assert_eq!(gwei.as_i64(), -15e8 as i64);
+
+        let token: I256 = parse_units(-1163.56926418, 8).unwrap().into();
+        assert_eq!(token.as_i64(), -116356926418);
+
+        let eth_dec_float: I256 = parse_units(-1.39563324, "ether").unwrap().into();
+        assert_eq!(eth_dec_float, I256::from_dec_str("-1395633240000000000").unwrap());
+
+        let eth_dec_string: I256 = parse_units("-1.39563324", "ether").unwrap().into();
+        assert_eq!(eth_dec_string, I256::from_dec_str("-1395633240000000000").unwrap());
+
+        let eth: I256 = parse_units(-1, "ether").unwrap().into();
+        assert_eq!(eth, I256::from_raw(WEI_IN_ETHER) * I256::minus_one());
+
+        let val: I256 = parse_units("-2.3", "ether").unwrap().into();
+        assert_eq!(val, I256::from_dec_str("-2300000000000000000").unwrap());
+
+        let n: I256 = parse_units("-.2", 2).unwrap().into();
+        assert_eq!(n, I256::from(-20), "leading dot");
+
+        let n: I256 = parse_units("-333.21", 2).unwrap().into();
+        assert_eq!(n, I256::from(-33321), "trailing dot");
+
+        let n: I256 = parse_units("-98766", 16).unwrap().into();
+        assert_eq!(n, I256::from_dec_str("-987660000000000000000").unwrap(), "no dot");
+
+        let n: I256 = parse_units("-3_3_0", 3).unwrap().into();
+        assert_eq!(n, I256::from(-330000), "underscore");
+
+        let n: I256 = parse_units("-330", 0).unwrap().into();
+        assert_eq!(n, I256::from(-330), "zero decimals");
+
+        let n: I256 = parse_units("-.1234", 3).unwrap().into();
+        assert_eq!(n, I256::from(-123), "truncate too many decimals");
+
+        assert!(parse_units("-1", 80).is_err(), "overflow");
+
+        let two_e30 =
+            I256::from(-2) * I256::from_raw(U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0]));
+        let n: I256 = parse_units("-2", 30).unwrap().into();
+        assert_eq!(n, two_e30, "-2e30");
+
+        let n: I256 = parse_units("-.33_319_2", 0).unwrap().into();
+        assert_eq!(n, I256::zero(), "mix");
+
+        let n: I256 = parse_units("-", 3).unwrap().into();
+        assert_eq!(n, I256::zero(), "empty");
     }
 
     #[test]


Test patch:
diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs
index efa35ad8..a7c16a8c 100644
--- a/ethers-middleware/tests/signer.rs
+++ b/ethers-middleware/tests/signer.rs
@@ -97,6 +97,7 @@ async fn pending_txs_with_confirmations_testnet() {
 
 #[cfg(not(feature = "celo"))]
 use ethers_core::types::{Address, Eip1559TransactionRequest};
+use ethers_core::utils::parse_ether;
 
 // different keys to avoid nonce errors
 #[tokio::test]
@@ -317,7 +318,7 @@ impl TestWallets {
                 .nonce(nonce)
                 .to(addr)
                 // 0.1 eth per wallet
-                .value(parse_units("1", 18).unwrap());
+                .value(parse_ether("1").unwrap());
             pending_txs.push(
                 provider.send_transaction(tx, Some(BlockNumber::Pending.into())).await.unwrap(),
             );