diff --git a/Cargo.toml b/Cargo.toml index 3916b1d..b240fff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,11 @@ futures-util = "0.3" async-trait = "0.1" # Ethereum -ethers = "2.0.14" -ethers-contract-derive = "2.0" -ethabi = "18.0.0" -ethabi-derive = "16.0.0" -ethereum-types = "0.14" jsonrpc-core = "18.0.0" web3 = { version = "0.19" } ethers-providers = "2" +alloy = { version = "0.5.4", features = ["full", "json-rpc", "sol-types"] } +alloy-sol-types = "0.8.9" # Crypto k256 = { version = "0.13" } @@ -32,6 +29,7 @@ cipher = "0.4" # Web reqwest = { version = "0.12", features = ["rustls-tls", "json", "deflate", "stream", "multipart"] } +url = { version = "2.5.2", features = ["serde"] } # SerDe serde = { version = "1.0", features = ["derive"] } @@ -66,6 +64,6 @@ clap = { version = "4.5", features = ["derive"] } [dev-dependencies] assert_matches = "1.5" -[patch.crates-io.ethers] -git = "https://github.com/ZelionD/ethers-rs.git" -branch = "feature/parse-tuple-keyword-in-func-params" +[patch.crates-io.alloy-sol-type-parser] +git = "https://github.com/ZelionD/core.git" +branch = "feature/parse-full-signature-tuples" diff --git a/config/default.json b/config/default.json index 6ef2136..c398ece 100644 --- a/config/default.json +++ b/config/default.json @@ -4,21 +4,21 @@ "main": { "name": "main", "domain": "ambrosus.io", - "rpc": "https://network.ambrosus.io", + "rpc": "https://rpc.airdao.io", "chainspec": "https://chainspec.ambrosus.io", "explorerUrl": "https://airdao.io" }, "test": { "name": "test", "domain": "ambrosus-test.io", - "rpc": "https://network.ambrosus-test.io", + "rpc": "https://testnet-rpc.airdao.io", "chainspec": "https://chainspec.ambrosus-test.io", "explorerUrl": "https://testnet.airdao.io" }, "dev": { "name": "dev", "domain": "ambrosus-dev.io", - "rpc": "https://network.ambrosus-dev.io", + "rpc": "https://devnet-rpc.airdao.io", "chainspec": "https://chainspec.ambrosus-dev.io", "explorerUrl": "https://devnet.airdao.io" } diff --git a/src/config.rs b/src/config.rs index eeaa5ec..c3c7fdf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,7 @@ pub struct Config { #[serde(rename_all = "camelCase")] pub struct Network { pub domain: String, - pub rpc: String, + pub rpc: reqwest::Url, pub chainspec: String, pub explorer_url: String, pub name: String, diff --git a/src/contract.rs b/src/contract.rs index 901f3d0..64ae8ba 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -1,17 +1,22 @@ -use ethabi::{Contract, Function}; -use ethereum_types::Address; -use ethers::abi::HumanReadableParser; -use serde::Deserialize; +use alloy::{ + json_abi::{Function, JsonAbi as Contract}, + primitives::Address, +}; +use anyhow::anyhow; +use serde::{de, Deserialize}; #[derive(Debug)] pub struct EthContract { pub address: Address, - inner: ethers::abi::Contract, + pub inner: Contract, } impl EthContract { pub fn function(&self, name: &str) -> anyhow::Result<&Function> { - self.inner.function(name).map_err(anyhow::Error::from) + self.inner + .function(name) + .and_then(|functions| functions.first()) + .ok_or_else(|| anyhow!("Function {name} not found in contract abi!")) } } @@ -21,48 +26,15 @@ impl<'de> Deserialize<'de> for EthContract { D: serde::Deserializer<'de>, { #[derive(Deserialize)] - struct Inner { + struct Inner<'a> { address: Address, - abi: Vec, + #[serde(borrow)] + abi: Vec<&'a str>, } let inner = Inner::deserialize(deserializer)?; - let mut contract_abi = Contract::default(); - - // Workaround due to issues in ethers parser. Ignore errors - inner.abi.iter().for_each(|input| { - if input.starts_with("constructor ") { - contract_abi.constructor = HumanReadableParser::parse_constructor(input).ok(); - } else if input.starts_with("function ") { - if let Ok(func) = HumanReadableParser::parse_function(input) { - contract_abi - .functions - .entry(func.name.clone()) - .or_default() - .push(func); - } - } else if input.starts_with("event ") { - if let Ok(event) = HumanReadableParser::parse_event(input) { - contract_abi - .events - .entry(event.name.clone()) - .or_default() - .push(event); - } - } else if input.starts_with("error ") { - if let Ok(error) = HumanReadableParser::parse_error(input) { - contract_abi - .errors - .entry(error.name.clone()) - .or_default() - .push(error); - } - } else if input.starts_with("fallback(") { - contract_abi.fallback = true; - } else if input.starts_with("receive(") { - contract_abi.receive = true; - } - }); + let contract_abi = Contract::parse(inner.abi.into_iter()) + .map_err(|e| de::Error::custom(format!("Failed to deserialize contract abi: {e:?}")))?; Ok(Self { address: inner.address, diff --git a/src/error.rs b/src/error.rs index 7209127..9c01666 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,21 +29,24 @@ pub enum AppError { /// Config error #[error("Config error: {0}")] Config(#[from] config::ConfigError), - /// Web3 error - #[error("Web3 error: {0}")] - Web3(#[from] web3::Error), /// Regex error #[error("RegExp error: {0}")] Regexp(#[from] regex::Error), - /// Ethers crate parse error - #[error("Ethers abi parse error: {0}")] - EthersParse(#[from] ethers::abi::ParseError), - /// Ethers crate abi error - #[error("Ethers abi error: {0}")] - EthersAbi(#[from] ethers::abi::Error), - /// Ethers crate invalid output type error - #[error("Ethers invalid output type error: {0}")] - EthersOutputType(#[from] ethers::abi::InvalidOutputType), + /// Alloy abi error + #[error("Alloy abi error: {0}")] + AlloyAbi(#[from] alloy::dyn_abi::Error), + /// Alloy types error + #[error("Alloy types error: {0}")] + AlloyTypes(#[from] alloy::sol_types::Error), + /// Alloy rpc transport error + #[error("Alloy rpc transport error: {0}")] + AlloyRpcTransport(#[from] alloy::transports::TransportError), + /// Alloy contract error + #[error("Alloy contract error: {0}")] + AlloyContract(#[from] alloy::contract::Error), + /// Url parse error + #[error("Url parse error: {0}")] + UrlParse(#[from] url::ParseError), /// Generic #[error("{0:#}")] Anyhow(#[from] anyhow::Error), diff --git a/src/main.rs b/src/main.rs index 1266dca..8a44580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ pub mod setup; pub mod state; pub mod utils; +use alloy::{network::AnyNetwork, providers::ProviderBuilder}; use anyhow::anyhow; use clap::{Parser, Subcommand}; use console::style; @@ -82,21 +83,28 @@ async fn run(config: &Config) -> Result<(), AppError> { cliclack::log::step(MessageType::DockerStarted)?; - let web3_client_remote = web3::Web3::new(web3::transports::Http::new(&setup.network.rpc)?); - let web3_client_local = web3::Web3::new(web3::transports::Http::new( - std::env::var("PARITY_URL") - .as_deref() - .unwrap_or("http://127.0.0.1:8545"), - )?); + let provider_remote = ProviderBuilder::new() + .with_recommended_fillers() + .network::() + .on_http(setup.network.rpc.clone()); + let provider_local = ProviderBuilder::new() + .with_recommended_fillers() + .network::() + .on_http( + std::env::var("PARITY_URL") + .as_deref() + .unwrap_or("http://127.0.0.1:8545") + .parse()?, + ); let mut check_status = - CheckStatusPhase::new(web3_client_remote.clone(), &setup.network, setup.address).await?; + CheckStatusPhase::new(provider_remote.clone(), &setup.network, setup.address).await?; check_status.run().await?; let mut actions_menu = ActionsMenuPhase::new( config.discord_webhook_url.clone(), - web3_client_remote, - web3_client_local, + provider_remote, + provider_local, ); loop { if actions_menu.quit { diff --git a/src/messages.rs b/src/messages.rs index 331755d..7e90208 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,5 +1,5 @@ +use alloy::primitives::Address; use chrono::{DateTime, Utc}; -use ethereum_types::Address; use std::{net::IpAddr, time::Duration}; use strum_macros::Display; diff --git a/src/phases/actions_menu.rs b/src/phases/actions_menu.rs index c4f73fe..d14b8a1 100644 --- a/src/phases/actions_menu.rs +++ b/src/phases/actions_menu.rs @@ -1,13 +1,19 @@ +use alloy::{ + eips::{BlockId, BlockNumberOrTag}, + network::{BlockResponse, HeaderResponse, Network}, + primitives::U256, + providers::{ + fillers::{FillProvider, TxFiller}, + Provider, + }, + rpc::types::{BlockTransactionsKind, SyncStatus}, + transports::Transport, +}; use anyhow::anyhow; use chrono::{DateTime, Utc}; -use ethereum_types::U256; use futures_util::{future::BoxFuture, FutureExt}; use serde::Deserialize; use std::path::PathBuf; -use web3::{ - types::{Block, BlockId, BlockNumber, SyncState}, - Transport, Web3, -}; use super::Phase; use crate::{ @@ -18,12 +24,16 @@ use crate::{ }; use messages::MessageType; -pub struct ActionsMenuPhase -where - ::Out: Send, +pub struct ActionsMenuPhase< + F, + P: Provider + Send + Sync + Clone, + T: Transport + Clone, + N: Network + Clone, +> where + F: TxFiller, { - web3_client_remote: Web3, - web3_client_local: Web3, + provider_remote: FillProvider, + provider_local: FillProvider, client: reqwest::Client, discord_webhook_url: String, pub quit: bool, @@ -47,41 +57,43 @@ struct DiscordError { message: String, } -impl ActionsMenuPhase +impl + Send + Sync + Clone, T: Transport + Clone, N: Network + Clone> + ActionsMenuPhase where - ::Out: Send, + F: TxFiller, { pub fn new( discord_webhook_url: String, - web3_client_remote: Web3, - web3_client_local: Web3, + provider_remote: FillProvider, + provider_local: FillProvider, ) -> Self { Self { quit: false, discord_webhook_url, client: reqwest::Client::new(), - web3_client_remote, - web3_client_local, + provider_remote, + provider_local, } } async fn check_sync(&self) -> Result<(), AppError> { - match self.web3_client_local.eth().syncing().await? { - SyncState::Syncing(state) => { + match self.provider_local.syncing().await? { + SyncStatus::Info(info) => { cliclack::note( "Sync check", MessageType::Syncing { - progress: state + progress: info .current_block - .saturating_sub(state.starting_block) + .saturating_sub(info.starting_block) .saturating_mul(U256::from(100)) - .checked_div(state.highest_block.saturating_sub(state.starting_block)) + .checked_div(info.highest_block.saturating_sub(info.starting_block)) .unwrap_or(U256::from(100)) - .as_u64(), + .try_into() + .map_err(anyhow::Error::from)?, }, )?; } - SyncState::NotSyncing => { + SyncStatus::None => { cliclack::note("Sync check", MessageType::NotSyncing)?; } } @@ -91,18 +103,17 @@ where async fn check_fork(&self) -> Result { match self - .web3_client_local - .eth() - .block(BlockId::Number(BlockNumber::Latest)) + .provider_local + .get_block( + BlockId::Number(BlockNumberOrTag::Latest), + BlockTransactionsKind::Hashes, + ) .await? { - Some(Block { - number: Some(block_number), - hash, - .. - }) if matches!( - self.web3_client_remote.eth().block(BlockId::Number(BlockNumber::Number(block_number))).await?, - Some(remote_block) if remote_block.hash == hash) => + Some(block) + if matches!( + self.provider_remote.get_block(BlockId::Number(BlockNumberOrTag::Number(block.header().number())), BlockTransactionsKind::Hashes).await?, + Some(remote_block) if remote_block.header().hash() == block.header().hash()) => { Ok(MessageType::NotForked) } @@ -246,9 +257,10 @@ where } } -impl Phase for ActionsMenuPhase +impl + Send + Sync + Clone, T: Transport + Clone, N: Network + Clone> Phase + for ActionsMenuPhase where - ::Out: Send, + F: TxFiller, { fn run(&mut self) -> BoxFuture<'_, Result<(), error::AppError>> { async { diff --git a/src/phases/check_status.rs b/src/phases/check_status.rs index c438b2d..75ee621 100644 --- a/src/phases/check_status.rs +++ b/src/phases/check_status.rs @@ -1,15 +1,22 @@ +use alloy::{ + contract::CallBuilder, + dyn_abi::{DynSolValue, JsonAbiExt}, + json_abi::Function, + primitives::{Address, U256}, + providers::{ + fillers::{FillProvider, TxFiller}, + Network, Provider, + }, + sol_types::{sol, SolType, SolValue}, + transports::Transport, +}; use anyhow::anyhow; use chrono::Utc; -use ethabi::Function; -use ethereum_types::{Address, U256}; -use ethers::abi::{Detokenize, Tokenize}; -use ethers_contract_derive::EthAbiType; use futures_util::{future::BoxFuture, FutureExt}; use std::{collections::HashMap, time::Duration}; -use web3::{types::CallRequest, Transport, Web3}; use super::Phase; -use crate::{config::Network, contract::EthContract, error::AppError, messages}; +use crate::{config, contract::EthContract, error::AppError, messages}; use messages::MessageType; const DEPLOYMENTS_JSON: [(u64, &str); 3] = [ @@ -27,26 +34,31 @@ const DEPLOYMENTS_JSON: [(u64, &str); 3] = [ ), ]; -pub struct CheckStatusPhase -where - ::Out: Send, +pub struct CheckStatusPhase< + F, + P: Provider + Send + Sync + Clone, + T: Transport + Clone, + N: Network + Clone, +> where + F: TxFiller, { - web3_client: Web3, + provider: FillProvider, contracts: HashMap, node_addr: Address, explorer_url: String, } -impl CheckStatusPhase +impl + Send + Sync + Clone, T: Transport + Clone, N: Network + Clone> + CheckStatusPhase where - ::Out: Send, + F: TxFiller, { pub async fn new( - web3_client: Web3, - network: &Network, + provider: FillProvider, + network: &config::Network, node_addr: Address, ) -> Result { - let chain_id = web3_client.eth().chain_id().await?.as_u64(); + let chain_id = provider.get_chain_id().await?; let mut deployments = DEPLOYMENTS_JSON .iter() .filter_map(|(chain_id, json_text)| { @@ -58,7 +70,7 @@ where }) .collect::>(); Ok(Self { - web3_client, + provider, contracts: deployments.remove(&chain_id).ok_or_else(|| { anyhow!( "Unable to find deployment information for chain id `{}`", @@ -70,46 +82,35 @@ where }) } - async fn query( + async fn query( &self, contract: Address, function: &Function, - params: P, - ) -> Result { - let input = function.encode_input(¶ms.into_tokens())?; - - let output = self - .web3_client - .eth() - .call( - CallRequest { - from: None, - to: Some(contract), - gas: None, - gas_price: None, - value: None, - data: Some(input.into()), - ..Default::default() - }, - None, - ) + params: &[DynSolValue], + ) -> Result + where + R: From<<::SolType as SolType>::RustType>, + { + let input = function.abi_encode_input(params)?; + + let output = CallBuilder::new_raw(&self.provider, input.into()) + .to(contract) + .call() .await?; - let decoded = function.decode_output(&output.0)?; - - ::from_tokens(decoded).map_err(AppError::from) + ::abi_decode(&output.0, true).map_err(AppError::from) } - #[allow(unused)] - async fn query_by_fn_signature( - &self, - contract: Address, - signature: &str, - params: P, - ) -> Result { - let eth_fn = ethers::abi::AbiParser::default().parse_function(signature)?; - self.query(contract, ð_fn, params).await - } + // #[allow(unused)] + // async fn query_by_fn_signature( + // &self, + // contract: Address, + // signature: &str, + // params: P, + // ) -> Result { + // let eth_fn = ethers::abi::AbiParser::default().parse_function(signature)?; + // self.query(contract, ð_fn, params).await + // } async fn get_stake(&self, node_addr: Address) -> Result { let contract = self @@ -117,8 +118,12 @@ where .get("ServerNodesManager") .ok_or_else(|| anyhow!("Unable to find contract `ServerNodesManager` abi"))?; - self.query(contract.address, contract.function("stakes")?, node_addr) - .await + self.query( + contract.address, + contract.function("stakes")?, + &[node_addr.into()], + ) + .await } async fn get_withdraw_lock_id(&self, node_addr: Address) -> Result { @@ -130,7 +135,7 @@ where self.query( contract.address, contract.function("lockedWithdraws")?, - node_addr, + &[node_addr.into()], ) .await } @@ -141,8 +146,12 @@ where .get("LockKeeper") .ok_or_else(|| anyhow!("Unable to find contract `LockKeeper` abi"))?; - self.query(contract.address, contract.function("getLock")?, lock_id) - .await + self.query( + contract.address, + contract.function("getLock")?, + &[lock_id.into()], + ) + .await } async fn is_onboarded(&self, node_addr: Address) -> Result { @@ -151,10 +160,10 @@ where .get("ValidatorSet") .ok_or_else(|| anyhow!("Unable to find contract `ValidatorSet` abi"))?; - self.query::( + self.query::( contract.address, contract.function("getNodeStake")?, - node_addr, + &[node_addr.into()], ) .await .map(|stake_val| !stake_val.is_zero()) @@ -169,7 +178,7 @@ where self.query( contract.address, contract.function("onboardingDelay")?, - node_addr, + &[node_addr.into()], ) .await } @@ -191,20 +200,23 @@ where node_addr: Address, stake: &Stake, ) -> Result { - let onboarding_delay = self.get_onboarding_delay(node_addr).await?; let now = Utc::now(); - let seconds_to_wait = onboarding_delay - .as_u64() - .saturating_sub(now.timestamp() as u64) - .saturating_sub(stake.timestamp_stake.as_u64()); + let seconds_to_wait = u64::try_from( + self.get_onboarding_delay(node_addr) + .await? + .saturating_sub(stake.timestamp_stake), + ) + .map_err(anyhow::Error::from)? + .saturating_sub(now.timestamp() as u64); Ok(Duration::from_secs(seconds_to_wait)) } } -impl Phase for CheckStatusPhase +impl + Send + Sync + Clone, T: Transport + Clone, N: Network + Clone> Phase + for CheckStatusPhase where - ::Out: Send, + F: TxFiller, { fn run(&mut self) -> BoxFuture<'_, Result<(), AppError>> { async { @@ -246,32 +258,32 @@ where } } -#[derive(Debug, EthAbiType)] -pub struct StakeInfo { - pub amount: U256, - pub staking_contract: Address, - pub is_always_top: bool, -} +sol! { + struct StakeInfo { + uint256 amount; + address staking_contract; + bool is_always_top; + } -#[derive(Debug, EthAbiType)] -pub struct Stake { - stake: U256, - timestamp_stake: U256, - owner_address: Address, - rewards_address: Address, -} + #[derive(Debug)] + struct Stake { + uint256 stake; + uint256 timestamp_stake; + address owner_address; + address rewards_address; + } -#[derive(Debug, EthAbiType)] -pub struct Lock { - locker: Address, - receiver: Address, - token: Address, - first_unlock_time: u64, - unlock_period: u64, - total_claims: u64, - times_claimed: u64, - interval_amount: U256, - description: String, + struct Lock { + address locker; + address receiver; + address token; + uint64 first_unlock_time; + uint64 unlock_period; + uint64 total_claims; + uint64 times_claimed; + uint256 interval_amount; + string description; + } } #[derive(Debug)] diff --git a/src/setup/docker_compose_file.rs b/src/setup/docker_compose_file.rs index 72f248f..c5ed503 100644 --- a/src/setup/docker_compose_file.rs +++ b/src/setup/docker_compose_file.rs @@ -1,5 +1,5 @@ +use alloy::primitives::Address; use anyhow::anyhow; -use ethereum_types::Address; use std::path::PathBuf; use yaml_rust2::{Yaml, YamlLoader}; diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 496bd4d..7cf8ef5 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -3,8 +3,8 @@ pub mod keystore; mod parity_config_file; pub mod utils; +use alloy::primitives::Address; use anyhow::anyhow; -use ethereum_types::Address; use futures::StreamExt; use k256::ecdsa::SigningKey; use rand::rngs::OsRng; diff --git a/src/setup/parity_config_file.rs b/src/setup/parity_config_file.rs index bc0d9c6..2f93081 100644 --- a/src/setup/parity_config_file.rs +++ b/src/setup/parity_config_file.rs @@ -1,4 +1,4 @@ -use ethereum_types::Address; +use alloy::primitives::Address; use std::{net::IpAddr, path::PathBuf}; use crate::error::AppError; diff --git a/src/state.rs b/src/state.rs index 76fe3aa..f2d9ff2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,6 @@ +use alloy::primitives::Address; use config::ConfigError; -use ethereum_types::Address; -use ethers::core::k256::ecdsa::SigningKey; +use k256::ecdsa::SigningKey; use serde::{Deserialize, Serialize}; use std::{ fs::File, diff --git a/src/utils/debug_info.rs b/src/utils/debug_info.rs index da1348f..bdc9318 100644 --- a/src/utils/debug_info.rs +++ b/src/utils/debug_info.rs @@ -1,5 +1,5 @@ +use alloy::primitives::Address; use chrono::Utc; -use ethereum_types::Address; use std::{fmt::write, path::PathBuf}; use super::exec; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e9e9a6c..69fd9e0 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,8 +3,8 @@ pub mod debug_info; pub mod exec; pub mod logger; +use alloy::primitives::Address; use backtrace::Backtrace; -use ethereum_types::{Address, H160}; use log::error; use serde::{de, Deserialize}; use sha3::{Digest, Keccak256}; @@ -102,8 +102,8 @@ pub mod secp256k1_signing_key_opt_str { } } -pub fn get_eth_address(uncompressed_public_key: &[u8]) -> H160 { - H160::from_slice( +pub fn get_eth_address(uncompressed_public_key: &[u8]) -> Address { + Address::from_slice( &Keccak256::new_with_prefix(&uncompressed_public_key[1..]) .finalize() .as_slice()[12..],