diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4c604cf..3cb9b136 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 - + - name: Install rust toolchain uses: actions-rs/toolchain@v1 with: @@ -22,7 +22,7 @@ jobs: components: rustfmt, clippy - name: Install dprint - run: cargo install --locked dprint + run: cargo install dprint --locked --version 0.47.1 - name: Dprint check run: dprint check @@ -59,7 +59,7 @@ jobs: needs: [rustfmt] strategy: matrix: - crate: [rosetta-server-astar, rosetta-server-ethereum, rosetta-server-polkadot, rosetta-client, rosetta-testing-arbitrum, rosetta-testing-binance, rosetta-testing-avalanche, rosetta-testing-base,] + crate: [rosetta-server-astar, rosetta-server-ethereum, rosetta-server-polkadot, rosetta-client, rosetta-testing-arbitrum, rosetta-testing-binance, rosetta-testing-avalanche, rosetta-testing-base, rosetta-testing-linea, ] name: ${{ matrix.crate }} runs-on: self-hosted steps: @@ -118,7 +118,7 @@ jobs: if: ${{matrix.crate == 'rosetta-testing-avalanche'}} run: | docker pull analoglabs/avalanche-cli - docker run -v MY_LOCAL_CLI_DIR:/root/.avalanche-cli/ analoglabs/avalanche-cli blockchain create localnew --evm --evm-token SUB --genesis ./genesis.json --teleporter=false --vm-version v0.6.9 + docker run -v MY_LOCAL_CLI_DIR:/root/.avalanche-cli/ analoglabs/avalanche-cli blockchain create localnew --evm --evm-token SUB --genesis ./genesis.json --teleporter=false --vm-version v0.6.9 | echo "Yes" docker run -d -v MY_LOCAL_CLI_DIR:/root/.avalanche-cli/ -p 9650:9650 --entrypoint bash analoglabs/avalanche-cli -c "/avalanche blockchain deploy localnew --local;tail -f /dev/null" - name: Setup BSC node @@ -127,6 +127,14 @@ jobs: docker pull manojanalog/bsc_for_analog docker run -d -p 8545:8545 -p 8546:8546 manojanalog/bsc_for_analog:latest geth --datadir ./datadir --unlock 0x5e5C830f97292a3C6Bfea464D3ad4CE631e6Fbc5 --allow-insecure-unlock --http --http.addr 0.0.0.0 --http.port 8545 --http.api personal,db,eth,net,web3 --mine --miner.etherbase 0x5e5C830f97292a3C6Bfea464D3ad4CE631e6Fbc5 --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.api personal,db,eth,net,web3 --rpc.allow-unprotected-txs --password password.txt + - name: Setup Linea-monorepo node + if: ${{ matrix.crate == 'rosetta-testing-linea' }} + run: | + git clone https://github.com/Consensys/linea-monorepo.git + cd linea-monorepo + make pnpm-install + make fresh-start-all + - name: test (${{ matrix.crate }}) run: cargo test --locked -p ${{ matrix.crate }} @@ -161,6 +169,7 @@ jobs: --exclude rosetta-server-astar \ --exclude rosetta-testing-avalanche \ --exclude rosetta-testing-binance \ + --exclude rosetta-testing-linea \ --exclude rosetta-server-ethereum \ --exclude rosetta-testing-base \ --exclude rosetta-server-polkadot \ @@ -189,6 +198,7 @@ jobs: --exclude rosetta-server-astar \ --exclude rosetta-testing-avalanche \ --exclude rosetta-testing-binance \ + --exclude rosetta-testing-linea \ --exclude rosetta-server-ethereum \ --exclude rosetta-testing-base \ --exclude rosetta-server-polkadot \ diff --git a/Cargo.lock b/Cargo.lock index a8edf127..90097d0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5958,6 +5958,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "rosetta-testing-linea" +version = "0.1.0" +dependencies = [ + "alloy-sol-types 0.8.3", + "anyhow", + "ethers", + "ethers-solc", + "hex-literal", + "rosetta-chain-testing", + "rosetta-client", + "rosetta-config-ethereum", + "rosetta-core", + "rosetta-server-ethereum", + "sha3", + "tokio", +] + [[package]] name = "rosetta-testing-polygon" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f00e54fb..cd806770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "chains/avalanche", "chains/rosetta-chain-testing", "chains/base", + "chains/linea", ] resolver = "2" diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index 54bdc8bf..12c6421d 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -279,6 +279,21 @@ pub fn base_config(network: &str) -> anyhow::Result { Ok(evm_config("avalanche", network, "AVAX", bip44_id, is_dev)) } +/// Retrieve the [`BlockchainConfig`] from the provided base `network` +/// +/// # Errors +/// Returns `Err` if the network is not supported +pub fn linea_config(network: &str) -> anyhow::Result { + // All available networks are listed here: + let (network, bip44_id, is_dev) = match network { + "dev" => ("dev", 1, true), + "sepolia" => ("sepolia", 59141, true), + "mainnet" => ("mainnet", 59144, false), + _ => anyhow::bail!("unsupported network: {}", network), + }; + Ok(evm_config("linea", network, "ETH", bip44_id, is_dev)) +} + /// Retrieve the [`BlockchainConfig`] from the provided ethereum `network` /// /// # Errors @@ -320,6 +335,11 @@ pub fn config(network: &str) -> anyhow::Result { "base" => return base_config("mainnet"), "base-sepolia" => return base_config("fuji"), + // Base + "linea-local" => return linea_config("dev"), + "linea" => return linea_config("mainnet"), + "linea-sepolia" => return linea_config("basu"), + network => return astar_config(network), }; diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index a1961b49..94eb53f7 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -67,6 +67,7 @@ impl MaybeWsEthereumClient { "binance" => rosetta_config_ethereum::binance_config(network)?, "avalanche" => rosetta_config_ethereum::avalanche_config(network)?, "base" => rosetta_config_ethereum::base_config(network)?, + "linea" => rosetta_config_ethereum::linea_config(network)?, blockchain => anyhow::bail!("unsupported blockchain: {blockchain}"), }; Self::from_config(config, addr, private_key).await diff --git a/chains/linea/Cargo.toml b/chains/linea/Cargo.toml new file mode 100644 index 00000000..a1fd5241 --- /dev/null +++ b/chains/linea/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rosetta-testing-linea" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/analog-labs/chain-connectors" +description = "linea rosetta test." + +[dependencies] +alloy-sol-types = { version = "0.8" } +anyhow = "1.0" +ethers = { version = "2.0", default-features = true, features = ["abigen", "rustls", "ws"] } +ethers-solc = "2.0" +hex-literal = "0.4" +rosetta-chain-testing = { path = "../rosetta-chain-testing" } +rosetta-client.workspace = true +rosetta-config-ethereum.workspace = true +rosetta-core.workspace = true +rosetta-server-ethereum.workspace = true +sha3 = "0.10" +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/chains/linea/src/lib.rs b/chains/linea/src/lib.rs new file mode 100644 index 00000000..638d4e8a --- /dev/null +++ b/chains/linea/src/lib.rs @@ -0,0 +1,250 @@ +//! # Binance Rosetta Server Test Suite +//! +//! This module contains a test suite for an Ethereum Rosetta server implementation +//! specifically designed for interacting with the Binance network. The code includes +//! tests for network status, account management, and smart contract interaction. +//! +//! ## Features +//! +//! - Network status tests to ensure proper connection and consistency with the Binance network. +//! - Account tests, including faucet funding, balance retrieval, and error handling. +//! - Smart contract tests covering deployment, event emission, and view function calls. +//! +//! ## Dependencies +//! +//! - `anyhow`: For flexible error handling. +//! - `alloy_sol_types`: Custom types and macros for interacting with Solidity contracts. +//! - `ethers`: Ethereum library for interaction with Ethereum clients. +//! - `ethers_solc`: Integration for compiling Solidity code using the Solc compiler. +//! - `hex_literal`: Macro for creating byte array literals from hexadecimal strings. +//! - `rosetta_client`: Client library for Rosetta API interactions. +//! - `rosetta_config_ethereum`: Configuration for Ethereum Rosetta server. +//! - `rosetta_server_ethereum`: Custom client implementation for interacting with Ethereum. +//! - `sha3`: SHA-3 (Keccak) implementation for hashing. +//! - `tokio`: Asynchronous runtime for running async functions. +//! +//! ## Usage +//! +//! To run the tests, execute the following command: +//! +//! ```sh +//! cargo test --package rosetta-testing-linea --lib -- tests --nocapture +//! ``` +//! +//! Note: The code assumes a local Binance RPC node running on `ws://127.0.0.1:8546`. Ensure +//! that this endpoint is configured correctly. + +#[allow(clippy::ignored_unit_patterns, clippy::pub_underscore_fields)] +#[cfg(test)] +mod tests { + use alloy_sol_types::{sol, SolCall}; + use anyhow::Result; + use ethers::types::H256; + + use ethers_solc::{artifacts::Source, CompilerInput, EvmVersion, Solc}; + use hex_literal::hex; + use rosetta_chain_testing::run_test; + use rosetta_client::Wallet; + use rosetta_config_ethereum::{AtBlock, CallResult}; + use rosetta_core::BlockchainClient; + use rosetta_server_ethereum::MaybeWsEthereumClient; + use sha3::Digest; + use std::{collections::BTreeMap, path::Path}; + + /// Binance rpc url + const LINEA_RPC_WS_URL: &str = "http://localhost:8545"; + + sol! { + interface TestContract { + event AnEvent(); + function emitEvent() external; + + function identity(bool a) external view returns (bool); + } + } + + #[tokio::test] + async fn network_status() { + run_test(async move { + let client = MaybeWsEthereumClient::new("linea", "dev", LINEA_RPC_WS_URL, None) + .await + .expect("Error creating client"); + // Check if the genesis is consistent + let genesis_block = client.genesis_block(); + assert_eq!(genesis_block.index, 0); + + // Check if the current block is consistent + let current_block = client.current_block().await.unwrap(); + if current_block.index > 0 { + assert_ne!(current_block.hash, genesis_block.hash); + } else { + assert_eq!(current_block.hash, genesis_block.hash); + } + + // Check if the finalized block is consistent + let finalized_block = client.finalized_block().await.unwrap(); + assert!(finalized_block.index >= genesis_block.index); + }) + .await; + } + + #[tokio::test] + async fn test_account() { + run_test(async move { + let client = MaybeWsEthereumClient::new("linea", "dev", LINEA_RPC_WS_URL, None) + .await + .expect("Error creating LineaClient"); + let wallet = Wallet::from_config(client.config().clone(), LINEA_RPC_WS_URL, None, None) + .await + .unwrap(); + let value = 10 * u128::pow(10, client.config().currency_decimals); + let _ = wallet.faucet(value, None).await; + let amount = wallet.balance().await.unwrap(); + assert_eq!(amount, value); + }) + .await; + } + + #[tokio::test] + async fn test_construction() { + run_test(async move { + let client = MaybeWsEthereumClient::new("linea", "dev", LINEA_RPC_WS_URL, None) + .await + .expect("Error creating LineaClient"); + let faucet = 100 * u128::pow(10, client.config().currency_decimals); + let value = u128::pow(10, client.config().currency_decimals); + let alice = Wallet::from_config(client.config().clone(), LINEA_RPC_WS_URL, None, None) + .await + .unwrap(); + let bob = Wallet::from_config(client.config().clone(), LINEA_RPC_WS_URL, None, None) + .await + .unwrap(); + assert_ne!(alice.public_key(), bob.public_key()); + + // Alice and bob have no balance + let balance = alice.balance().await.unwrap(); + assert_eq!(balance, 0); + let balance = bob.balance().await.unwrap(); + assert_eq!(balance, 0); + + // Transfer faucets to alice + alice.faucet(faucet, None).await.unwrap(); + let balance = alice.balance().await.unwrap(); + assert_eq!(balance, faucet); + + // Alice transfers to bob + alice.transfer(bob.account(), value, None, None).await.unwrap(); + let amount = bob.balance().await.unwrap(); + assert_eq!(amount, value); + }) + .await; + } + + fn compile_snippet(source: &str) -> Result> { + let solc = Solc::default(); + let source = format!("contract Contract {{ {source} }}"); + let mut sources = BTreeMap::new(); + sources.insert(Path::new("contract.sol").into(), Source::new(source)); + let input = CompilerInput::with_sources(sources)[0] + .clone() + .evm_version(EvmVersion::Homestead); + let output = solc.compile_exact(&input)?; + let file = output.contracts.get("contract.sol").unwrap(); + let contract = file.get("Contract").unwrap(); + let bytecode = contract + .evm + .as_ref() + .unwrap() + .bytecode + .as_ref() + .unwrap() + .object + .as_bytes() + .unwrap() + .to_vec(); + Ok(bytecode) + } + + #[tokio::test] + async fn test_smart_contract() { + run_test(async move { + let client = MaybeWsEthereumClient::new("linea", "dev", LINEA_RPC_WS_URL, None) + .await + .expect("Error creating LineaClient"); + let faucet = 10 * u128::pow(10, client.config().currency_decimals); + let wallet = Wallet::from_config(client.config().clone(), LINEA_RPC_WS_URL, None, None) + .await + .unwrap(); + wallet.faucet(faucet, None).await.unwrap(); + + let bytes = compile_snippet( + r" + event AnEvent(); + function emitEvent() public { + emit AnEvent(); + } + ", + ) + .unwrap(); + let tx_hash = wallet.eth_deploy_contract(bytes).await.unwrap().tx_hash().0; + let receipt = wallet.eth_transaction_receipt(tx_hash).await.unwrap().unwrap(); + let contract_address = receipt.contract_address.unwrap(); + let tx_hash = { + let call = TestContract::emitEventCall {}; + wallet + .eth_send_call(contract_address.0, call.abi_encode(), 0, None, None) + .await + .unwrap() + .tx_hash() + .0 + }; + let receipt = wallet.eth_transaction_receipt(tx_hash).await.unwrap().unwrap(); + assert_eq!(receipt.logs.len(), 1); + let topic = receipt.logs[0].topics[0]; + let expected = H256(sha3::Keccak256::digest("AnEvent()").into()); + assert_eq!(topic, expected); + }) + .await; + } + + #[tokio::test] + async fn test_smart_contract_view() { + run_test(async move { + let client = MaybeWsEthereumClient::new("linea", "dev", LINEA_RPC_WS_URL, None) + .await + .expect("Error creating LineaClient"); + let faucet = 10 * u128::pow(10, client.config().currency_decimals); + let wallet = Wallet::from_config(client.config().clone(), LINEA_RPC_WS_URL, None, None) + .await + .unwrap(); + wallet.faucet(faucet, None).await.unwrap(); + let bytes = compile_snippet( + r" + function identity(bool a) public view returns (bool) { + return a; + } + ", + ) + .unwrap(); + let tx_hash = wallet.eth_deploy_contract(bytes).await.unwrap().tx_hash().0; + let receipt = wallet.eth_transaction_receipt(tx_hash).await.unwrap().unwrap(); + let contract_address = receipt.contract_address.unwrap(); + + let response = { + let call = TestContract::identityCall { a: true }; + wallet + .eth_view_call(contract_address.0, call.abi_encode(), AtBlock::Latest) + .await + .unwrap() + }; + assert_eq!( + response, + CallResult::Success( + hex!("0000000000000000000000000000000000000000000000000000000000000001") + .to_vec() + ) + ); + }) + .await; + } +} diff --git a/deny.toml b/deny.toml index faf7383a..8c458d36 100644 --- a/deny.toml +++ b/deny.toml @@ -110,6 +110,11 @@ ignore = [ # RUSTSEC-2024-0388 derivative is unmaintained; consider using an alternative { id = 'RUSTSEC-2024-0388', reason = 'derivative is unmaintained; consider using an alternative' }, + + # RUSTSEC-2024-0421 + { id = 'RUSTSEC-2024-0421', reason = 'idna accepts Punycode labels that do not produce any non-ASCII when decoded' }, + # RUSTSEC-2024-0399 + { id = 'RUSTSEC-2024-0399', reason = 'ustls network-reachable panic in Acceptor::accept' }, ] # This section is considered when running `cargo deny check sources`. diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index f12b584c..f4c736d2 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -62,6 +62,10 @@ impl GenericClient { let client = EthereumClient::new("base", network, url, private_key).await?; Self::Ethereum(client) }, + Blockchain::Linea => { + let client = EthereumClient::new("linea", network, url, private_key).await?; + Self::Ethereum(client) + }, Blockchain::Avalanche => { let client = EthereumClient::new("avalanche", network, url, private_key).await?; Self::Ethereum(client) @@ -92,6 +96,7 @@ impl GenericClient { Blockchain::Arbitrum | Blockchain::Binance | Blockchain::Base | + Blockchain::Linea | Blockchain::Avalanche => { let client = EthereumClient::from_config(config, url, private_key).await?; Self::Ethereum(client) diff --git a/rosetta-client/src/lib.rs b/rosetta-client/src/lib.rs index 794f7e3a..504ee9cf 100644 --- a/rosetta-client/src/lib.rs +++ b/rosetta-client/src/lib.rs @@ -53,6 +53,8 @@ pub enum Blockchain { Avalanche, /// Base Base, + /// Linea + Linea, } impl std::str::FromStr for Blockchain { @@ -72,6 +74,7 @@ impl std::str::FromStr for Blockchain { "binance" => Self::Binance, "avalanche" => Self::Avalanche, "base" => Self::Base, + "linea" => Self::Linea, _ => anyhow::bail!("unsupported blockchain {}", blockchain), }) } diff --git a/rosetta-client/src/tx_builder.rs b/rosetta-client/src/tx_builder.rs index 4c894e7a..c2a01276 100644 --- a/rosetta-client/src/tx_builder.rs +++ b/rosetta-client/src/tx_builder.rs @@ -17,7 +17,7 @@ impl GenericTransactionBuilder { pub fn new(config: &BlockchainConfig) -> Result { Ok(match config.blockchain { "astar" => Self::Astar(rosetta_tx_ethereum::EthereumTransactionBuilder), - "ethereum" | "polygon" | "arbitrum" | "binance" | "base" | "avalanche" => { + "ethereum" | "polygon" | "arbitrum" | "binance" | "base" | "avalanche" | "linea" => { Self::Ethereum(rosetta_tx_ethereum::EthereumTransactionBuilder) }, "polkadot" | "westend" | "rococo" => {