Repository: gakonst/ethers-rs
Pull request #989 (2022-03-05 01:16:53)

fix(abigen): safe ident field names
<!--
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

I was generating bindings for a contract that imports [dYdX's I_Solo.sol](https://github.com/dydxprotocol/perpetual/blob/master/contracts/external/dydx/I_Solo.sol) and got an error because `AssetAmount` has a field that is called `ref`.
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

`ref` is a reserved word, so I use `util::safe_ident` instead of `util::ident` for field names.

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [ ] Added Documentation
- [x] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbb59e47..1fa750c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,13 @@
 
 ## ethers-contract-abigen
 
+### Unreleased
+
+- Generate correct bindings of struct's field names that are reserved words
+  [#989](https://github.com/gakonst/ethers-rs/pull/989).
+
+### 0.6.0
+
 - Add `MultiAbigen` to generate a series of contract bindings that can be kept in the repo
   [#724](https://github.com/gakonst/ethers-rs/pull/724).
 - Add provided `event_derives` to call and event enums as well
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index d57c8ea5..bb08335f 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -107,7 +107,7 @@ impl Context {
             if is_tuple {
                 fields.push(ty);
             } else {
-                let field_name = util::ident(&field.name().to_snake_case());
+                let field_name = util::safe_ident(&field.name().to_snake_case());
                 fields.push(quote! { pub #field_name: #ty });
             }
         }
@@ -155,7 +155,7 @@ impl Context {
         let mut fields = Vec::with_capacity(sol_struct.fields().len());
         let mut param_types = Vec::with_capacity(sol_struct.fields().len());
         for field in sol_struct.fields() {
-            let field_name = util::ident(&field.name().to_snake_case());
+            let field_name = util::safe_ident(&field.name().to_snake_case());
             match field.r#type() {
                 FieldType::Elementary(ty) => {
                     param_types.push(ty.clone());
diff --git a/ethers-core/src/types/bytes.rs b/ethers-core/src/types/bytes.rs
index 9ec30263..21f2c719 100644
--- a/ethers-core/src/types/bytes.rs
+++ b/ethers-core/src/types/bytes.rs
@@ -2,8 +2,13 @@ use serde::{
     de::{Error, Unexpected},
     Deserialize, Deserializer, Serialize, Serializer,
 };
+use thiserror::Error;
 
-use std::fmt::{Display, Formatter, LowerHex, Result as FmtResult};
+use std::{
+    clone::Clone,
+    fmt::{Debug, Display, Formatter, LowerHex, Result as FmtResult},
+    str::FromStr,
+};
 
 /// Wrapper type around Bytes to deserialize/serialize "0x" prefixed ethereum hex strings
 #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
@@ -64,6 +69,24 @@ impl<'a, const N: usize> From<&'a [u8; N]> for Bytes {
     }
 }
 
+#[derive(Debug, Clone, Error)]
+#[error("Failed to parse bytes: {0}")]
+pub struct ParseBytesError(String);
+
+impl FromStr for Bytes {
+    type Err = ParseBytesError;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        if value.len() >= 2 && &value[0..2] == "0x" {
+            let bytes: Vec<u8> = hex::decode(&value[2..])
+                .map_err(|e| ParseBytesError(format!("Invalid hex: {}", e)))?;
+            Ok(bytes.into())
+        } else {
+            Err(ParseBytesError("Doesn't start with 0x".to_string()))
+        }
+    }
+}
+
 pub fn serialize_bytes<S, T>(x: T, s: S) -> Result<S::Ok, S::Error>
 where
     S: Serializer,
@@ -97,4 +120,16 @@ mod tests {
         assert_eq!(format!("{:x}", b), expected);
         assert_eq!(format!("{}", b), expected);
     }
+
+    #[test]
+    fn test_from_str() {
+        let b = Bytes::from_str("0x1213");
+        assert!(b.is_ok());
+        let b = b.unwrap();
+        assert_eq!(b.as_ref(), hex::decode("1213").unwrap());
+
+        let b = Bytes::from_str("1213");
+        assert!(b.is_err());
+        assert_eq!(b.err().unwrap().0, "Doesn't start with 0x");
+    }
 }


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index 4afcec6f..80f5616c 100644
--- a/ethers-contract/tests/abigen.rs
+++ b/ethers-contract/tests/abigen.rs
@@ -518,3 +518,15 @@ fn can_gen_multi_etherscan() {
     let _contract = MyContract::new(Address::default(), Arc::clone(&provider));
     let _contract = MyContract2::new(Address::default(), provider);
 }
+
+#[test]
+fn can_gen_reserved_word_field_names() {
+    abigen!(
+        Test,
+        r#"[
+        struct Foo { uint256 ref; }
+    ]"#,
+    );
+
+    let _foo = Foo { ref_: U256::default() };
+}