diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 86c4276b..0b92af0f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -59,7 +59,7 @@ jobs: needs: [rustfmt] strategy: matrix: - crate: [rosetta-server-astar, rosetta-server-ethereum, rosetta-server-polkadot, rosetta-client, rosetta-testing-arbitrum] + crate: [rosetta-server-astar, rosetta-server-ethereum, rosetta-server-polkadot, rosetta-client, rosetta-testing-arbitrum, rosetta-testing-binance] name: ${{ matrix.crate }} runs-on: self-hosted steps: @@ -104,6 +104,12 @@ jobs: run: | cd nitro-testnode ./test-node.bash --detach + + - name: Setup BSC node + if: ${{ matrix.crate == 'rosetta-testing-binance' }} + run: | + 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: test (${{ matrix.crate }}) run: cargo test --locked -p ${{ matrix.crate }} @@ -135,6 +141,7 @@ jobs: cargo clippy --locked --workspace --examples --tests --all-features \ --exclude rosetta-testing-arbitrum \ --exclude rosetta-server-astar \ + --exclude rosetta-testing-binance \ --exclude rosetta-server-ethereum \ --exclude rosetta-server-polkadot \ --exclude rosetta-client \ @@ -157,6 +164,7 @@ jobs: cargo test --locked --workspace --all-features \ --exclude rosetta-testing-arbitrum \ --exclude rosetta-server-astar \ + --exclude rosetta-testing-binance \ --exclude rosetta-server-ethereum \ --exclude rosetta-server-polkadot \ --exclude rosetta-client diff --git a/Cargo.lock b/Cargo.lock index d24fedff..222b6dc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5751,6 +5751,23 @@ dependencies = [ "url", ] +[[package]] +name = "rosetta-testing-binance" +version = "0.1.0" +dependencies = [ + "alloy-sol-types", + "anyhow", + "ethers", + "ethers-solc", + "hex-literal", + "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 ea3a0a87..a131e8ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "chains/arbitrum/testing/rosetta-testing-arbitrum", "rosetta-utils", "chains/polygon/rosetta-testing-polygon", + "chains/binance", "chains/avalanche", ] resolver = "2" diff --git a/chains/binance/Cargo.toml b/chains/binance/Cargo.toml new file mode 100644 index 00000000..6e59d2b6 --- /dev/null +++ b/chains/binance/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rosetta-testing-binance" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/analog-labs/chain-connectors" +description = "binance rosetta test." + +[dependencies] +alloy-sol-types = { version = "0.7" } +anyhow = "1.0" +ethers = { version = "2.0", default-features = true, features = ["abigen", "rustls", "ws"] } +ethers-solc = "2.0" +hex-literal = "0.4" +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/binance/src/lib.rs b/chains/binance/src/lib.rs new file mode 100644 index 00000000..6ba60578 --- /dev/null +++ b/chains/binance/src/lib.rs @@ -0,0 +1,285 @@ +//! # 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-binance --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_client::Wallet; + use rosetta_config_ethereum::{AtBlock, CallResult}; + use rosetta_core::BlockchainClient; + use rosetta_server_ethereum::MaybeWsEthereumClient; + use sha3::Digest; + use std::{collections::BTreeMap, future::Future, path::Path}; + + /// Binance rpc url + const BINANCE_RPC_WS_URL: &str = "ws://127.0.0.1:8546"; + + sol! { + interface TestContract { + event AnEvent(); + function emitEvent() external; + + function identity(bool a) external view returns (bool); + } + } + + /// Run the test in another thread while sending txs to force binance to mine new blocks + /// # Panic + /// Panics if the future panics + async fn run_test + Send + 'static>(future: Fut) { + // Guarantee that only one test is incrementing blocks at a time + static LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + + // Run the test in another thread + let test_handler: tokio::task::JoinHandle<()> = tokio::spawn(future); + + // Acquire Lock + let guard = LOCK.lock().await; + + // Check if the test is finished after acquiring the lock + if test_handler.is_finished() { + // Release lock + drop(guard); + + // Now is safe to panic + if let Err(err) = test_handler.await { + std::panic::resume_unwind(err.into_panic()); + } + return; + } + + // Now is safe to panic + if let Err(err) = test_handler.await { + // Resume the panic on the main task + std::panic::resume_unwind(err.into_panic()); + } + } + + #[tokio::test] + async fn network_status() { + run_test(async move { + let client = MaybeWsEthereumClient::new("binance", "dev", BINANCE_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("binance", "dev", BINANCE_RPC_WS_URL, None) + .await + .expect("Error creating BinanceClient"); + let wallet = + Wallet::from_config(client.config().clone(), BINANCE_RPC_WS_URL, None, None) + .await + .unwrap(); + let value = 10 * u128::pow(10, client.config().currency_decimals); + let _ = wallet.faucet(value).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("binance", "dev", BINANCE_RPC_WS_URL, None) + .await + .expect("Error creating BinanceClient"); + 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(), BINANCE_RPC_WS_URL, None, None) + .await + .unwrap(); + let bob = Wallet::from_config(client.config().clone(), BINANCE_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).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("binance", "dev", BINANCE_RPC_WS_URL, None) + .await + .expect("Error creating BinanceClient"); + let faucet = 10 * u128::pow(10, client.config().currency_decimals); + let wallet = + Wallet::from_config(client.config().clone(), BINANCE_RPC_WS_URL, None, None) + .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().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("binance", "dev", BINANCE_RPC_WS_URL, None) + .await + .expect("Error creating BinanceClient"); + let faucet = 10 * u128::pow(10, client.config().currency_decimals); + let wallet = + Wallet::from_config(client.config().clone(), BINANCE_RPC_WS_URL, None, None) + .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().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/chains/ethereum/config/src/lib.rs b/chains/ethereum/config/src/lib.rs index 4330fe03..6f3178e4 100644 --- a/chains/ethereum/config/src/lib.rs +++ b/chains/ethereum/config/src/lib.rs @@ -223,7 +223,7 @@ pub fn polygon_config(network: &str) -> anyhow::Result { /// # Errors /// Returns `Err` if the network is not supported pub fn arbitrum_config(network: &str) -> anyhow::Result { - // All available networks are listed here: + // All available networks in arbitrum are listed here: let (network, bip44_id, is_dev) = match network { "dev" => ("dev", 1, true), "goerli" => ("goerli", 1, true), @@ -233,6 +233,21 @@ pub fn arbitrum_config(network: &str) -> anyhow::Result { Ok(evm_config("arbitrum", network, "ARB", bip44_id, is_dev)) } +/// Retrieve the [`BlockchainConfig`] from the provided binance `network` +/// +/// # Errors +/// Returns `Err` if the network is not supported +pub fn binance_config(network: &str) -> anyhow::Result { + // All available networks in binance are listed here: + let (network, bip44_id, is_dev) = match network { + "dev" => ("dev", 1, true), + "testnet" => ("testnet", 97, true), + "mainnet" => ("mainnet", 56, false), + _ => anyhow::bail!("unsupported network: {}", network), + }; + Ok(evm_config("binance", network, "bnb", bip44_id, is_dev)) +} + /// Retrieve the [`BlockchainConfig`] from the provided avalanche `network` /// /// # Errors @@ -273,6 +288,11 @@ pub fn config(network: &str) -> anyhow::Result { "arbitrum" => return arbitrum_config("mainnet"), "arbitrum-goerli" => return arbitrum_config("goerli"), + // Binance + "binance-dev" => return binance_config("dev"), + "binance" => return binance_config("mainnet"), + "testnet" => return binance_config("testnet"), + // Avalanche "avalanche-local" => return avalanche_config("dev"), "avalanche" => return avalanche_config("mainnet"), diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index 360386af..0599d77f 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -51,7 +51,7 @@ pub enum MaybeWsEthereumClient { impl MaybeWsEthereumClient { /// Creates a new ethereum client from `network` and `addr`. - /// Supported blockchains are `ethereum`, `polygon`, `arbitrum` and avalanche. + /// Supported blockchains are `ethereum`, `polygon`, `arbitrum`, binance and avalanche. /// /// # Errors /// Will return `Err` when the network is invalid, or when the provided `addr` is unreacheable. @@ -65,6 +65,7 @@ impl MaybeWsEthereumClient { "ethereum" => rosetta_config_ethereum::config(network)?, "polygon" => rosetta_config_ethereum::polygon_config(network)?, "arbitrum" => rosetta_config_ethereum::arbitrum_config(network)?, + "binance" => rosetta_config_ethereum::binance_config(network)?, "avalanche" => rosetta_config_ethereum::avalanche_config(network)?, blockchain => anyhow::bail!("unsupported blockchain: {blockchain}"), }; diff --git a/chains/polygon/rosetta-testing-polygon/src/lib.rs b/chains/polygon/rosetta-testing-polygon/src/lib.rs index 5a99f616..64bf1ad9 100644 --- a/chains/polygon/rosetta-testing-polygon/src/lib.rs +++ b/chains/polygon/rosetta-testing-polygon/src/lib.rs @@ -126,7 +126,7 @@ mod tests { run_test(async move { let client = MaybeWsEthereumClient::new("polygon", "dev", POLYGON_RPC_WS_URL, None) .await - .expect("Error creating ArbitrumClient"); + .expect("Error creating PolygonClient"); let wallet = Wallet::from_config(client.config().clone(), POLYGON_RPC_WS_URL, None, None) .await @@ -145,7 +145,7 @@ mod tests { run_test(async move { let client = MaybeWsEthereumClient::new("polygon", "dev", POLYGON_RPC_WS_URL, None) .await - .expect("Error creating ArbitrumClient"); + .expect("Error creating PolygonClient"); let faucet = 100 * u128::pow(10, client.config().currency_decimals); let value = u128::pow(10, client.config().currency_decimals); let alice = @@ -207,7 +207,7 @@ mod tests { run_test(async move { let client = MaybeWsEthereumClient::new("polygon", "dev", POLYGON_RPC_WS_URL, None) .await - .expect("Error creating ArbitrumClient"); + .expect("Error creating PolygonClient"); let faucet = 10 * u128::pow(10, client.config().currency_decimals); let wallet = Wallet::from_config(client.config().clone(), POLYGON_RPC_WS_URL, None, None) @@ -251,7 +251,7 @@ mod tests { run_test(async move { let client = MaybeWsEthereumClient::new("polygon", "dev", POLYGON_RPC_WS_URL, None) .await - .expect("Error creating ArbitrumClient"); + .expect("Error creating PolygonClient"); let faucet = 10 * u128::pow(10, client.config().currency_decimals); let wallet = Wallet::from_config(client.config().clone(), POLYGON_RPC_WS_URL, None, None) diff --git a/rosetta-client/src/client.rs b/rosetta-client/src/client.rs index 02668b8f..21ee6d54 100644 --- a/rosetta-client/src/client.rs +++ b/rosetta-client/src/client.rs @@ -54,6 +54,10 @@ impl GenericClient { let client = EthereumClient::new("arbitrum", network, url, private_key).await?; Self::Ethereum(client) }, + Blockchain::Binance => { + let client = EthereumClient::new("binance", network, url, private_key).await?; + Self::Ethereum(client) + }, Blockchain::Avalanche => { let client = EthereumClient::new("avalanche", network, url, private_key).await?; Self::Ethereum(client) @@ -79,7 +83,10 @@ impl GenericClient { ) -> Result { let blockchain = Blockchain::from_str(config.blockchain)?; Ok(match blockchain { - Blockchain::Ethereum | Blockchain::Polygon | Blockchain::Arbitrum | Blockchain::Avalanche => { + Blockchain::Ethereum | + Blockchain::Polygon | + Blockchain::Arbitrum | + Blockchain::Binance | 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 ca854ea4..45aa7579 100644 --- a/rosetta-client/src/lib.rs +++ b/rosetta-client/src/lib.rs @@ -47,6 +47,8 @@ pub enum Blockchain { Polygon, /// Arbitrum Arbitrum, + /// Binance + Binance, /// Avalanche Avalanche } @@ -65,6 +67,7 @@ impl std::str::FromStr for Blockchain { "wococo" => Self::Wococo, "polygon" => Self::Polygon, "arbitrum" => Self::Arbitrum, + "binance" => Self::Binance, "avalanche" => Self::Avalanche, _ => anyhow::bail!("unsupported blockchain {}", blockchain), }) diff --git a/rosetta-client/src/tx_builder.rs b/rosetta-client/src/tx_builder.rs index 0686ff13..bc9733ad 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" | "avalanche" => { + "ethereum" | "polygon" | "arbitrum" | "binance" | "avalanche" => { Self::Ethereum(rosetta_tx_ethereum::EthereumTransactionBuilder) }, "polkadot" | "westend" | "rococo" => { diff --git a/scripts/check.sh b/scripts/check.sh index c8283a3d..59dc6eb9 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -137,7 +137,8 @@ if [[ "${RUN_TESTS}" == "1" ]]; then --exclude rosetta-server-ethereum \ --exclude rosetta-server-polkadot \ --exclude rosetta-client \ - --exclude rosetta-testing-arbitrum + --exclude rosetta-testing-arbitrum \ + --exclude rosetta-testing-binance # cargo test --locked --all-features --workspace exec_cmd 'cleanup docker' "${SCRIPT_DIR}/reset_docker.sh" fi