use std::error::Error;
use serde::{Serialize, Deserialize};
use std::str::FromStr;
use std::fmt;
use std::sync::Arc;
use url::Url;
use thiserror::Error;
use async_trait::async_trait;
use web3::{
Web3,
transports::{Http, WebSocket},
types::{H160, H256, U256, BlockNumber, Block, BlockId, Transaction, TransactionReceipt, CallRequest},
contract::Contract,
Error as Web3Error
};
use ethers::{
core::{
types::{Address, TransactionRequest, Bytes},
abi::Abi,
},
providers::{Provider, Http as EthersHttp, Middleware},
signers::{LocalWallet, Signer},
};
use tokio::sync::Mutex;
use crate::AnyaResult;
#[derive(Error, Debug)]
pub enum ClientError {
#[error("Network error: {0}")]
NetworkError(String),
#[error("RPC error: {0}")]
RpcError(String),
#[error("Contract error: {0}")]
ContractError(String),
#[error("Transaction error: {0}")]
TransactionError(String),
#[error("Encoding error: {0}")]
EncodingError(String),
#[error("Wallet error: {0}")]
WalletError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NetworkType {
Mainnet,
Testnet,
Regtest,
}
impl fmt::Display for NetworkType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NetworkType::Mainnet => write!(f, "mainnet"),
NetworkType::Testnet => write!(f, "testnet"),
NetworkType::Regtest => write!(f, "regtest"),
}
}
}
impl NetworkType {
pub fn chain_id(&self) -> u64 {
match self {
NetworkType::Mainnet => 30, NetworkType::Testnet => 31, NetworkType::Regtest => 33, }
}
pub fn default_node_url(&self) -> &'static str {
match self {
NetworkType::Mainnet => "https://public-node.rsk.co",
NetworkType::Testnet => "https://public-node.testnet.rsk.co",
NetworkType::Regtest => "http://localhost:4444",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
pub network: NetworkType,
pub node_url: String,
pub gas_price_strategy: GasPriceStrategy,
pub tx_confirmation_timeout: u64,
pub tx_confirmation_blocks: u64,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
network: NetworkType::Testnet,
node_url: NetworkType::Testnet.default_node_url().to_string(),
gas_price_strategy: GasPriceStrategy::Standard,
tx_confirmation_timeout: 300, tx_confirmation_blocks: 6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GasPriceStrategy {
Fixed(u64),
Standard,
Fast,
Rapid,
}
impl GasPriceStrategy {
fn apply(&self, base_gas_price: U256) -> U256 {
match self {
GasPriceStrategy::Fixed(price) => U256::from(*price),
GasPriceStrategy::Standard => base_gas_price,
GasPriceStrategy::Fast => base_gas_price * U256::from(15) / U256::from(10),
GasPriceStrategy::Rapid => base_gas_price * U256::from(2),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionResponse {
pub hash: String,
pub block_hash: Option<String>,
pub block_number: Option<u64>,
pub from: String,
pub to: Option<String>,
pub contract_address: Option<String>,
pub value: String,
pub gas_price: String,
pub gas: String,
pub gas_used: Option<String>,
pub nonce: u64,
pub input: String,
pub status: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ContractCallRequest {
pub to: String,
pub data: Vec<u8>,
pub value: Option<u64>,
pub from: Option<String>,
pub gas: Option<u64>,
pub gas_price: Option<u64>,
}
#[async_trait]
pub trait RskClientTrait {
async fn get_block_number(&self) -> Result<u64, ClientError>;
async fn get_block(&self, block_id: &str) -> Result<Block<H256>, ClientError>;
async fn get_transaction(&self, tx_hash: &str) -> Result<Option<Transaction>, ClientError>;
async fn get_transaction_receipt(&self, tx_hash: &str) -> Result<Option<TransactionReceipt>, ClientError>;
async fn get_balance(&self, address: &str, block: Option<BlockNumber>) -> Result<U256, ClientError>;
async fn get_code(&self, address: &str, block: Option<BlockNumber>) -> Result<Bytes, ClientError>;
async fn get_transaction_count(&self, address: &str, block: Option<BlockNumber>) -> Result<U256, ClientError>;
async fn send_raw_transaction(&self, _data: &[u8]) -> Result<H256, ClientError>;
async fn call(&self, request: ContractCallRequest, block: Option<BlockNumber>) -> Result<Bytes, ClientError>;
async fn estimate_gas(&self, request: ContractCallRequest) -> Result<U256, ClientError>;
async fn get_gas_price(&self) -> Result<U256, ClientError>;
async fn wait_for_transaction(&self, tx_hash: H256, timeout: Option<u64>, confirmations: Option<usize>) -> Result<Option<TransactionReceipt>, ClientError>;
}
pub struct RSKClient {
web3: Web3<Http>,
config: ClientConfig,
provider: Provider<EthersHttp>,
contract_cache: Arc<Mutex<std::collections::HashMap<String, Contract<Http>>>>,
}
impl RSKClient {
pub async fn new(config: ClientConfig) -> Result<Self, ClientError> {
let url = Url::parse(&config.node_url)
.map_err(|e| ClientError::ConfigurationError(format!("Invalid node URL: {}", e)))?;
let transport = Http::new(&config.node_url)
.map_err(|e| ClientError::NetworkError(format!("Failed to create HTTP transport: {}", e)))?;
let web3 = Web3::new(transport);
let provider = Provider::<EthersHttp>::try_from(config.node_url.clone())
.map_err(|e| ClientError::ConfigurationError(format!("Failed to create Ethers provider: {}", e)))?;
let _block_number = web3.eth().block_number().await
.map_err(|e| ClientError::NetworkError(format!("Failed to connect to node: {}", e)))?;
Ok(Self {
web3,
config,
provider,
contract_cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
})
}
pub async fn contract(&self, address: &str, abi: &[u8]) -> Result<Contract<Http>, ClientError> {
let mut cache = self.contract_cache.lock().await;
if let Some(contract) = cache.get(address) {
return Ok(contract.clone());
}
let address = H160::from_str(address)
.map_err(|e| ClientError::ValidationError(format!("Invalid contract address: {}", e)))?;
let contract = Contract::from_json(self.web3.eth(), address, abi)
.map_err(|e| ClientError::ContractError(format!("Failed to create contract: {}", e)))?;
cache.insert(address.to_string(), contract.clone());
Ok(contract)
}
pub fn create_wallet(&self, private_key: &str) -> Result<LocalWallet, ClientError> {
let wallet = private_key.parse::<LocalWallet>()
.map_err(|e| ClientError::WalletError(format!("Invalid private key: {}", e)))?;
let wallet = wallet.with_chain_id(self.config.network.chain_id());
Ok(wallet)
}
pub async fn send_transaction<S: Signer>(
&self,
from: &S,
to: &str,
value: Option<U256>,
data: Option<Vec<u8>>,
gas_limit: Option<U256>,
nonce: Option<U256>,
) -> Result<H256, ClientError> {
let to_address = if to.is_empty() {
None } else {
Some(H160::from_str(to)
.map_err(|e| ClientError::ValidationError(format!("Invalid to address: {}", e)))?)
};
let base_gas_price = self.web3.eth().gas_price().await
.map_err(|e| ClientError::RpcError(format!("Failed to get gas price: {}", e)))?;
let gas_price = self.config.gas_price_strategy.apply(base_gas_price);
let nonce = if let Some(n) = nonce {
n
} else {
let from_address = H160::from_slice(from.address().as_bytes());
self.web3.eth().transaction_count(from_address, None).await
.map_err(|e| ClientError::RpcError(format!("Failed to get nonce: {}", e)))?
};
let tx_request = TransactionRequest::new()
.from(from.address())
.to(to_address.map(|addr| Address::from_slice(addr.as_bytes())))
.gas_price(gas_price.as_u128())
.nonce(nonce.as_u64());
let tx_request = if let Some(v) = value {
tx_request.value(v.as_u128())
} else {
tx_request
};
let tx_request = if let Some(d) = data {
tx_request.data(d)
} else {
tx_request
};
let tx_request = if let Some(g) = gas_limit {
tx_request.gas(g.as_u64())
} else {
let gas = self.provider.estimate_gas(&tx_request, None).await
.map_err(|e| ClientError::RpcError(format!("Failed to estimate gas: {}", e)))?;
let gas = gas * 12 / 10;
tx_request.gas(gas)
};
let signed_tx = from.sign_transaction(tx_request).await
.map_err(|e| ClientError::WalletError(format!("Failed to sign transaction: {}", e)))?;
let tx_hash = self.provider.send_raw_transaction(signed_tx).await
.map_err(|e| ClientError::TransactionError(format!("Failed to send transaction: {}", e)))?;
Ok(H256::from_slice(tx_hash.as_bytes()))
}
pub async fn call_contract<S: Signer>(
&self,
from: &S,
contract: &Contract<Http>,
method: &str,
params: Vec<ethers::core::abi::Token>,
value: Option<U256>,
) -> Result<Vec<ethers::core::abi::Token>, ClientError> {
let data = contract.function(method)
.map_err(|e| ClientError::ContractError(format!("Function not found: {}", e)))?
.encode_input(¶ms)
.map_err(|e| ClientError::EncodingError(format!("Failed to encode parameters: {}", e)))?;
let request = CallRequest {
from: Some(H160::from_slice(from.address().as_bytes())),
to: Some(contract.address()),
gas: None,
gas_price: None,
value,
data: Some(web3::types::Bytes(data.clone())),
transaction_type: None,
access_list: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
};
let result = self.web3.eth().call(request, None).await
.map_err(|e| ClientError::RpcError(format!("Call failed: {}", e)))?;
let tokens = contract.function(method)
.map_err(|e| ClientError::ContractError(format!("Function not found: {}", e)))?
.decode_output(&result.0)
.map_err(|e| ClientError::EncodingError(format!("Failed to decode output: {}", e)))?;
Ok(tokens)
}
pub async fn deploy_contract<S: Signer>(
&self,
from: &S,
bytecode: Vec<u8>,
abi: &[u8],
constructor_args: Vec<ethers::core::abi::Token>,
value: Option<U256>,
gas_limit: Option<U256>,
) -> Result<(H256, H160), ClientError> {
let abi: Abi = serde_json::from_slice(abi)
.map_err(|e| ClientError::ContractError(format!("Failed to parse ABI: {}", e)))?;
let mut data = bytecode;
if !constructor_args.is_empty() {
if let Some(constructor) = abi.constructor() {
let encoded = constructor.encode_input(data.as_slice(), &constructor_args)
.map_err(|e| ClientError::EncodingError(format!("Failed to encode constructor arguments: {}", e)))?;
data = encoded;
}
}
let tx_hash = self.send_transaction(
from,
"", value,
Some(data),
gas_limit,
None,
).await?;
let receipt = self.wait_for_transaction(
tx_hash,
Some(self.config.tx_confirmation_timeout),
Some(self.config.tx_confirmation_blocks as usize),
).await?
.ok_or_else(|| ClientError::TransactionError("Transaction not found after timeout".to_string()))?;
let contract_address = receipt.contract_address
.ok_or_else(|| ClientError::ContractError("Contract address not found in receipt".to_string()))?;
Ok((tx_hash, contract_address))
}
}
#[async_trait]
impl RskClientTrait for RSKClient {
async fn get_block_number(&self) -> Result<u64, ClientError> {
self.web3.eth().block_number().await
.map(|n| n.as_u64())
.map_err(|e| ClientError::RpcError(format!("Failed to get block number: {}", e)))
}
async fn get_block(&self, block_id: &str) -> Result<Block<H256>, ClientError> {
let block_id = if block_id.starts_with("0x") {
let hash = H256::from_str(block_id)
.map_err(|e| ClientError::ValidationError(format!("Invalid block hash: {}", e)))?;
BlockId::Hash(hash)
} else {
let num = block_id.parse::<u64>()
.map_err(|e| ClientError::ValidationError(format!("Invalid block number: {}", e)))?;
BlockId::Number(BlockNumber::Number(num.into()))
};
self.web3.eth().block(block_id).await
.map_err(|e| ClientError::RpcError(format!("Failed to get block: {}", e)))?
.ok_or_else(|| ClientError::ValidationError(format!("Block not found: {}", block_id)))
}
async fn get_transaction(&self, tx_hash: &str) -> Result<Option<Transaction>, ClientError> {
let hash = H256::from_str(tx_hash)
.map_err(|e| ClientError::ValidationError(format!("Invalid transaction hash: {}", e)))?;
self.web3.eth().transaction(web3::types::TransactionId::Hash(hash)).await
.map_err(|e| ClientError::RpcError(format!("Failed to get transaction: {}", e)))
}
async fn get_transaction_receipt(&self, tx_hash: &str) -> Result<Option<TransactionReceipt>, ClientError> {
let hash = H256::from_str(tx_hash)
.map_err(|e| ClientError::ValidationError(format!("Invalid transaction hash: {}", e)))?;
self.web3.eth().transaction_receipt(hash).await
.map_err(|e| ClientError::RpcError(format!("Failed to get transaction receipt: {}", e)))
}
async fn get_balance(&self, address: &str, block: Option<BlockNumber>) -> Result<U256, ClientError> {
let address = H160::from_str(address)
.map_err(|e| ClientError::ValidationError(format!("Invalid address: {}", e)))?;
self.web3.eth().balance(address, block).await
.map_err(|e| ClientError::RpcError(format!("Failed to get balance: {}", e)))
}
async fn get_code(&self, address: &str, block: Option<BlockNumber>) -> Result<Bytes, ClientError> {
let address = H160::from_str(address)
.map_err(|e| ClientError::ValidationError(format!("Invalid address: {}", e)))?;
let code = self.web3.eth().code(address, block).await
.map_err(|e| ClientError::RpcError(format!("Failed to get code: {}", e)))?;
Ok(Bytes::from(code.0))
}
async fn get_transaction_count(&self, address: &str, block: Option<BlockNumber>) -> Result<U256, ClientError> {
let address = H160::from_str(address)
.map_err(|e| ClientError::ValidationError(format!("Invalid address: {}", e)))?;
self.web3.eth().transaction_count(address, block).await
.map_err(|e| ClientError::RpcError(format!("Failed to get transaction count: {}", e)))
}
async fn send_raw_transaction(&self, _data: &[u8]) -> Result<H256, ClientError> {
self.web3.eth().send_raw_transaction(web3::types::Bytes(data.to_vec())).await
.map_err(|e| ClientError::TransactionError(format!("Failed to send raw transaction: {}", e)))
}
async fn call(&self, request: ContractCallRequest, block: Option<BlockNumber>) -> Result<Bytes, ClientError> {
let to = H160::from_str(&request.to)
.map_err(|e| ClientError::ValidationError(format!("Invalid to address: {}", e)))?;
let from = if let Some(from) = request.from {
Some(H160::from_str(&from)
.map_err(|e| ClientError::ValidationError(format!("Invalid from address: {}", e)))?)
} else {
None
};
let call_request = CallRequest {
from,
to: Some(to),
gas: request.gas.map(U256::from),
gas_price: request.gas_price.map(U256::from),
value: request.value.map(U256::from),
data: Some(web3::types::Bytes(request.data)),
transaction_type: None,
access_list: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
};
let result = self.web3.eth().call(call_request, block).await
.map_err(|e| ClientError::RpcError(format!("Call failed: {}", e)))?;
Ok(Bytes::from(result.0))
}
async fn estimate_gas(&self, request: ContractCallRequest) -> Result<U256, ClientError> {
let to = if request.to.is_empty() {
None } else {
Some(H160::from_str(&request.to)
.map_err(|e| ClientError::ValidationError(format!("Invalid to address: {}", e)))?)
};
let from = if let Some(from) = request.from {
Some(H160::from_str(&from)
.map_err(|e| ClientError::ValidationError(format!("Invalid from address: {}", e)))?)
} else {
None
};
let call_request = CallRequest {
from,
to,
gas: None,
gas_price: request.gas_price.map(U256::from),
value: request.value.map(U256::from),
data: Some(web3::types::Bytes(request.data)),
transaction_type: None,
access_list: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
};
self.web3.eth().estimate_gas(call_request, None).await
.map_err(|e| ClientError::RpcError(format!("Failed to estimate gas: {}", e)))
}
async fn get_gas_price(&self) -> Result<U256, ClientError> {
self.web3.eth().gas_price().await
.map_err(|e| ClientError::RpcError(format!("Failed to get gas price: {}", e)))
}
async fn wait_for_transaction(&self, tx_hash: H256, timeout: Option<u64>, confirmations: Option<usize>) -> Result<Option<TransactionReceipt>, ClientError> {
let timeout = timeout.unwrap_or(self.config.tx_confirmation_timeout);
let confirmations = confirmations.unwrap_or(self.config.tx_confirmation_blocks as usize);
let start = std::time::Instant::now();
let timeout_duration = std::time::Duration::from_secs(timeout);
loop {
if start.elapsed() > timeout_duration {
return Err(ClientError::TransactionError(format!(
"Timeout waiting for transaction confirmation: {}",
tx_hash
)));
}
match self.web3.eth().transaction_receipt(tx_hash).await {
Ok(Some(receipt)) => {
if let Some(block_number) = receipt.block_number {
let current_block = self.web3.eth().block_number().await
.map_err(|e| ClientError::RpcError(format!("Failed to get block number: {}", e)))?;
let conf = current_block.as_u64().saturating_sub(block_number.as_u64());
if conf >= confirmations as u64 {
return Ok(Some(receipt));
}
}
},
Ok(None) => {
},
Err(e) => {
return Err(ClientError::RpcError(format!("Failed to get transaction receipt: {}", e)));
}
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
}