ethers_contract_abigen/source/
mod.rs

1//! Parse ABI artifacts from different sources.
2
3// TODO: Support `online` for WASM
4
5#[cfg(all(feature = "online", not(target_arch = "wasm32")))]
6mod online;
7#[cfg(all(feature = "online", not(target_arch = "wasm32")))]
8pub use online::Explorer;
9
10use crate::util;
11use eyre::{Error, Result};
12use std::{env, fs, path::PathBuf, str::FromStr};
13
14/// A source of an Ethereum smart contract's ABI.
15///
16/// See [`parse`][#method.parse] for more information.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum Source {
19    /// A raw ABI string.
20    String(String),
21
22    /// An ABI located on the local file system.
23    Local(PathBuf),
24
25    /// An address of a smart contract address verified at a supported blockchain explorer.
26    #[cfg(all(feature = "online", not(target_arch = "wasm32")))]
27    Explorer(Explorer, ethers_core::types::Address),
28
29    /// The package identifier of an npm package with a path to a Truffle artifact or ABI to be
30    /// retrieved from `unpkg.io`.
31    #[cfg(all(feature = "online", not(target_arch = "wasm32")))]
32    Npm(String),
33
34    /// An ABI to be retrieved over HTTP(S).
35    #[cfg(all(feature = "online", not(target_arch = "wasm32")))]
36    Http(url::Url),
37}
38
39impl Default for Source {
40    fn default() -> Self {
41        Self::String("[]".to_string())
42    }
43}
44
45impl FromStr for Source {
46    type Err = Error;
47
48    fn from_str(s: &str) -> Result<Self> {
49        Source::parse(s)
50    }
51}
52
53impl Source {
54    /// Parses an ABI from a source.
55    ///
56    /// This method accepts the following:
57    ///
58    /// - `{ ... }` or `[ ... ]`: A raw or human-readable ABI object or array of objects.
59    ///
60    /// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file. This relative path
61    ///   is rooted in the current working directory.
62    ///
63    /// - `/absolute/path/to/Contract.json` or `file:///absolute/path/to/Contract.json`: an absolute
64    ///   path or file URL to an ABI JSON file.
65    ///
66    /// If the `online` feature is enabled:
67    ///
68    /// - `npm:@org/package@1.0.0/path/to/contract.json`: A npmjs package with an optional version
69    ///   and path (defaulting to the latest version and `index.js`), retrieved through `unpkg.io`.
70    ///
71    /// - `http://...`: an HTTP URL to a contract ABI. <br> Note: either the `rustls` or `openssl`
72    ///   feature must be enabled to support *HTTPS* URLs.
73    ///
74    /// - `<name>:<address>`, `<chain>:<address>` or `<url>/.../<address>`: an address or URL of a
75    ///   verified contract on a blockchain explorer. <br> Supported explorers and their respective
76    ///   chain:
77    ///   - `etherscan`   -> `mainnet`
78    ///   - `bscscan`     -> `bsc`
79    ///   - `polygonscan` -> `polygon`
80    ///   - `snowtrace`   -> `avalanche`
81    pub fn parse(source: impl AsRef<str>) -> Result<Self> {
82        let source = source.as_ref().trim();
83        match source.chars().next() {
84            Some('[' | '{') => Ok(Self::String(source.to_string())),
85
86            #[cfg(any(not(feature = "online"), target_arch = "wasm32"))]
87            _ => Ok(Self::local(source)?),
88
89            #[cfg(all(feature = "online", not(target_arch = "wasm32")))]
90            Some('/') => Self::local(source),
91            #[cfg(all(feature = "online", not(target_arch = "wasm32")))]
92            _ => Self::parse_online(source),
93        }
94    }
95
96    /// Creates a local filesystem source from a path string.
97    pub fn local(path: impl AsRef<str>) -> Result<Self> {
98        // resolve env vars
99        let path = path.as_ref().trim_start_matches("file://");
100        let mut resolved = util::resolve_path(path)?;
101
102        if resolved.is_relative() {
103            // set root at manifest dir, if the path exists
104            if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
105                let new = PathBuf::from(manifest_dir).join(&resolved);
106                if new.exists() {
107                    resolved = new;
108                }
109            }
110        }
111
112        // canonicalize
113        if let Ok(canonicalized) = dunce::canonicalize(&resolved) {
114            resolved = canonicalized;
115        } else {
116            let path = resolved.display().to_string();
117            let err = if path.contains(':') {
118                eyre::eyre!("File does not exist: {path}\nYou may need to enable the `online` feature to parse this source.")
119            } else {
120                eyre::eyre!("File does not exist: {path}")
121            };
122            return Err(err)
123        }
124
125        Ok(Source::Local(resolved))
126    }
127
128    /// Returns `true` if `self` is `String`.
129    pub fn is_string(&self) -> bool {
130        matches!(self, Self::String(_))
131    }
132
133    /// Returns `self` as `String`.
134    pub fn as_string(&self) -> Option<&String> {
135        match self {
136            Self::String(s) => Some(s),
137            _ => None,
138        }
139    }
140
141    /// Returns `true` if `self` is `Local`.
142    pub fn is_local(&self) -> bool {
143        matches!(self, Self::Local(_))
144    }
145
146    /// Returns `self` as `Local`.
147    pub fn as_local(&self) -> Option<&PathBuf> {
148        match self {
149            Self::Local(p) => Some(p),
150            _ => None,
151        }
152    }
153
154    /// Retrieves the source JSON of the artifact this will either read the JSON from the file
155    /// system or retrieve a contract ABI from the network depending on the source type.
156    pub fn get(&self) -> Result<String> {
157        match self {
158            Self::Local(path) => Ok(fs::read_to_string(path)?),
159            Self::String(abi) => Ok(abi.clone()),
160
161            #[cfg(all(feature = "online", not(target_arch = "wasm32")))]
162            _ => self.get_online(),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::path::Path;
171
172    #[test]
173    fn parse_source() {
174        let rel = "../tests/solidity-contracts/console.json";
175        let abs = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/solidity-contracts/console.json");
176        let abs_url = concat!(
177            "file://",
178            env!("CARGO_MANIFEST_DIR"),
179            "/../tests/solidity-contracts/console.json"
180        );
181        let exp = Source::Local(dunce::canonicalize(Path::new(rel)).unwrap());
182        assert_eq!(Source::parse(rel).unwrap(), exp);
183        assert_eq!(Source::parse(abs).unwrap(), exp);
184        assert_eq!(Source::parse(abs_url).unwrap(), exp);
185
186        // ABI
187        let source = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#;
188        let parsed = Source::parse(source).unwrap();
189        assert_eq!(parsed, Source::String(source.to_owned()));
190
191        // Hardhat-like artifact
192        let source = format!(
193            r#"{{"_format": "hh-sol-artifact-1", "contractName": "Verifier", "sourceName": "contracts/verifier.sol", "abi": {source}, "bytecode": "0x", "deployedBytecode": "0x", "linkReferences": {{}}, "deployedLinkReferences": {{}}}}"#,
194        );
195        let parsed = Source::parse(&source).unwrap();
196        assert_eq!(parsed, Source::String(source));
197    }
198}