diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..00a3de7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: test ${{ matrix.rust }} ${{ matrix.flags }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + rust: ["stable", "beta", "nightly", "1.65"] # MSRV + flags: ["--no-default-features", "", "--all-features"] + exclude: + # Skip because some features have highest MSRV. + - rust: "1.65" # MSRV + flags: "--all-features" + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + # Only run tests on latest stable and above + - name: build + if: ${{ matrix.rust == '1.65' }} # MSRV + run: cargo build --workspace ${{ matrix.flags }} + - name: test + if: ${{ matrix.rust != '1.65' }} # MSRV + run: cargo test --workspace ${{ matrix.flags }} + + miri: + name: miri ${{ matrix.flags }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + flags: ["--no-default-features", "", "--all-features"] + env: + MIRIFLAGS: -Zmiri-strict-provenance + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@miri + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo miri setup ${{ matrix.flags }} + - run: cargo miri test ${{ matrix.flags }} + + wasm: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - name: check + run: cargo check --workspace --target wasm32-unknown-unknown + + feature-checks: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@cargo-hack + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - name: cargo hack + run: cargo hack check --feature-powerset --depth 2 + + clippy: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@clippy + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo clippy --workspace --all-targets --all-features + env: + RUSTFLAGS: -Dwarnings + + docs: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo doc --workspace --all-features --no-deps --document-private-items + env: + RUSTDOCFLAGS: "--cfg docsrs -D warnings" + + fmt: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - run: cargo fmt --all --check diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml new file mode 100644 index 0000000..0b6448d --- /dev/null +++ b/.github/workflows/deps.yml @@ -0,0 +1,18 @@ +name: deps + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: [cron: "00 00 * * *"] + +jobs: + cargo-deny: + name: cargo deny check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check all diff --git a/.gitignore b/.gitignore index ea8c4bf..975b01a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +/Cargo.lock +.vscode +.idea diff --git a/Cargo.toml b/Cargo.toml index bf66244..06dc5c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,23 @@ [package] -name = "chains" +name = "alloy-chains" +description = "Canonical type definitions for EIP-155 chains" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +rust-version = "1.65" +keywords = ["ethers", "primitives", "ethereum", "revm", "reth"] +categories = ["no-std", "data-structures", "cryptography::cryptocurrencies"] +homepage = "https://github.com/alloy-rs/chains" +repository = "https://github.com/alloy-rs/chains" [dependencies] +num_enum = { version = "0.7", default-features = false } +serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } +strum = { version = "0.25", default-features = false, features = ["derive"] } + +[dev-dependencies] +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } + +[features] +default = ["std"] +std = ["strum/std", "serde?/std"] +serde = ["dep:serde"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..532322f --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# alloy-chains + +Canonical type definitions for EIP-155 chains. + +## Supported Rust Versions + + + +Alloy will keep a rolling MSRV (minimum supported rust version) policy of **at +least** 6 months. When increasing the MSRV, the new Rust version must have been +released at least six months ago. The current MSRV is 1.65.0. + +Note that the MSRV is not increased automatically, and only as part of a minor +release. + +## Note on `no_std` + +All crates in this workspace should support `no_std` environments, with the +`alloc` crate. If you find a crate that does not support `no_std`, please +[open an issue]. + +[open an issue]: https://github.com/alloy-rs/chains/issues/new/choose + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in these crates by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..0437112 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.65" diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..8d7bf3e --- /dev/null +++ b/deny.toml @@ -0,0 +1,54 @@ +[advisories] +vulnerability = "deny" +unmaintained = "warn" +unsound = "warn" +yanked = "warn" +notice = "warn" + +[bans] +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" + +[licenses] +unlicensed = "deny" +confidence-threshold = 0.9 +# copyleft = "deny" + +allow = [ + "MIT", + "MIT-0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "Unlicense", + "MPL-2.0", + # https://github.com/briansmith/ring/issues/902 + "LicenseRef-ring", + # https://github.com/briansmith/webpki/issues/148 + "LicenseRef-webpki", +] + +exceptions = [ + # CC0 is a permissive license but somewhat unclear status for source code + # so we prefer to not have dependencies using it + # https://tldrlegal.com/license/creative-commons-cc0-1.0-universal + { allow = ["CC0-1.0"], name = "tiny-keccak" }, +] + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +[[licenses.clarify]] +name = "webpki" +expression = "LicenseRef-webpki" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..20d1810 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,13 @@ +reorder_imports = true +use_field_init_shorthand = true +use_small_heuristics = "Max" +merge_derives = false + +# Nightly +max_width = 100 +comment_width = 100 +imports_granularity = "Crate" +wrap_comments = true +format_code_in_doc_comments = true +doc_comment_code_block_width = 100 +format_macro_matchers = true diff --git a/src/chain.rs b/src/chain.rs new file mode 100644 index 0000000..545f38e --- /dev/null +++ b/src/chain.rs @@ -0,0 +1,704 @@ +use core::{fmt, time::Duration}; +use num_enum::TryFromPrimitiveError; + +// When adding a new chain: +// 1. add new variant to the Chain enum; +// 2. add extra information in the last `impl` block (explorer URLs, block time) when applicable; +// 3. (optional) add aliases: +// - Strum (in kebab-case): `#[strum(to_string = "
", serialize = "", ...)]` +// `to_string = "
"` must be present and will be used in `Display`, `Serialize` +// and `FromStr`, while `serialize = ""` will be appended to `FromStr`. +// More info: +// - Serde (in snake_case): `#[cfg_attr(feature = "serde", serde(alias = "", ...))]` +// Aliases are appended to the `Deserialize` implementation. +// More info: +// - Add a test at the bottom of the file + +// We don't derive Serialize because it is manually implemented using AsRef and it would +// break a lot of things since Serialize is `kebab-case` vs Deserialize `snake_case`. +// This means that the Chain type is not "round-trippable", because the Serialize and Deserialize +// implementations do not use the same case style. + +/// An Ethereum EIP-155 chain. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(strum::AsRefStr)] // AsRef, fmt::Display and serde::Serialize +#[derive(strum::EnumVariantNames)] // Chain::VARIANTS +#[derive(strum::EnumString)] // FromStr, TryFrom<&str> +#[derive(strum::EnumIter)] // Chain::iter +#[derive(strum::EnumCount)] // Chain::COUNT +#[derive(num_enum::TryFromPrimitive)] // TryFrom +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[strum(serialize_all = "kebab-case")] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[repr(u64)] +#[allow(missing_docs)] +pub enum Chain { + #[strum(to_string = "mainnet", serialize = "ethlive")] + #[cfg_attr(feature = "serde", serde(alias = "ethlive"))] + Mainnet = 1, + Morden = 2, + Ropsten = 3, + Rinkeby = 4, + Goerli = 5, + Kovan = 42, + Holesky = 17000, + Sepolia = 11155111, + + Optimism = 10, + OptimismKovan = 69, + OptimismGoerli = 420, + OptimismSepolia = 11155420, + + Arbitrum = 42161, + ArbitrumTestnet = 421611, + ArbitrumGoerli = 421613, + ArbitrumSepolia = 421614, + ArbitrumNova = 42170, + + Cronos = 25, + CronosTestnet = 338, + + Rsk = 30, + + #[strum(to_string = "bsc", serialize = "binance-smart-chain")] + #[cfg_attr(feature = "serde", serde(alias = "bsc"))] + BinanceSmartChain = 56, + #[strum(to_string = "bsc-testnet", serialize = "binance-smart-chain-testnet")] + #[cfg_attr(feature = "serde", serde(alias = "bsc_testnet"))] + BinanceSmartChainTestnet = 97, + + Poa = 99, + Sokol = 77, + + Scroll = 534352, + ScrollAlphaTestnet = 534353, + + Metis = 1088, + + #[strum(to_string = "xdai", serialize = "gnosis", serialize = "gnosis-chain")] + #[cfg_attr(feature = "serde", serde(alias = "xdai", alias = "gnosis", alias = "gnosis_chain"))] + Gnosis = 100, + + Polygon = 137, + #[strum(to_string = "mumbai", serialize = "polygon-mumbai")] + #[cfg_attr(feature = "serde", serde(alias = "mumbai"))] + PolygonMumbai = 80001, + #[strum(serialize = "polygon-zkevm", serialize = "zkevm")] + #[cfg_attr(feature = "serde", serde(alias = "zkevm", alias = "polygon_zkevm"))] + PolygonZkEvm = 1101, + #[strum(serialize = "polygon-zkevm-testnet", serialize = "zkevm-testnet")] + #[cfg_attr(feature = "serde", serde(alias = "zkevm_testnet", alias = "polygon_zkevm_testnet"))] + PolygonZkEvmTestnet = 1442, + + Fantom = 250, + FantomTestnet = 4002, + + Moonbeam = 1284, + MoonbeamDev = 1281, + + Moonriver = 1285, + + Moonbase = 1287, + + Dev = 1337, + #[strum(to_string = "anvil-hardhat", serialize = "anvil", serialize = "hardhat")] + #[cfg_attr(feature = "serde", serde(alias = "anvil", alias = "hardhat"))] + AnvilHardhat = 31337, + + Evmos = 9001, + EvmosTestnet = 9000, + + Chiado = 10200, + + Oasis = 26863, + + Emerald = 42262, + EmeraldTestnet = 42261, + + FilecoinMainnet = 314, + FilecoinCalibrationTestnet = 314159, + + Avalanche = 43114, + #[strum(to_string = "fuji", serialize = "avalanche-fuji")] + #[cfg_attr(feature = "serde", serde(alias = "fuji"))] + AvalancheFuji = 43113, + + Celo = 42220, + CeloAlfajores = 44787, + CeloBaklava = 62320, + + Aurora = 1313161554, + AuroraTestnet = 1313161555, + + Canto = 7700, + CantoTestnet = 740, + + Boba = 288, + + Base = 8453, + BaseGoerli = 84531, + + Linea = 59144, + LineaTestnet = 59140, + + #[strum(to_string = "zksync")] + #[cfg_attr(feature = "serde", serde(alias = "zksync"))] + ZkSync = 324, + #[strum(to_string = "zksync-testnet")] + #[cfg_attr(feature = "serde", serde(alias = "zksync_testnet"))] + ZkSyncTestnet = 280, + + #[strum(to_string = "mantle")] + #[cfg_attr(feature = "serde", serde(alias = "mantle"))] + Mantle = 5000, + #[strum(to_string = "mantle-testnet")] + #[cfg_attr(feature = "serde", serde(alias = "mantle_testnet"))] + MantleTestnet = 5001, +} + +// This must be implemented manually so we avoid a conflict with `TryFromPrimitive` where it treats +// the `#[default]` attribute as its own `#[num_enum(default)]` +impl Default for Chain { + fn default() -> Self { + Self::Mainnet + } +} + +macro_rules! impl_try_from_numeric { + ($($native:ty)+) => { + $( + impl TryFrom<$native> for Chain { + type Error = TryFromPrimitiveError; + + fn try_from(value: $native) -> Result { + (value as u64).try_into() + } + } + )+ + }; +} + +impl From for u64 { + fn from(chain: Chain) -> Self { + chain as u64 + } +} + +impl_try_from_numeric!(u8 u16 u32 usize); + +impl fmt::Display for Chain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad(self.as_ref()) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Chain { + fn serialize(&self, s: S) -> Result { + s.serialize_str(self.as_ref()) + } +} + +// NB: all utility functions *should* be explicitly exhaustive (not use `_` matcher) so we don't +// forget to update them when adding a new `Chain` variant. +#[allow(clippy::match_like_matches_macro)] +#[deny(unreachable_patterns, unused_variables)] +impl Chain { + /// Returns the chain's average blocktime, if applicable. + /// + /// It can be beneficial to know the average blocktime to adjust the polling of an HTTP provider + /// for example. + /// + /// **Note:** this is not an accurate average, but is rather a sensible default derived from + /// blocktime charts such as [Etherscan's](https://etherscan.com/chart/blocktime) + /// or [Polygonscan's](https://polygonscan.com/chart/blocktime). + /// + /// # Examples + /// + /// ``` + /// use alloy_chains::Chain; + /// use std::time::Duration; + /// + /// assert_eq!(Chain::Mainnet.average_blocktime_hint(), Some(Duration::from_millis(12_000)),); + /// assert_eq!(Chain::Optimism.average_blocktime_hint(), Some(Duration::from_millis(2_000)),); + /// ``` + pub const fn average_blocktime_hint(&self) -> Option { + use Chain::*; + + let ms = match self { + Mainnet => 12_000, + Arbitrum | ArbitrumTestnet | ArbitrumGoerli | ArbitrumSepolia | ArbitrumNova => 1_300, + Optimism | OptimismGoerli | OptimismSepolia => 2_000, + Polygon | PolygonMumbai => 2_100, + Moonbeam | Moonriver => 12_500, + BinanceSmartChain | BinanceSmartChainTestnet => 3_000, + Avalanche | AvalancheFuji => 2_000, + Fantom | FantomTestnet => 1_200, + Cronos | CronosTestnet | Canto | CantoTestnet => 5_700, + Evmos | EvmosTestnet => 1_900, + Aurora | AuroraTestnet => 1_100, + Oasis => 5_500, + Emerald => 6_000, + Dev | AnvilHardhat => 200, + Celo | CeloAlfajores | CeloBaklava => 5_000, + FilecoinCalibrationTestnet | FilecoinMainnet => 30_000, + Scroll | ScrollAlphaTestnet => 3_000, + Gnosis | Chiado => 5_000, + // Explicitly exhaustive. See NB above. + Morden | Ropsten | Rinkeby | Goerli | Kovan | Sepolia | Holesky | Moonbase + | MoonbeamDev | OptimismKovan | Poa | Sokol | Rsk | EmeraldTestnet | Boba | Base + | BaseGoerli | ZkSync | ZkSyncTestnet | PolygonZkEvm | PolygonZkEvmTestnet | Metis + | Linea | LineaTestnet | Mantle | MantleTestnet => return None, + }; + + Some(Duration::from_millis(ms)) + } + + /// Returns whether the chain implements EIP-1559 (with the type 2 EIP-2718 transaction type). + /// + /// # Examples + /// + /// ``` + /// use alloy_chains::Chain; + /// + /// assert!(!Chain::Mainnet.is_legacy()); + /// assert!(Chain::Celo.is_legacy()); + /// ``` + pub const fn is_legacy(&self) -> bool { + use Chain::*; + + match self { + // Known legacy chains / non EIP-1559 compliant + OptimismKovan + | Fantom + | FantomTestnet + | BinanceSmartChain + | BinanceSmartChainTestnet + | ArbitrumTestnet + | Rsk + | Oasis + | Emerald + | EmeraldTestnet + | Celo + | CeloAlfajores + | CeloBaklava + | Boba + | ZkSync + | ZkSyncTestnet + | Mantle + | MantleTestnet + | PolygonZkEvm + | PolygonZkEvmTestnet + | Scroll => true, + + // Known EIP-1559 chains + Mainnet + | Goerli + | Sepolia + | Holesky + | Base + | BaseGoerli + | Optimism + | OptimismGoerli + | OptimismSepolia + | Polygon + | PolygonMumbai + | Avalanche + | AvalancheFuji + | Arbitrum + | ArbitrumGoerli + | ArbitrumSepolia + | ArbitrumNova + | FilecoinMainnet + | Linea + | LineaTestnet + | FilecoinCalibrationTestnet + | Gnosis + | Chiado => false, + + // Unknown / not applicable, default to false for backwards compatibility + Dev | AnvilHardhat | Morden | Ropsten | Rinkeby | Cronos | CronosTestnet | Kovan + | Sokol | Poa | Moonbeam | MoonbeamDev | Moonriver | Moonbase | Evmos + | EvmosTestnet | Aurora | AuroraTestnet | Canto | CantoTestnet | ScrollAlphaTestnet + | Metis => false, + } + } + + /// Returns whether the chain supports the `PUSH0` opcode or not. + /// + /// For more information, see EIP-3855: + /// `` + pub const fn supports_push0(&self) -> bool { + match self { + Chain::Mainnet | Chain::Goerli | Chain::Sepolia | Chain::Gnosis | Chain::Chiado => true, + _ => false, + } + } + + /// Returns the chain's blockchain explorer and its API (Etherscan and Etherscan-like) URLs. + /// + /// Returns `(API_URL, BASE_URL)` + /// + /// # Examples + /// + /// ``` + /// use alloy_chains::Chain; + /// + /// assert_eq!( + /// Chain::Mainnet.etherscan_urls(), + /// Some(("https://api.etherscan.io/api", "https://etherscan.io")) + /// ); + /// assert_eq!( + /// Chain::Avalanche.etherscan_urls(), + /// Some(("https://api.snowtrace.io/api", "https://snowtrace.io")) + /// ); + /// assert_eq!(Chain::AnvilHardhat.etherscan_urls(), None); + /// ``` + pub const fn etherscan_urls(&self) -> Option<(&'static str, &'static str)> { + use Chain::*; + + let urls = match self { + Mainnet => ("https://api.etherscan.io/api", "https://etherscan.io"), + Ropsten => ("https://api-ropsten.etherscan.io/api", "https://ropsten.etherscan.io"), + Kovan => ("https://api-kovan.etherscan.io/api", "https://kovan.etherscan.io"), + Rinkeby => ("https://api-rinkeby.etherscan.io/api", "https://rinkeby.etherscan.io"), + Goerli => ("https://api-goerli.etherscan.io/api", "https://goerli.etherscan.io"), + Sepolia => ("https://api-sepolia.etherscan.io/api", "https://sepolia.etherscan.io"), + Holesky => ("https://api-holesky.etherscan.io/api", "https://holesky.etherscan.io"), + + Polygon => ("https://api.polygonscan.com/api", "https://polygonscan.com"), + PolygonMumbai => { + ("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com") + } + + PolygonZkEvm => { + ("https://api-zkevm.polygonscan.com/api", "https://zkevm.polygonscan.com") + } + PolygonZkEvmTestnet => ( + "https://api-testnet-zkevm.polygonscan.com/api", + "https://testnet-zkevm.polygonscan.com", + ), + + Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"), + AvalancheFuji => { + ("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io") + } + + Optimism => { + ("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io") + } + OptimismGoerli => ( + "https://api-goerli-optimistic.etherscan.io/api", + "https://goerli-optimism.etherscan.io", + ), + OptimismKovan => ( + "https://api-kovan-optimistic.etherscan.io/api", + "https://kovan-optimistic.etherscan.io", + ), + OptimismSepolia => ( + "https://api-sepolia-optimistic.etherscan.io/api", + "https://sepolia-optimism.etherscan.io", + ), + + Fantom => ("https://api.ftmscan.com/api", "https://ftmscan.com"), + FantomTestnet => ("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com"), + + BinanceSmartChain => ("https://api.bscscan.com/api", "https://bscscan.com"), + BinanceSmartChainTestnet => { + ("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com") + } + + Arbitrum => ("https://api.arbiscan.io/api", "https://arbiscan.io"), + ArbitrumTestnet => { + ("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io") + } + ArbitrumGoerli => ("https://api-goerli.arbiscan.io/api", "https://goerli.arbiscan.io"), + ArbitrumSepolia => { + ("https://api-sepolia.arbiscan.io/api", "https://sepolia.arbiscan.io") + } + ArbitrumNova => ("https://api-nova.arbiscan.io/api", "https://nova.arbiscan.io/"), + + Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"), + CronosTestnet => { + ("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com") + } + + Moonbeam => ("https://api-moonbeam.moonscan.io/api", "https://moonbeam.moonscan.io/"), + Moonbase => ("https://api-moonbase.moonscan.io/api", "https://moonbase.moonscan.io/"), + Moonriver => ("https://api-moonriver.moonscan.io/api", "https://moonriver.moonscan.io"), + + Gnosis => ("https://api.gnosisscan.io/api", "https://gnosisscan.io"), + + Scroll => ("https://api.scrollscan.com", "https://scrollscan.com"), + ScrollAlphaTestnet => { + ("https://blockscout.scroll.io/api", "https://blockscout.scroll.io/") + } + + Metis => { + ("https://andromeda-explorer.metis.io/api", "https://andromeda-explorer.metis.io/") + } + + Chiado => { + ("https://blockscout.chiadochain.net/api", "https://blockscout.chiadochain.net") + } + + FilecoinCalibrationTestnet => ( + "https://api.calibration.node.glif.io/rpc/v1", + "https://calibration.filfox.info/en", + ), + + Sokol => ("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol"), + + Poa => ("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core"), + + Rsk => ("https://blockscout.com/rsk/mainnet/api", "https://blockscout.com/rsk/mainnet"), + + Oasis => ("https://scan.oasischain.io/api", "https://scan.oasischain.io/"), + + Emerald => { + ("https://explorer.emerald.oasis.dev/api", "https://explorer.emerald.oasis.dev/") + } + EmeraldTestnet => ( + "https://testnet.explorer.emerald.oasis.dev/api", + "https://testnet.explorer.emerald.oasis.dev/", + ), + + Aurora => ("https://api.aurorascan.dev/api", "https://aurorascan.dev"), + AuroraTestnet => { + ("https://testnet.aurorascan.dev/api", "https://testnet.aurorascan.dev") + } + + Evmos => ("https://evm.evmos.org/api", "https://evm.evmos.org/"), + EvmosTestnet => ("https://evm.evmos.dev/api", "https://evm.evmos.dev/"), + + Celo => ("https://explorer.celo.org/mainnet/api", "https://explorer.celo.org/mainnet"), + CeloAlfajores => { + ("https://explorer.celo.org/alfajores/api", "https://explorer.celo.org/alfajores") + } + CeloBaklava => { + ("https://explorer.celo.org/baklava/api", "https://explorer.celo.org/baklava") + } + + Canto => ("https://evm.explorer.canto.io/api", "https://evm.explorer.canto.io/"), + CantoTestnet => ( + "https://testnet-explorer.canto.neobase.one/api", + "https://testnet-explorer.canto.neobase.one/", + ), + + Boba => ("https://api.bobascan.com/api", "https://bobascan.com"), + + Base => ("https://api.basescan.org/api", "https://basescan.org"), + + BaseGoerli => ("https://api-goerli.basescan.org/api", "https://goerli.basescan.org"), + + ZkSync => { + ("https://zksync2-mainnet-explorer.zksync.io/", "https://explorer.zksync.io/") + } + ZkSyncTestnet => ( + "https://zksync2-testnet-explorer.zksync.dev/", + "https://goerli.explorer.zksync.io/", + ), + Linea => ("https://api.lineascan.build/api", "https://lineascan.build/"), + LineaTestnet => { + ("https://explorer.goerli.linea.build/api", "https://explorer.goerli.linea.build/") + } + Mantle => ("https://explorer.mantle.xyz/api", "https://explorer.mantle.xyz"), + MantleTestnet => { + ("https://explorer.testnet.mantle.xyz/api", "https://explorer.testnet.mantle.xyz") + } + + AnvilHardhat | Dev | Morden | MoonbeamDev | FilecoinMainnet => { + // this is explicitly exhaustive so we don't forget to add new urls when adding a + // new chain + return None; + } + }; + + Some(urls) + } + + /// Returns the chain's blockchain explorer's API key environment variable's default name. + /// + /// # Examples + /// + /// ``` + /// use alloy_chains::Chain; + /// + /// assert_eq!(Chain::Mainnet.etherscan_api_key_name(), Some("ETHERSCAN_API_KEY")); + /// assert_eq!(Chain::AnvilHardhat.etherscan_api_key_name(), None); + /// ``` + pub const fn etherscan_api_key_name(&self) -> Option<&'static str> { + use Chain::*; + + let api_key_name = match self { + Mainnet + | Morden + | Ropsten + | Kovan + | Rinkeby + | Goerli + | Holesky + | Optimism + | OptimismGoerli + | OptimismKovan + | OptimismSepolia + | BinanceSmartChain + | BinanceSmartChainTestnet + | Arbitrum + | ArbitrumTestnet + | ArbitrumGoerli + | ArbitrumSepolia + | ArbitrumNova + | Cronos + | CronosTestnet + | Aurora + | AuroraTestnet + | Celo + | CeloAlfajores + | CeloBaklava + | Base + | Linea + | Mantle + | MantleTestnet + | BaseGoerli + | Gnosis + | Scroll => "ETHERSCAN_API_KEY", + + Avalanche | AvalancheFuji => "SNOWTRACE_API_KEY", + + Polygon | PolygonMumbai | PolygonZkEvm | PolygonZkEvmTestnet => "POLYGONSCAN_API_KEY", + + Fantom | FantomTestnet => "FTMSCAN_API_KEY", + + Moonbeam | Moonbase | MoonbeamDev | Moonriver => "MOONSCAN_API_KEY", + + Canto | CantoTestnet => "BLOCKSCOUT_API_KEY", + + Boba => "BOBASCAN_API_KEY", + + // Explicitly exhaustive. See NB above. + ScrollAlphaTestnet + | Metis + | Chiado + | Sepolia + | Rsk + | Sokol + | Poa + | Oasis + | Emerald + | EmeraldTestnet + | Evmos + | EvmosTestnet + | AnvilHardhat + | Dev + | ZkSync + | ZkSyncTestnet + | FilecoinMainnet + | LineaTestnet + | FilecoinCalibrationTestnet => return None, + }; + + Some(api_key_name) + } + + /// Returns the chain's blockchain explorer's API key, from the environment variable with the + /// name specified in [`etherscan_api_key_name`](Chain::etherscan_api_key_name). + /// + /// # Examples + /// + /// ``` + /// use alloy_chains::Chain; + /// + /// let chain = Chain::Mainnet; + /// std::env::set_var(chain.etherscan_api_key_name().unwrap(), "KEY"); + /// assert_eq!(chain.etherscan_api_key().as_deref(), Some("KEY")); + /// ``` + #[cfg(feature = "std")] + pub fn etherscan_api_key(&self) -> Option { + self.etherscan_api_key_name().and_then(|name| std::env::var(name).ok()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use strum::{EnumCount, IntoEnumIterator}; + + #[test] + #[cfg(feature = "serde")] + fn default() { + assert_eq!(serde_json::to_string(&Chain::default()).unwrap(), "\"mainnet\""); + } + + #[test] + fn enum_iter() { + assert_eq!(Chain::COUNT, Chain::iter().size_hint().0); + } + + #[test] + fn roundtrip_string() { + for chain in Chain::iter() { + let chain_string = chain.to_string(); + assert_eq!(chain_string, format!("{chain}")); + assert_eq!(chain_string.as_str(), chain.as_ref()); + #[cfg(feature = "serde")] + assert_eq!(serde_json::to_string(&chain).unwrap(), format!("\"{chain_string}\"")); + + assert_eq!(chain_string.parse::().unwrap(), chain); + } + } + + #[test] + #[cfg(feature = "serde")] + fn roundtrip_serde() { + for chain in Chain::iter() { + let chain_string = serde_json::to_string(&chain).unwrap(); + let chain_string = chain_string.replace('-', "_"); + assert_eq!(serde_json::from_str::<'_, Chain>(&chain_string).unwrap(), chain); + } + } + + #[test] + fn aliases() { + use Chain::*; + + // kebab-case + const ALIASES: &[(Chain, &[&str])] = &[ + (Mainnet, &["ethlive"]), + (BinanceSmartChain, &["bsc", "binance-smart-chain"]), + (BinanceSmartChainTestnet, &["bsc-testnet", "binance-smart-chain-testnet"]), + (Gnosis, &["gnosis", "gnosis-chain"]), + (PolygonMumbai, &["mumbai"]), + (PolygonZkEvm, &["zkevm", "polygon-zkevm"]), + (PolygonZkEvmTestnet, &["zkevm-testnet", "polygon-zkevm-testnet"]), + (AnvilHardhat, &["anvil", "hardhat"]), + (AvalancheFuji, &["fuji"]), + (ZkSync, &["zksync"]), + (Mantle, &["mantle"]), + (MantleTestnet, &["mantle-testnet"]), + ]; + + for &(chain, aliases) in ALIASES { + for &alias in aliases { + assert_eq!(alias.parse::().unwrap(), chain); + + #[cfg(feature = "serde")] + { + let s = alias.to_string().replace('-', "_"); + assert_eq!(serde_json::from_str::(&format!("\"{s}\"")).unwrap(), chain); + } + } + } + } + + #[test] + #[cfg(feature = "serde")] + fn serde_to_string_match() { + for chain in Chain::iter() { + let chain_serde = serde_json::to_string(&chain).unwrap(); + let chain_string = format!("\"{chain}\""); + assert_eq!(chain_serde, chain_string); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..9973455 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,24 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#[cfg(test)] -mod tests { - use super::*; +#[allow(unused_imports, unused_extern_crates)] +#[macro_use] +extern crate alloc; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +mod chain; +pub use chain::Chain;