diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c8658892..35aaa533 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,6 +27,12 @@ jobs: - name: Dprint check run: dprint check + - name: Install nightly toolchain + run: rustup toolchain install nightly + + - name: Install cargo-fmt for nightly toolchain + run: rustup component add rustfmt --toolchain nightly + - name: cargo fmt run: cargo +nightly fmt --all -- --check @@ -68,9 +74,10 @@ jobs: - name: Install rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: 1.74.1 components: clippy target: x86_64-unknown-linux-musl + override: true - name: cargo clippy (${{ matrix.crate }}) run: | @@ -108,10 +115,19 @@ jobs: - name: Install rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: 1.74.1 components: clippy target: x86_64-unknown-linux-musl + override: true + - name: Checkout code + run: git clone -b release --recurse-submodules https://github.com/ManojJiSharma/nitro-testnode.git + + - name: Run the arbitrum nitro-testnode + run: | + cd nitro-testnode + ./test-node.bash --detach + - name: clippy run: | cargo clippy --locked --workspace --examples --tests --all-features \ @@ -128,9 +144,6 @@ jobs: -Dclippy::pedantic \ -Aclippy::module_name_repetitions - - name: Cleanup Docker - run: ./scripts/reset_docker.sh - - name: Pull nodes run: ./scripts/pull_nodes.sh @@ -142,3 +155,6 @@ jobs: --exclude rosetta-server-ethereum \ --exclude rosetta-server-polkadot \ --exclude rosetta-client + + - name: Cleanup Docker + run: ./scripts/reset_docker.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9df2e92d..32aac61c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ # These are backup files generated by rustfmt **/*.rs.bk + + +# Ignore nitro-testnode folder +chains/arbitrum/testing/nitro-testnode/ diff --git a/Cargo.lock b/Cargo.lock index 2f920a98..ee66f736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5436,6 +5436,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "rosetta-testing-arbitrum" +version = "0.1.0" +dependencies = [ + "alloy-sol-types 0.5.4", + "anyhow", + "ethers", + "ethers-solc", + "rosetta-client", + "rosetta-config-ethereum", + "rosetta-core", + "rosetta-docker", + "rosetta-server-ethereum", + "sequential-test", + "serde_json", + "sha3", + "tokio", + "tracing", + "url", +] + [[package]] name = "rosetta-tx-ethereum" version = "0.5.0" @@ -6112,6 +6133,21 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +[[package]] +name = "sequential-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5facc5f409a55d25bf271c853402a00e1187097d326757043f5dd711944d07" + +[[package]] +name = "sequential-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d9c0d773bc7e7733264f460e5dfa00b2510421ddd6284db0749eef8dfb79e9" +dependencies = [ + "sequential-macro", +] + [[package]] name = "serde" version = "1.0.195" diff --git a/Cargo.toml b/Cargo.toml index 9ac48aa1..fa004aad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "rosetta-docker", "rosetta-server", "rosetta-types", + "chains/arbitrum/testing/rosetta-testing-arbitrum", ] resolver = "2" diff --git a/chains/arbitrum/testing/rosetta-testing-arbitrum/Cargo.toml b/chains/arbitrum/testing/rosetta-testing-arbitrum/Cargo.toml new file mode 100644 index 00000000..f2139ce0 --- /dev/null +++ b/chains/arbitrum/testing/rosetta-testing-arbitrum/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rosetta-testing-arbitrum" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/analog-labs/chain-connectors" +description = "Arbitrum unit test." + +[dependencies] +anyhow = "1.0" +ethers = { version = "2.0", default-features = true, features = ["abigen", "rustls"] } +rosetta-config-ethereum.workspace = true +rosetta-core.workspace = true +rosetta-docker = { workspace = true, features = ["tests"] } +rosetta-server-ethereum.workspace = true +sequential-test = "0.2.4" +serde_json.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing = "0.1.40" + +[dev-dependencies] +alloy-sol-types = { version = "0.5" } +ethers-solc = "2.0" +rosetta-client.workspace = true +sha3 = "0.10" +url = "2.4" diff --git a/chains/arbitrum/testing/rosetta-testing-arbitrum/Readme.md b/chains/arbitrum/testing/rosetta-testing-arbitrum/Readme.md new file mode 100644 index 00000000..d496dc72 --- /dev/null +++ b/chains/arbitrum/testing/rosetta-testing-arbitrum/Readme.md @@ -0,0 +1,11 @@ +# Arbitrum test instructions + +Before running the test-cases need to setup the [Arbitrum nitro-testnode](https://github.com/OffchainLabs/nitro-testnode). + +Place the nitro-testnode in side the testing folder. + +Clone repo using : + +```bash +git clone -b release --recurse-submodules https://github.com/OffchainLabs/nitro-testnode.git +``` diff --git a/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs b/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs new file mode 100644 index 00000000..360961d9 --- /dev/null +++ b/chains/arbitrum/testing/rosetta-testing-arbitrum/src/lib.rs @@ -0,0 +1,407 @@ +//! # Arbitrum Nitro Testnet Rosetta Server +//! +//! This module contains the production test for an Arbitrum Rosetta server implementation +//! specifically designed for interacting with the Arbitrum Nitro Testnet. 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 Arbitrum Nitro +//! Testnet. +//! - 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. +//! - `rosetta_client`: Client library for Rosetta API interactions. +//! - `rosetta_config_ethereum`: Configuration for Ethereum Rosetta server. +//! - `rosetta_server_arbitrum`: Custom client implementation for interacting with Arbitrum. +//! - `sequential_test`: Macro for ensuring sequential execution of asynchronous tests. +//! - `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-arbitrum --lib -- tests --nocapture +//! ``` +//! +//! Note: The code assumes a local Arbitrum Nitro Testnet node running on `ws://127.0.0.1:8548` and +//! a local Ethereum node on `http://localhost:8545`. Ensure that these endpoints are configured correctly. + +#[allow(clippy::ignored_unit_patterns)] +#[cfg(test)] +mod tests { + use alloy_sol_types::{sol, SolCall}; + use anyhow::{Context, Result}; + use ethers::{ + providers::Middleware, + signers::{LocalWallet, Signer}, + types::{transaction::eip2718::TypedTransaction, TransactionRequest, H160, H256, U256}, + utils::hex, + }; + use ethers_solc::{artifacts::Source, CompilerInput, EvmVersion, Solc}; + use rosetta_client::Wallet; + use rosetta_config_ethereum::{AtBlock, CallResult}; + use rosetta_core::{types::PartialBlockIdentifier, BlockchainClient}; + use rosetta_server_ethereum::MaybeWsEthereumClient; + use sequential_test::sequential; + use sha3::Digest; + use std::{collections::BTreeMap, future::Future, path::Path, str::FromStr}; + use tokio::sync::oneshot::{error::TryRecvError, Receiver}; + + sol! { + interface TestContract { + event AnEvent(); + function emitEvent() external; + + function identity(bool a) external view returns (bool); + } + } + + async fn run_test(_future: Fut, mut stop_rx: Receiver<()>) { + loop { + if matches!(stop_rx.try_recv(), Ok(()) | Err(TryRecvError::Closed)) { + break; + } + let hex_string = "0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659"; + let hex_string = &hex_string[2..]; + let mut private_key_result = [0; 32]; + let bytes = hex::decode(hex_string).expect("Failed to decode hex string"); + private_key_result.copy_from_slice(&bytes); + let result = MaybeWsEthereumClient::new( + "arbitrum", + "dev", + "ws://127.0.0.1:8548", + Some(private_key_result), + ) + .await; + assert!(result.is_ok(), "Error creating ArbitrumClient"); + let wallet = LocalWallet::from_bytes(&private_key_result).unwrap(); + let provider = ethers::providers::Provider::::try_from( + "http://localhost:8547", + ) + .expect("Failed to create HTTP provider"); + let address = H160::from_str("0x8Db77D3B019a52788bD3804724f5653d7C9Cf0b6").unwrap(); + let nonce = provider + .get_transaction_count( + H160::from_str("0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E").unwrap(), + None, + ) + .await + .unwrap(); + let chain_id = provider.get_chainid().await.unwrap().as_u64(); + // Create a transaction request + let transaction_request = TransactionRequest { + from: None, + to: Some(ethers::types::NameOrAddress::Address(address)), + value: Some(U256::from(1)), + gas: Some(U256::from(210_000)), + gas_price: Some(U256::from(500_000_000)), + nonce: Some(nonce), + data: None, + chain_id: Some(chain_id.into()), + }; + let tx: TypedTransaction = transaction_request.into(); + let signature = wallet.sign_transaction(&tx).await.unwrap(); + let tx = tx.rlp_signed(&signature); + let _ = provider + .send_raw_transaction(tx) + .await + .unwrap() + .confirmations(1) + .await + .unwrap() + .context("failed to retrieve tx receipt") + .unwrap() + .transaction_hash + .0 + .to_vec(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } + + #[tokio::test] + #[sequential] + async fn network_status() { + let hex_string = "0x8aab161e2a1e57367b60bd870861e3042c2513f8a856f9fee014e7b96e0a2a36"; + // Remove the "0x" prefix + let hex_string = &hex_string[2..]; + let mut result = [0; 32]; + // Parse the hexadecimal string into a Vec + let bytes = hex::decode(hex_string).expect("Failed to decode hex string"); + result.copy_from_slice(&bytes); + + match MaybeWsEthereumClient::new("arbitrum", "dev", "ws://127.0.0.1:8548", Some(result)) + .await + { + Ok(client) => { + // The client was successfully created, continue with the rest of the function + // ... + println!("Client created successfully"); + // Check if the genesis is consistent + let expected_genesis = client.genesis_block().clone(); + tracing::info!("expected_genesis=> {expected_genesis:?}"); + let actual_genesis = client + .block(&PartialBlockIdentifier { index: Some(0), hash: None }) + .await + .unwrap() + .block_identifier; + + tracing::info!("actual_genesis=> {actual_genesis:?}"); + assert_eq!(expected_genesis, actual_genesis); + // Check if the current block is consistent + let expected_current = client.current_block().await.unwrap(); + tracing::info!("expected_current=> {expected_current:?}"); + let actual_current = client + .block(&PartialBlockIdentifier { + index: None, + hash: Some(expected_current.hash.clone()), + }) + .await; + match actual_current { + Ok(block) => { + tracing::info!("actual_current=> {:?}", block.block_identifier); + assert_eq!(expected_current, block.block_identifier); + }, + Err(error) => { + tracing::error!("{error:?}"); + }, + } + + // Check if the finalized block is consistent + let expected_finalized = client.finalized_block().await.unwrap(); + tracing::info!("expected_finalized=> {expected_finalized:?}"); + let actual_finalized = client + .block(&PartialBlockIdentifier { + index: None, + hash: Some(expected_finalized.hash.clone()), + }) + .await; + + match actual_finalized { + Ok(block) => { + tracing::info!("actual_finalized=> {:?}", block.block_identifier); + assert_eq!(expected_finalized, block.block_identifier); + }, + Err(error) => { + tracing::error!("ad{error:?}"); + }, + } + + tracing::info!("Arbitrum network is up and running"); + }, + Err(err) => { + // An error occurred while creating the client, handle the error here + eprintln!("Error creating client: {err:?}"); + }, + } + } + + #[tokio::test] + #[sequential] + async fn test_account() { + let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel::<()>(); + let handler = tokio::spawn(async move { + run_test(async {}, stop_rx).await; + }); + let hex_string = "0x8aab161e2a1e57367b60bd870861e3042c2513f8a856f9fee014e7b96e0a2a36"; + // Remove the "0x" prefix + let hex_string = &hex_string[2..]; + let mut private_key_result = [0; 32]; + // Parse the hexadecimal string into a Vec + let bytes = hex::decode(hex_string).expect("Failed to decode hex string"); + private_key_result.copy_from_slice(&bytes); + let result = MaybeWsEthereumClient::new( + "arbitrum", + "dev", + "ws://127.0.0.1:8548", + Some(private_key_result), + ) + .await; + assert!(result.is_ok(), "Error creating ArbitrumClient"); + let client = result.unwrap(); + let value = 100 * u128::pow(10, client.config().currency_decimals); + let wallet = Wallet::from_config( + client.config().clone(), + "ws://127.0.0.1:8548", + None, + Some(private_key_result), + ) + .await; + match wallet { + Ok(w) => { + let _ = w.faucet(value).await; + let amount = w.balance().await.unwrap(); + assert_eq!((amount.value), (value).to_string()); + assert_eq!(amount.currency, client.config().currency()); + assert!(amount.metadata.is_none()); + }, + Err(e) => { + println!("Error : {e:?}"); + }, + } + stop_tx.send(()).expect("Failed to send stop signal"); + handler.await.expect("Failed to join the background task"); + } + + 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) + } + + #[allow(clippy::needless_raw_string_hashes)] + #[tokio::test] + #[sequential] + async fn test_smart_contract() { + let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel::<()>(); + let handler = tokio::spawn(async move { + run_test(async {}, stop_rx).await; + }); + let hex_string = "0x8aab161e2a1e57367b60bd870861e3042c2513f8a856f9fee014e7b96e0a2a36"; + // Remove the "0x" prefix + let hex_string = &hex_string[2..]; + let mut private_key_result = [0; 32]; + // Parse the hexadecimal string into a Vec + let bytes = hex::decode(hex_string).expect("Failed to decode hex string"); + private_key_result.copy_from_slice(&bytes); + let result = MaybeWsEthereumClient::new( + "arbitrum", + "dev", + "ws://127.0.0.1:8548", + Some(private_key_result), + ) + .await; + assert!(result.is_ok(), "Error creating ArbitrumClient"); + + let client = result.unwrap(); + + let faucet = 100 * u128::pow(10, client.config().currency_decimals); + let wallet = Wallet::from_config( + client.config().clone(), + "ws://127.0.0.1:8548", + None, + Some(private_key_result), + ) + .await + .unwrap(); + wallet.faucet(faucet).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(); + 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).await.unwrap() + }; + 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); + stop_tx.send(()).expect("Failed to send stop signal"); + handler.await.expect("Failed to join the background task"); + } + + #[allow(clippy::needless_raw_string_hashes)] + #[tokio::test] + #[sequential] + async fn test_smart_contract_view() { + let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel::<()>(); + let handler = tokio::spawn(async move { + run_test(async {}, stop_rx).await; + }); + let hex_string = "0x8aab161e2a1e57367b60bd870861e3042c2513f8a856f9fee014e7b96e0a2a36"; + // Remove the "0x" prefix + let hex_string = &hex_string[2..]; + let mut private_key_result = [0; 32]; + // Parse the hexadecimal string into a Vec + let bytes = hex::decode(hex_string).expect("Failed to decode hex string"); + private_key_result.copy_from_slice(&bytes); + let result = MaybeWsEthereumClient::new( + "arbitrum", + "dev", + "ws://127.0.0.1:8548", + Some(private_key_result), + ) + .await; + assert!(result.is_ok(), "Error creating ArbitrumClient"); + let client = result.unwrap(); + let faucet = 100 * u128::pow(10, client.config().currency_decimals); + let wallet = Wallet::from_config( + client.config().clone(), + "ws://127.0.0.1:8548", + None, + Some(private_key_result), + ) + .await + .unwrap(); + wallet.faucet(faucet).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(); + 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( + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1 + ] + .to_vec() + ) + ); + stop_tx.send(()).expect("Failed to send stop signal"); + handler.await.expect("Failed to join the background task"); + } +} diff --git a/chains/astar/server/src/lib.rs b/chains/astar/server/src/lib.rs index d5214c17..b59fbef7 100644 --- a/chains/astar/server/src/lib.rs +++ b/chains/astar/server/src/lib.rs @@ -67,7 +67,8 @@ impl AstarClient { let backend = LegacyBackend::new(rpc_client); let substrate_client = OnlineClient::::from_backend(Arc::new(backend)).await?; - let ethereum_client = MaybeWsEthereumClient::from_jsonrpsee(config, ws_client).await?; + let ethereum_client = + MaybeWsEthereumClient::from_jsonrpsee(config, ws_client, None).await?; Ok(Self { client: ethereum_client, ws_client: substrate_client, rpc_methods }) } @@ -301,6 +302,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::needless_raw_string_hashes)] async fn test_smart_contract() -> Result<()> { let config = rosetta_config_astar::config("dev")?; @@ -336,6 +338,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::needless_raw_string_hashes)] async fn test_smart_contract_view() -> Result<()> { let config = rosetta_config_astar::config("dev")?; let faucet = 100 * u128::pow(10, config.currency_decimals); diff --git a/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index a4debad2..7b673b61 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -32,6 +32,7 @@ pub fn polygon_config(network: &str) -> Result { /// # Errors /// Returns `Err` if the network is not supported pub fn arbitrum_config(network: &str) -> Result { + // All available networks are listed here: let (network, bip44_id, is_dev) = match network { "dev" => ("dev", 1, true), "goerli" => ("goerli", 1, true), diff --git a/chains/ethereum/server/src/client.rs b/chains/ethereum/server/src/client.rs index d94846cf..40fc9817 100644 --- a/chains/ethereum/server/src/client.rs +++ b/chains/ethereum/server/src/client.rs @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use ethers::{ prelude::*, providers::{JsonRpcClient, Middleware, Provider}, - types::Bytes, + types::{transaction::eip2718::TypedTransaction, Bytes}, utils::{keccak256, rlp::Encodable}, }; use rosetta_config_ethereum::{ @@ -22,7 +22,13 @@ use rosetta_core::{ }, BlockchainConfig, }; -use std::{str::FromStr, sync::Arc}; +use std::{ + str::FromStr, + sync::{ + atomic::{self, Ordering}, + Arc, + }, +}; /// Strategy used to determine the finalized block #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -52,6 +58,8 @@ pub struct EthereumClient

