#[cfg(feature = "evm")]
pub mod evm;
#[cfg(feature = "solana")]
pub mod solana;
use crate::{Error, Result, Signature};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "evm")]
pub use evm::{EvmAdapter, EvmConfig};
#[cfg(feature = "aa")]
pub use evm::aa::{SmartAccountConfig, SmartAccountModule, UserOperation};
#[cfg(feature = "solana")]
pub use solana::{SolanaAdapter, SolanaConfig};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChainId(pub u64);
impl ChainId {
pub const ETHEREUM_MAINNET: ChainId = ChainId(1);
pub const ETHEREUM_SEPOLIA: ChainId = ChainId(11155111);
pub const ARBITRUM_ONE: ChainId = ChainId(42161);
pub const OPTIMISM: ChainId = ChainId(10);
pub const BASE: ChainId = ChainId(8453);
pub const POLYGON: ChainId = ChainId(137);
pub const BSC: ChainId = ChainId(56);
pub const AVALANCHE: ChainId = ChainId(43114);
pub const SOLANA_MAINNET: ChainId = ChainId(101);
pub const SOLANA_DEVNET: ChainId = ChainId(102);
pub const SOLANA_TESTNET: ChainId = ChainId(103);
pub fn name(&self) -> &'static str {
match self.0 {
1 => "Ethereum Mainnet",
11155111 => "Ethereum Sepolia",
42161 => "Arbitrum One",
10 => "Optimism",
8453 => "Base",
137 => "Polygon",
56 => "BNB Smart Chain",
43114 => "Avalanche C-Chain",
101 => "Solana Mainnet",
102 => "Solana Devnet",
103 => "Solana Testnet",
_ => "Unknown Chain",
}
}
pub fn is_solana(&self) -> bool {
matches!(self.0, 101 | 102 | 103)
}
pub fn is_evm(&self) -> bool {
!self.is_solana()
}
}
impl fmt::Display for ChainId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name(), self.0)
}
}
impl From<u64> for ChainId {
fn from(id: u64) -> Self {
ChainId(id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Balance {
pub raw: String,
pub formatted: String,
pub symbol: String,
pub decimals: u8,
}
impl Balance {
pub fn new(raw: impl Into<String>, decimals: u8, symbol: impl Into<String>) -> Self {
let raw_str = raw.into();
let symbol_str = symbol.into();
let formatted = Self::format_balance(&raw_str, decimals);
Self {
raw: raw_str,
formatted,
symbol: symbol_str,
decimals,
}
}
fn format_balance(raw: &str, decimals: u8) -> String {
let raw_value: u128 = raw.parse().unwrap_or(0);
if raw_value == 0 {
return "0".to_string();
}
let divisor = 10u128.pow(decimals as u32);
let whole = raw_value / divisor;
let fraction = raw_value % divisor;
if fraction == 0 {
whole.to_string()
} else {
let fraction_str = format!("{:0>width$}", fraction, width = decimals as usize);
let trimmed = fraction_str.trim_end_matches('0');
format!("{}.{}", whole, trimmed)
}
}
pub fn is_zero(&self) -> bool {
self.raw == "0" || self.raw.is_empty()
}
pub fn raw_value(&self) -> u128 {
self.raw.parse().unwrap_or(0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxParams {
pub from: String,
pub to: String,
pub value: String,
#[serde(default)]
pub data: Option<Vec<u8>>,
#[serde(default)]
pub gas_limit: Option<u64>,
#[serde(default)]
pub nonce: Option<u64>,
#[serde(default)]
pub priority: TxPriority,
}
impl TxParams {
pub fn new(from: impl Into<String>, to: impl Into<String>, value: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
value: value.into(),
data: None,
gas_limit: None,
nonce: None,
priority: TxPriority::Medium,
}
}
pub fn with_data(mut self, data: Vec<u8>) -> Self {
self.data = Some(data);
self
}
pub fn with_gas_limit(mut self, limit: u64) -> Self {
self.gas_limit = Some(limit);
self
}
pub fn with_nonce(mut self, nonce: u64) -> Self {
self.nonce = Some(nonce);
self
}
pub fn with_priority(mut self, priority: TxPriority) -> Self {
self.priority = priority;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TxPriority {
Low,
#[default]
Medium,
High,
Urgent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnsignedTx {
pub chain_id: ChainId,
pub signing_payload: Vec<u8>,
pub raw_tx: Vec<u8>,
pub summary: TxSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxSummary {
pub tx_type: String,
pub from: String,
pub to: String,
pub value: String,
pub estimated_fee: String,
#[serde(default)]
pub details: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedTx {
pub chain_id: ChainId,
pub raw_tx: Vec<u8>,
pub tx_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxHash {
pub hash: String,
pub explorer_url: Option<String>,
}
impl TxHash {
pub fn new(hash: impl Into<String>) -> Self {
Self {
hash: hash.into(),
explorer_url: None,
}
}
pub fn with_explorer_url(mut self, url: impl Into<String>) -> Self {
self.explorer_url = Some(url.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GasPrices {
pub low: GasPrice,
pub medium: GasPrice,
pub high: GasPrice,
pub base_fee: Option<u128>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GasPrice {
pub max_fee: u128,
pub max_priority_fee: u128,
pub estimated_wait_secs: Option<u64>,
}
#[async_trait]
pub trait ChainAdapter: Send + Sync {
fn chain_id(&self) -> ChainId;
fn native_symbol(&self) -> &str;
fn native_decimals(&self) -> u8;
async fn get_balance(&self, address: &str) -> Result<Balance>;
async fn get_nonce(&self, address: &str) -> Result<u64>;
async fn build_transaction(&self, params: TxParams) -> Result<UnsignedTx>;
async fn broadcast(&self, signed_tx: &SignedTx) -> Result<TxHash>;
fn derive_address(&self, public_key: &[u8]) -> Result<String>;
async fn get_gas_prices(&self) -> Result<GasPrices>;
async fn estimate_gas(&self, params: &TxParams) -> Result<u64>;
async fn wait_for_confirmation(&self, tx_hash: &str, timeout_secs: u64) -> Result<TxReceipt>;
fn is_valid_address(&self, address: &str) -> bool;
fn explorer_tx_url(&self, tx_hash: &str) -> Option<String>;
fn explorer_address_url(&self, address: &str) -> Option<String>;
fn finalize_transaction(
&self,
unsigned_tx: &UnsignedTx,
signature: &Signature,
) -> Result<SignedTx>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxReceipt {
pub tx_hash: String,
pub block_number: u64,
pub status: TxStatus,
pub gas_used: Option<u64>,
pub effective_gas_price: Option<u128>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TxStatus {
Success,
Failed,
Pending,
}
#[cfg(feature = "runtime")]
#[derive(Clone)]
pub struct RpcClient {
urls: Vec<String>,
client: reqwest::Client,
current_index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
#[cfg(feature = "runtime")]
impl RpcClient {
pub fn new(urls: Vec<String>) -> Result<Self> {
if urls.is_empty() {
return Err(Error::InvalidConfig("At least one RPC URL required".into()));
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::ChainError(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
urls,
client,
current_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
})
}
fn current_url(&self) -> &str {
let idx = self
.current_index
.load(std::sync::atomic::Ordering::Relaxed);
&self.urls[idx % self.urls.len()]
}
fn rotate_url(&self) {
self.current_index
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub async fn request<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: serde_json::Value,
) -> Result<T> {
let mut last_error = None;
for _ in 0..self.urls.len() {
let url = self.current_url();
match self.make_request(url, method, params.clone()).await {
Ok(result) => return Ok(result),
Err(e) => {
tracing::warn!("RPC request failed on {}: {}", url, e);
last_error = Some(e);
self.rotate_url();
}
}
}
Err(last_error.unwrap_or_else(|| Error::ChainError("All RPC endpoints failed".into())))
}
async fn make_request<T: serde::de::DeserializeOwned>(
&self,
url: &str,
method: &str,
params: serde_json::Value,
) -> Result<T> {
let request_body = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1
});
let response = self
.client
.post(url)
.json(&request_body)
.send()
.await
.map_err(|e| Error::ChainError(format!("RPC request failed: {}", e)))?;
let response_body: serde_json::Value = response
.json()
.await
.map_err(|e| Error::ChainError(format!("Failed to parse RPC response: {}", e)))?;
if let Some(error) = response_body.get("error") {
return Err(Error::ChainError(format!("RPC error: {}", error)));
}
let result = response_body
.get("result")
.ok_or_else(|| Error::ChainError("Missing result in RPC response".into()))?;
serde_json::from_value(result.clone())
.map_err(|e| Error::ChainError(format!("Failed to deserialize result: {}", e)))
}
}
#[cfg(feature = "runtime")]
impl std::fmt::Debug for RpcClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RpcClient")
.field("urls", &self.urls)
.field(
"current_index",
&self
.current_index
.load(std::sync::atomic::Ordering::Relaxed),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_id_names() {
assert_eq!(ChainId::ETHEREUM_MAINNET.name(), "Ethereum Mainnet");
assert_eq!(ChainId::SOLANA_MAINNET.name(), "Solana Mainnet");
assert!(ChainId::ETHEREUM_MAINNET.is_evm());
assert!(ChainId::SOLANA_MAINNET.is_solana());
}
#[test]
fn test_balance_formatting() {
let balance = Balance::new("1000000000000000000", 18, "ETH");
assert_eq!(balance.formatted, "1");
let balance = Balance::new("1500000000000000000", 18, "ETH");
assert_eq!(balance.formatted, "1.5");
let balance = Balance::new("1000000000000000", 18, "ETH");
assert_eq!(balance.formatted, "0.001");
let balance = Balance::new("0", 18, "ETH");
assert_eq!(balance.formatted, "0");
}
#[test]
fn test_tx_params_builder() {
let params = TxParams::new("0xfrom", "0xto", "1.0")
.with_gas_limit(21000)
.with_priority(TxPriority::High);
assert_eq!(params.gas_limit, Some(21000));
assert_eq!(params.priority, TxPriority::High);
}
#[test]
fn test_tx_priority_default() {
let priority = TxPriority::default();
assert_eq!(priority, TxPriority::Medium);
}
}