Repository: gakonst/ethers-rs (rust)

Issue:
Abigen fails for identifier named _100pct
**Version**

```
├── ethers v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   ├── ethers-addressbook v0.1.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   ├── ethers-contract v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   ├── ethers-contract-abigen v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-contract-derive v0.6.0 (proc-macro) (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   ├── ethers-contract-abigen v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-providers v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   ├── ethers-etherscan v0.2.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-solc v0.2.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   ├── ethers-middleware v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   ├── ethers-contract v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-etherscan v0.2.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-providers v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   │   ├── ethers-signers v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01)
│   │   │   ├── ethers-core v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   ├── ethers-providers v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   ├── ethers-signers v0.6.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)
│   └── ethers-solc v0.2.0 (https://github.com/gakonst/ethers-rs#faba6e01) (*)                                                                                                                                          
```

**Platform**

```
Darwin Meets-Mac-mini.local 21.3.0 Darwin Kernel Version 21.3.0: Wed Jan  5 21:37:58 PST 2022; root:xnu-8019.80.24~20/RELEASE_ARM64_T8101 arm64
```

**Description**

Abigen fails for identifier named `_100pct`. I am guessing this has to do with the particular case of `_` followed by a number. The transforming logic probably eliminates the `_` and starts with whatever is next, in this case, the digit which becomes an invalid identifier. `10_0pct_` was the ident generated for `_100pct`.

I tried this code:

```
{
  "inputs": [],
  "name": "_100pct",
  "outputs": [
    {
      "internalType": "uint256",
      "name": "",
      "type": "uint256"
    }
  ],
  "stateMutability": "view",
  "type": "function"
},
```

Pull request description:
fix(abigen): safe ident underscore followed by numeric
<!--
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
Closes #967
<!--
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
check if identifiers are valid and prefix with an underscore if they start with a numeric value


<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog


Patch:
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
index 212ebdb8..4d54aea2 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs
@@ -342,7 +342,7 @@ impl Context {
         let mut all_functions = HashMap::new();
         for function in self.abi.functions() {
             all_functions
-                .entry(function.name.to_lowercase())
+                .entry(util::safe_snake_case_ident(&function.name))
                 .or_insert_with(Vec::new)
                 .push(function);
         }
@@ -558,7 +558,7 @@ fn expand_function_name(function: &Function, alias: Option<&MethodAlias>) -> Ide
     if let Some(alias) = alias {
         alias.function_name.clone()
     } else {
-        util::safe_ident(&function.name.to_snake_case())
+        util::safe_ident(&util::safe_snake_case(&function.name))
     }
 }
 
@@ -567,7 +567,7 @@ fn expand_call_struct_name(function: &Function, alias: Option<&MethodAlias>) ->
     let name = if let Some(alias) = alias {
         format!("{}Call", alias.struct_name)
     } else {
-        format!("{}Call", function.name.to_pascal_case())
+        format!("{}Call", util::safe_pascal_case(&function.name))
     };
     util::ident(&name)
 }
@@ -577,7 +577,7 @@ fn expand_call_struct_variant_name(function: &Function, alias: Option<&MethodAli
     if let Some(alias) = alias {
         alias.struct_name.clone()
     } else {
-        util::safe_ident(&function.name.to_pascal_case())
+        util::safe_ident(&util::safe_pascal_case(&function.name))
     }
 }
 
diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs
index a3fc1819..346a4b94 100644
--- a/ethers-contract/ethers-contract-abigen/src/source.rs
+++ b/ethers-contract/ethers-contract-abigen/src/source.rs
@@ -279,6 +279,12 @@ fn get_etherscan_contract(address: Address, domain: &str) -> Result<String> {
     if abi.starts_with("Contract source code not verified") {
         eyre::bail!("Contract source code not verified: {:?}", address);
     }
+    if abi.starts_with('{') && abi.contains("Max rate limit reached") {
+        eyre::bail!(
+            "Max rate limit reached, please use etherscan API Key for higher rate limit: {:?}",
+            address
+        );
+    }
 
     Ok(abi)
 }
diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs
index b13b5772..d9c812e0 100644
--- a/ethers-contract/ethers-contract-abigen/src/util.rs
+++ b/ethers-contract/ethers-contract-abigen/src/util.rs
@@ -21,6 +21,25 @@ pub fn safe_ident(name: &str) -> Ident {
     syn::parse_str::<SynIdent>(name).unwrap_or_else(|_| ident(&format!("{}_", name)))
 }
 
+///  Converts a `&str` to `snake_case` `String` while respecting identifier rules
+pub fn safe_snake_case(ident: &str) -> String {
+    safe_identifier_name(ident.to_snake_case())
+}
+
+///  Converts a `&str` to `PascalCase` `String` while respecting identifier rules
+pub fn safe_pascal_case(ident: &str) -> String {
+    safe_identifier_name(ident.to_pascal_case())
+}
+
+/// respects identifier rules, such as, an identifier must not start with a numeric char
+fn safe_identifier_name(name: String) -> String {
+    if name.starts_with(|c: char| c.is_numeric()) {
+        format!("_{}", name)
+    } else {
+        name
+    }
+}
+
 /// Expands an identifier as snakecase and preserve any leading or trailing underscores
 pub fn safe_snake_case_ident(name: &str) -> Ident {
     let i = name.to_snake_case();


Test patch:
diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs
index 1aa57525..4afcec6f 100644
--- a/ethers-contract/tests/abigen.rs
+++ b/ethers-contract/tests/abigen.rs
@@ -374,6 +374,23 @@ fn can_handle_unique_underscore_functions() {
     let _contract_call = ConsoleLogCalls::Log2(call);
 }
 
+#[test]
+fn can_handle_underscore_numeric() {
+    abigen!(
+        Test,
+        r#"[
+            _100pct(string)
+        ]"#
+    );
+    let call = _100PctCall("message".to_string());
+
+    let provider = Arc::new(Provider::new(MockProvider::new()));
+    let contract = Test::new(Address::default(), Arc::clone(&provider));
+    // NOTE: this seems to be weird behaviour of `Inflector::to_snake_case` which turns "100pct" ->
+    // "10_0pct"
+    let _call = contract._10_0pct("hello".to_string());
+}
+
 #[test]
 fn can_handle_duplicates_with_same_name() {
     abigen!(
@@ -427,6 +444,23 @@ fn can_generate_nested_types() {
     assert_eq!(call, decoded_call);
 }
 
+#[test]
+fn can_handle_different_calls() {
+    abigen!(
+        Test,
+        r#"[
+        function fooBar()
+        function FOO_BAR()
+    ]"#,
+    );
+
+    let (client, _mock) = Provider::mocked();
+    let contract = Test::new(Address::default(), Arc::new(client));
+
+    let _ = contract.fooBar();
+    let _ = contract.FOO_BAR();
+}
+
 #[test]
 fn can_handle_case_sensitive_calls() {
     abigen!(