{ client: Arc>, genesis_block: NonPendingBlock, block_finality_strategy: BlockFinalityStrategy, + nonce: Arc, + private_key: Option<[u8; 32]>, } impl

Clone for EthereumClient

{ @@ -61,6 +69,8 @@ impl

Clone for EthereumClient

{ client: self.client.clone(), genesis_block: self.genesis_block.clone(), block_finality_strategy: self.block_finality_strategy, + nonce: self.nonce.clone(), + private_key: self.private_key, } } } @@ -69,15 +79,30 @@ impl

EthereumClient

where P: JsonRpcClient + 'static, { - pub async fn new(config: BlockchainConfig, rpc_client: P) -> Result { + #[allow(clippy::missing_errors_doc)] + pub async fn new( + config: BlockchainConfig, + rpc_client: P, + private_key: Option<[u8; 32]>, + ) -> Result { let block_finality_strategy = BlockFinalityStrategy::from_config(&config); let client = Arc::new(Provider::new(rpc_client)); + let (private_key, nonce) = if let Some(private) = private_key { + let wallet = LocalWallet::from_bytes(&private)?; + let address = wallet.address(); + let nonce = Arc::new(atomic::AtomicU32::from( + client.get_transaction_count(address, None).await?.as_u32(), + )); + (private_key, nonce) + } else { + (None, Arc::new(atomic::AtomicU32::new(0))) + }; let Some(genesis_block) = get_non_pending_block(Arc::clone(&client), BlockNumber::Number(0.into())).await? else { anyhow::bail!("FATAL: genesis block not found"); }; - Ok(Self { config, client, genesis_block, block_finality_strategy }) + Ok(Self { config, client, genesis_block, block_finality_strategy, nonce, private_key }) } pub const fn config(&self) -> &BlockchainConfig { @@ -88,10 +113,12 @@ where &self.genesis_block.identifier } + #[allow(clippy::missing_errors_doc)] pub async fn node_version(&self) -> Result { Ok(self.client.client_version().await?) } + #[allow(clippy::missing_errors_doc)] pub async fn current_block(&self) -> Result { let index = self.client.get_block_number().await?.as_u64(); let Some(block_hash) = self.client.get_block(index).await?.context("missing block")?.hash @@ -101,6 +128,7 @@ where Ok(BlockIdentifier { index, hash: hex::encode(block_hash) }) } + #[allow(clippy::missing_errors_doc)] pub async fn finalized_block(&self, latest_block: Option) -> Result { let number: BlockNumber = match self.block_finality_strategy { BlockFinalityStrategy::Confirmations(confirmations) => { @@ -130,6 +158,7 @@ where Ok(finalized_block) } + #[allow(clippy::missing_errors_doc)] pub async fn balance(&self, address: &Address, block: &BlockIdentifier) -> Result { let block = hex::decode(&block.hash)? .try_into() @@ -142,28 +171,66 @@ where .as_u128()) } - #[allow(clippy::unused_async)] + #[allow(clippy::unused_async, clippy::missing_errors_doc)] pub async fn coins(&self, _address: &Address, _block: &BlockIdentifier) -> Result> { anyhow::bail!("not a utxo chain"); } + #[allow(clippy::single_match_else, clippy::missing_errors_doc)] pub async fn faucet(&self, address: &Address, param: u128) -> Result> { - // first account will be the coinbase account on a dev net - let coinbase = self.client.get_accounts().await?[0]; - let address: H160 = address.address().parse()?; - let tx = TransactionRequest::new().to(address).value(param).from(coinbase); - Ok(self - .client - .send_transaction(tx, None) - .await? - .confirmations(2) - .await? - .context("failed to retrieve tx receipt")? - .transaction_hash - .0 - .to_vec()) + match self.private_key { + Some(private_key) => { + let chain_id = self.client.get_chainid().await?.as_u64(); + let address: H160 = address.address().parse()?; + let wallet = LocalWallet::from_bytes(&private_key)?; + let nonce_u32 = U256::from(self.nonce.load(Ordering::Relaxed)); + // Create a transaction request + let transaction_request = TransactionRequest { + from: None, + to: Some(ethers::types::NameOrAddress::Address(address)), + value: Some(U256::from(param)), + gas: Some(U256::from(210_000)), + gas_price: Some(U256::from(500_000_000)), + nonce: Some(nonce_u32), + data: None, + chain_id: Some(chain_id.into()), + }; + + let tx: TypedTransaction = transaction_request.into(); + let signature = wallet.sign_transaction(&tx).await?; + let tx = tx.rlp_signed(&signature); + let response = self + .client + .send_raw_transaction(tx) + .await? + .confirmations(2) + .await? + .context("failed to retrieve tx receipt")? + .transaction_hash + .0 + .to_vec(); + Ok(response) + }, + None => { + // first account will be the coinbase account on a dev net + let coinbase = self.client.get_accounts().await?[0]; + let address: H160 = address.address().parse()?; + let tx = TransactionRequest::new().to(address).value(param).from(coinbase); + Ok(self + .client + .send_transaction(tx, None) + .await? + .confirmations(2) + .await? + .context("failed to retrieve tx receipt")? + .transaction_hash + .0 + .to_vec()) + }, + } } + #[allow(clippy::missing_errors_doc)] pub async fn metadata( &self, public_key: &PublicKey, @@ -196,6 +263,7 @@ where }) } + #[allow(clippy::missing_errors_doc)] pub async fn submit(&self, transaction: &[u8]) -> Result> { let tx = transaction.to_vec().into(); Ok(self @@ -210,6 +278,7 @@ where .to_vec()) } + #[allow(clippy::missing_errors_doc)] pub async fn block(&self, block_identifier: &PartialBlockIdentifier) -> Result { let block_id = if let Some(hash) = block_identifier.hash.as_ref() { BlockId::Hash(H256::from_str(hash)?) @@ -233,16 +302,6 @@ where let block_reward_transaction = crate::utils::block_reward_transaction(&self.client, self.config(), &block).await?; transactions.push(block_reward_transaction); - for transaction in &block.transactions { - let transaction = crate::utils::get_transaction( - &self.client, - self.config(), - block.clone(), - transaction, - ) - .await?; - transactions.push(transaction); - } Ok(Block { block_identifier: BlockIdentifier { index: block_number.as_u64(), @@ -258,6 +317,7 @@ where }) } + #[allow(clippy::missing_errors_doc)] pub async fn block_transaction( &self, block: &BlockIdentifier, @@ -276,7 +336,7 @@ where Ok(transaction) } - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_lines, clippy::missing_errors_doc)] pub async fn call(&self, req: &EthQuery) -> Result { let result = match req { EthQuery::GetBalance(GetBalance { address, block }) => { @@ -414,6 +474,7 @@ impl

