ethers_contract_abigen/source/
online.rs

1use super::Source;
2use crate::util;
3use ethers_core::types::{Address, Chain};
4use ethers_etherscan::Client;
5use eyre::{Context, Result};
6use std::{fmt, str::FromStr};
7use url::Url;
8
9/// An [etherscan](https://etherscan.io)-like blockchain explorer.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum Explorer {
12    /// <https://etherscan.io>
13    #[default]
14    Etherscan,
15    /// <https://bscscan.com>
16    Bscscan,
17    /// <https://polygonscan.com>
18    Polygonscan,
19    /// <https://snowtrace.io>
20    Snowtrace,
21}
22
23impl FromStr for Explorer {
24    type Err = eyre::Report;
25
26    fn from_str(s: &str) -> Result<Self> {
27        match s.to_lowercase().as_str() {
28            "etherscan" | "etherscan.io" => Ok(Self::Etherscan),
29            "bscscan" | "bscscan.com" => Ok(Self::Bscscan),
30            "polygonscan" | "polygonscan.com" => Ok(Self::Polygonscan),
31            "snowtrace" | "snowtrace.io" => Ok(Self::Snowtrace),
32            _ => Err(eyre::eyre!("Invalid or unsupported blockchain explorer: {s}")),
33        }
34    }
35}
36
37impl fmt::Display for Explorer {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        fmt::Debug::fmt(self, f)
40    }
41}
42
43impl Explorer {
44    /// Returns the chain's Explorer, if it is known.
45    pub fn from_chain(chain: Chain) -> Result<Self> {
46        match chain {
47            Chain::Mainnet => Ok(Self::Etherscan),
48            Chain::BinanceSmartChain => Ok(Self::Bscscan),
49            Chain::Polygon => Ok(Self::Polygonscan),
50            Chain::Avalanche => Ok(Self::Snowtrace),
51            _ => Err(eyre::eyre!("Provided chain has no known blockchain explorer")),
52        }
53    }
54
55    /// Returns the Explorer's chain. If it has multiple, the main one is returned.
56    pub const fn chain(&self) -> Chain {
57        match self {
58            Self::Etherscan => Chain::Mainnet,
59            Self::Bscscan => Chain::BinanceSmartChain,
60            Self::Polygonscan => Chain::Polygon,
61            Self::Snowtrace => Chain::Avalanche,
62        }
63    }
64
65    /// Creates an `ethers-etherscan` client using this Explorer's settings.
66    pub fn client(self, api_key: Option<String>) -> Result<Client> {
67        let chain = self.chain();
68        let client = match api_key {
69            Some(api_key) => Client::new(chain, api_key),
70            None => Client::new_from_opt_env(chain),
71        }?;
72        Ok(client)
73    }
74
75    /// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an artifact JSON for
76    /// compatibility with the code generation facilities.
77    pub fn get(self, address: Address) -> Result<String> {
78        // TODO: Improve this
79        let client = self.client(None)?;
80        let future = client.contract_abi(address);
81        let abi = match tokio::runtime::Handle::try_current() {
82            Ok(handle) => handle.block_on(future),
83            _ => tokio::runtime::Runtime::new().expect("Could not start runtime").block_on(future),
84        }?;
85        Ok(serde_json::to_string(&abi)?)
86    }
87}
88
89impl Source {
90    #[inline]
91    pub(super) fn parse_online(source: &str) -> Result<Self> {
92        if let Ok(url) = Url::parse(source) {
93            match url.scheme() {
94                // file://<path>
95                "file" => Self::local(source),
96
97                // npm:<npm package>
98                "npm" => Ok(Self::npm(url.path())),
99
100                // try first: <explorer url>/.../<address>
101                // then: any http url
102                "http" | "https" => Ok(url
103                    .host_str()
104                    .and_then(|host| Self::from_explorer(host, &url).ok())
105                    .unwrap_or(Self::Http(url))),
106
107                // custom scheme: <explorer or chain>:<address>
108                // fallback: local fs path
109                scheme => Self::from_explorer(scheme, &url)
110                    .or_else(|_| Self::local(source))
111                    .wrap_err("Invalid path or URL"),
112            }
113        } else {
114            // not a valid URL so fallback to path
115            Self::local(source)
116        }
117    }
118
119    /// Parse `s` as an explorer ("etherscan"), explorer domain ("etherscan.io") or a chain that has
120    /// an explorer ("mainnet").
121    ///
122    /// The URL can be either `<explorer>:<address>` or `<explorer_url>/.../<address>`
123    fn from_explorer(s: &str, url: &Url) -> Result<Self> {
124        let explorer: Explorer = s.parse().or_else(|_| Explorer::from_chain(s.parse()?))?;
125        let address = last_segment_address(url).ok_or_else(|| eyre::eyre!("Invalid URL: {url}"))?;
126        Ok(Self::Explorer(explorer, address))
127    }
128
129    /// Creates an HTTP source from a URL.
130    pub fn http(url: impl AsRef<str>) -> Result<Self> {
131        Ok(Self::Http(Url::parse(url.as_ref())?))
132    }
133
134    /// Creates an Etherscan source from an address string.
135    pub fn explorer(chain: Chain, address: Address) -> Result<Self> {
136        let explorer = Explorer::from_chain(chain)?;
137        Ok(Self::Explorer(explorer, address))
138    }
139
140    /// Creates an Etherscan source from an address string.
141    pub fn npm(package_path: impl Into<String>) -> Self {
142        Self::Npm(package_path.into())
143    }
144
145    #[inline]
146    pub(super) fn get_online(&self) -> Result<String> {
147        match self {
148            Self::Http(url) => {
149                util::http_get(url.clone()).wrap_err("Failed to retrieve ABI from URL")
150            }
151            Self::Explorer(explorer, address) => explorer.get(*address),
152            Self::Npm(package) => {
153                // TODO: const?
154                let unpkg = Url::parse("https://unpkg.io/").unwrap();
155                let url = unpkg.join(package).wrap_err("Invalid NPM package")?;
156                util::http_get(url).wrap_err("Failed to retrieve ABI from NPM package")
157            }
158            _ => unreachable!(),
159        }
160    }
161}
162
163fn last_segment_address(url: &Url) -> Option<Address> {
164    url.path().rsplit('/').next()?.parse().ok()
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn parse_online_source() {
173        assert_eq!(
174            Source::parse("https://my.domain.eth/path/to/Contract.json").unwrap(),
175            Source::http("https://my.domain.eth/path/to/Contract.json").unwrap()
176        );
177
178        assert_eq!(
179            Source::parse("npm:@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json").unwrap(),
180            Source::npm("@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json")
181        );
182
183        let explorers = &[
184            ("mainnet:", "etherscan:", "https://etherscan.io/address/", Chain::Mainnet),
185            ("bsc:", "bscscan:", "https://bscscan.com/address/", Chain::BinanceSmartChain),
186            ("polygon:", "polygonscan:", "https://polygonscan.com/address/", Chain::Polygon),
187            ("avalanche:", "snowtrace:", "https://snowtrace.io/address/", Chain::Avalanche),
188        ];
189
190        let address: Address = "0x0102030405060708091011121314151617181920".parse().unwrap();
191        for &(chain_s, scan_s, url_s, chain) in explorers {
192            let expected = Source::explorer(chain, address).unwrap();
193
194            let tests2 = [chain_s, scan_s, url_s].map(|s| s.to_string() + &format!("{address:?}"));
195            let tests2 = tests2.map(Source::parse).into_iter().chain(Some(Ok(expected.clone())));
196            let tests2 = tests2.collect::<Result<Vec<_>>>().unwrap();
197
198            for slice in tests2.windows(2) {
199                let [a, b] = slice else { unreachable!() };
200                if a != b {
201                    panic!("Expected: {expected:?}; Got: {a:?} | {b:?}");
202                }
203            }
204        }
205    }
206
207    #[test]
208    fn get_mainnet_contract() {
209        // Skip if ETHERSCAN_API_KEY is not set
210        if std::env::var("ETHERSCAN_API_KEY").is_err() {
211            return
212        }
213
214        let source = Source::parse("mainnet:0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
215        let abi = source.get().unwrap();
216        assert!(!abi.is_empty());
217    }
218}