ethers_contract_abigen/source/
online.rs1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum Explorer {
12 #[default]
14 Etherscan,
15 Bscscan,
17 Polygonscan,
19 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 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 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 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 pub fn get(self, address: Address) -> Result<String> {
78 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" => Self::local(source),
96
97 "npm" => Ok(Self::npm(url.path())),
99
100 "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 scheme => Self::from_explorer(scheme, &url)
110 .or_else(|_| Self::local(source))
111 .wrap_err("Invalid path or URL"),
112 }
113 } else {
114 Self::local(source)
116 }
117 }
118
119 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 pub fn http(url: impl AsRef<str>) -> Result<Self> {
131 Ok(Self::Http(Url::parse(url.as_ref())?))
132 }
133
134 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 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 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 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}