EthereumClient

where P: PubsubClient + 'static, { + #[allow(clippy::missing_errors_doc)] pub async fn listen(&self) -> Result> { let new_head_subscription = self.client.subscribe_blocks().await?; Ok(EthereumEventStream::new(self, new_head_subscription)) diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 44e35452..cc134ad0 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use client::EthereumClient; +pub use client::EthereumClient; use ethers::providers::Http; pub use rosetta_config_ethereum::{ EthereumMetadata, EthereumMetadataParams, Query as EthQuery, QueryResult as EthQueryResult, @@ -36,7 +36,7 @@ pub enum MaybeWsEthereumClient { impl MaybeWsEthereumClient { /// Creates a new ethereum client from `network` and `addr`. - /// Supported blockchains are `ethereum` and `polygon` + /// Supported blockchains are `ethereum`, `polygon` and `arbitrum` /// /// # Errors /// Will return `Err` when the network is invalid, or when the provided `addr` is unreacheable. @@ -44,6 +44,7 @@ impl MaybeWsEthereumClient { blockchain: &str, network: &str, addr: S, + private_key: Option<[u8; 32]>, ) -> Result { let config = match blockchain { "ethereum" => rosetta_config_ethereum::config(network)?, @@ -51,7 +52,7 @@ impl MaybeWsEthereumClient { "arbitrum" => rosetta_config_ethereum::arbitrum_config(network)?, blockchain => anyhow::bail!("unsupported blockchain: {blockchain}"), }; - Self::from_config(config, addr).await + Self::from_config(config, addr, private_key).await } /// Creates a new bitcoin client from `config` and `addr` @@ -61,14 +62,15 @@ impl MaybeWsEthereumClient { pub async fn from_config + Send>( config: BlockchainConfig, addr: S, + private_key: Option<[u8; 32]>, ) -> Result { let uri = Url::parse(addr.as_ref())?; if uri.scheme() == "ws" || uri.scheme() == "wss" { let client = default_client(uri.as_str(), None).await?; - Self::from_jsonrpsee(config, client).await + Self::from_jsonrpsee(config, client, private_key).await } else { let http_connection = Http::new(uri); - let client = EthereumClient::new(config, http_connection).await?; + let client = EthereumClient::new(config, http_connection, private_key).await?; Ok(Self::Http(client)) } } @@ -78,9 +80,13 @@ impl MaybeWsEthereumClient { /// /// # Errors /// Will return `Err` when the network is invalid, or when the provided `addr` is unreacheable. - pub async fn from_jsonrpsee(config: BlockchainConfig, client: DefaultClient) -> Result { + pub async fn from_jsonrpsee( + config: BlockchainConfig, + client: DefaultClient, + private_key: Option<[u8; 32]>, + ) -> Result { let ws_connection = EthPubsubAdapter::new(client); - let client = EthereumClient::new(config, ws_connection).await?; + let client = EthereumClient::new(config, ws_connection, private_key).await?; Ok(Self::Ws(client)) } } @@ -227,7 +233,7 @@ mod tests { pub async fn client_from_config(config: BlockchainConfig) -> Result { let url = config.node_uri.to_string(); - MaybeWsEthereumClient::from_config(config, url.as_str()).await + MaybeWsEthereumClient::from_config(config, url.as_str(), None).await } #[tokio::test] @@ -283,6 +289,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::needless_raw_string_hashes)] async fn test_smart_contract() -> Result<()> { let config = rosetta_config_ethereum::config("dev")?; @@ -317,6 +324,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::needless_raw_string_hashes)] async fn test_smart_contract_view() -> Result<()> { let config = rosetta_config_ethereum::config("dev")?; diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 00333eca..3cf01fe8 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -96,7 +96,6 @@ pub async fn get_transaction( if tx_receipt.block_hash.context("Block hash not found in tx receipt")? != block_hash { bail!("Transaction receipt block hash does not match block hash"); } - let currency = config.currency(); let mut operations = vec![]; diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index 0dd8ffb5..337fa7f6 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -35,22 +35,27 @@ pub enum GenericClient { #[allow(clippy::missing_errors_doc)] impl GenericClient { - pub async fn new(blockchain: Blockchain, network: &str, url: &str) -> Result { + pub async fn new( + blockchain: Blockchain, + network: &str, + url: &str, + private_key: Option<[u8; 32]>, + ) -> Result { Ok(match blockchain { Blockchain::Bitcoin => { let client = BitcoinClient::new(network, url).await?; Self::Bitcoin(client) }, Blockchain::Ethereum => { - let client = EthereumClient::new("ethereum", network, url).await?; + let client = EthereumClient::new("ethereum", network, url, private_key).await?; Self::Ethereum(client) }, Blockchain::Polygon => { - let client = EthereumClient::new("polygon", network, url).await?; + let client = EthereumClient::new("polygon", network, url, private_key).await?; Self::Ethereum(client) }, Blockchain::Arbitrum => { - let client = EthereumClient::new("arbitrum", network, url).await?; + let client = EthereumClient::new("arbitrum", network, url, private_key).await?; Self::Ethereum(client) }, Blockchain::Astar => { @@ -67,7 +72,11 @@ impl GenericClient { }) } - pub async fn from_config(config: BlockchainConfig, url: &str) -> Result { + pub async fn from_config( + config: BlockchainConfig, + url: &str, + private_key: Option<[u8; 32]>, + ) -> Result { let blockchain = Blockchain::from_str(config.blockchain)?; Ok(match blockchain { Blockchain::Bitcoin => { @@ -75,7 +84,7 @@ impl GenericClient { Self::Bitcoin(client) }, Blockchain::Ethereum | Blockchain::Polygon | Blockchain::Arbitrum => { - let client = EthereumClient::from_config(config, url).await?; + let client = EthereumClient::from_config(config, url, private_key).await?; Self::Ethereum(client) }, Blockchain::Astar => { diff --git a/rosetta-client/src/wallet.rs b/rosetta-client/src/wallet.rs index 868ec867..03b81a9f 100644 --- a/rosetta-client/src/wallet.rs +++ b/rosetta-client/src/wallet.rs @@ -37,8 +37,9 @@ impl Wallet { network: &str, url: &str, keyfile: Option<&Path>, + private_key: Option<[u8; 32]>, ) -> Result { - let client = GenericClient::new(blockchain, network, url).await?; + let client = GenericClient::new(blockchain, network, url, private_key).await?; Self::from_client(client, keyfile) } @@ -48,8 +49,9 @@ impl Wallet { config: BlockchainConfig, url: &str, keyfile: Option<&Path>, + private_key: Option<[u8; 32]>, ) -> Result { - let client = GenericClient::from_config(config, url).await?; + let client = GenericClient::from_config(config, url, private_key).await?; Self::from_client(client, keyfile) } diff --git a/rosetta-docker/src/lib.rs b/rosetta-docker/src/lib.rs index 95d9c7eb..25c8c9cc 100644 --- a/rosetta-docker/src/lib.rs +++ b/rosetta-docker/src/lib.rs @@ -61,7 +61,7 @@ impl Env { pub async fn ephemeral_wallet(&self) -> Result { let config = self.client.config().clone(); let node_uri = config.node_uri.to_string(); - Wallet::from_config(config, &node_uri, None).await + Wallet::from_config(config, &node_uri, None, None).await } /// Stop all containers