use super::{ChainAdapter, EvmAdapter};
use crate::{Error, Result, Signature};
use alloy_primitives::{Address, U256};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tiny_keccak::{Hasher, Keccak};
pub const ENTRY_POINT_V06: &str = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
pub const ENTRY_POINT_V07: &str = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartAccountConfig {
pub entry_point: String,
pub account_factory: String,
pub paymaster: Option<String>,
pub use_sponsored_gas: bool,
pub version: EntryPointVersion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum EntryPointVersion {
#[default]
V06,
V07,
}
impl SmartAccountConfig {
pub fn new(entry_point: impl Into<String>, account_factory: impl Into<String>) -> Self {
Self {
entry_point: entry_point.into(),
account_factory: account_factory.into(),
paymaster: None,
use_sponsored_gas: false,
version: EntryPointVersion::V06,
}
}
pub fn with_default_entry_point_v06(account_factory: impl Into<String>) -> Self {
Self::new(ENTRY_POINT_V06, account_factory)
}
pub fn with_default_entry_point_v07(account_factory: impl Into<String>) -> Self {
Self {
entry_point: ENTRY_POINT_V07.to_string(),
account_factory: account_factory.into(),
paymaster: None,
use_sponsored_gas: false,
version: EntryPointVersion::V07,
}
}
pub fn with_paymaster(mut self, paymaster: impl Into<String>) -> Self {
self.paymaster = Some(paymaster.into());
self.use_sponsored_gas = true;
self
}
pub fn with_sponsored_gas(mut self, enabled: bool) -> Self {
self.use_sponsored_gas = enabled;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserOperation {
pub sender: String,
pub nonce: U256,
#[serde(with = "bytes_hex")]
pub init_code: Vec<u8>,
#[serde(with = "bytes_hex")]
pub call_data: Vec<u8>,
pub call_gas_limit: U256,
pub verification_gas_limit: U256,
pub pre_verification_gas: U256,
pub max_fee_per_gas: U256,
pub max_priority_fee_per_gas: U256,
#[serde(with = "bytes_hex")]
pub paymaster_and_data: Vec<u8>,
#[serde(with = "bytes_hex")]
pub signature: Vec<u8>,
}
mod bytes_hex {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("0x{}", hex::encode(bytes)))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let s = s.strip_prefix("0x").unwrap_or(&s);
hex::decode(s).map_err(serde::de::Error::custom)
}
}
impl UserOperation {
pub fn new(sender: impl Into<String>, nonce: U256, call_data: Vec<u8>) -> Self {
Self {
sender: sender.into(),
nonce,
init_code: vec![],
call_data,
call_gas_limit: U256::from(100000),
verification_gas_limit: U256::from(100000),
pre_verification_gas: U256::from(21000),
max_fee_per_gas: U256::ZERO,
max_priority_fee_per_gas: U256::ZERO,
paymaster_and_data: vec![],
signature: vec![],
}
}
pub fn with_init_code(mut self, factory: &str, init_data: Vec<u8>) -> Result<Self> {
let factory_addr = Address::from_str(factory)
.map_err(|e| Error::InvalidConfig(format!("Invalid factory address: {}", e)))?;
let mut init_code = factory_addr.to_vec();
init_code.extend(init_data);
self.init_code = init_code;
Ok(self)
}
pub fn with_gas_limits(
mut self,
call_gas: u64,
verification_gas: u64,
pre_verification_gas: u64,
) -> Self {
self.call_gas_limit = U256::from(call_gas);
self.verification_gas_limit = U256::from(verification_gas);
self.pre_verification_gas = U256::from(pre_verification_gas);
self
}
pub fn with_gas_prices(mut self, max_fee: u128, max_priority_fee: u128) -> Self {
self.max_fee_per_gas = U256::from(max_fee);
self.max_priority_fee_per_gas = U256::from(max_priority_fee);
self
}
pub fn with_paymaster(mut self, paymaster: &str, data: Vec<u8>) -> Result<Self> {
let paymaster_addr = Address::from_str(paymaster)
.map_err(|e| Error::InvalidConfig(format!("Invalid paymaster address: {}", e)))?;
let mut paymaster_and_data = paymaster_addr.to_vec();
paymaster_and_data.extend(data);
self.paymaster_and_data = paymaster_and_data;
Ok(self)
}
pub fn with_signature(mut self, signature: Vec<u8>) -> Self {
self.signature = signature;
self
}
pub fn hash(&self, entry_point: &str, chain_id: u64) -> Result<[u8; 32]> {
let entry_point_addr = Address::from_str(entry_point)
.map_err(|e| Error::InvalidConfig(format!("Invalid EntryPoint address: {}", e)))?;
let packed = self.pack_for_hash()?;
let mut hasher = Keccak::v256();
hasher.update(&packed);
let mut inner_hash = [0u8; 32];
hasher.finalize(&mut inner_hash);
let mut final_hasher = Keccak::v256();
final_hasher.update(&inner_hash);
final_hasher.update(entry_point_addr.as_slice());
let mut chain_id_bytes = [0u8; 32];
chain_id_bytes[24..].copy_from_slice(&chain_id.to_be_bytes());
final_hasher.update(&chain_id_bytes);
let mut hash = [0u8; 32];
final_hasher.finalize(&mut hash);
Ok(hash)
}
fn pack_for_hash(&self) -> Result<Vec<u8>> {
let sender = Address::from_str(&self.sender)
.map_err(|e| Error::InvalidConfig(format!("Invalid sender: {}", e)))?;
let mut packed = Vec::new();
packed.extend_from_slice(&[0u8; 12]);
packed.extend_from_slice(sender.as_slice());
packed.extend_from_slice(&self.nonce.to_be_bytes::<32>());
let init_code_hash = keccak256(&self.init_code);
packed.extend_from_slice(&init_code_hash);
let call_data_hash = keccak256(&self.call_data);
packed.extend_from_slice(&call_data_hash);
packed.extend_from_slice(&self.call_gas_limit.to_be_bytes::<32>());
packed.extend_from_slice(&self.verification_gas_limit.to_be_bytes::<32>());
packed.extend_from_slice(&self.pre_verification_gas.to_be_bytes::<32>());
packed.extend_from_slice(&self.max_fee_per_gas.to_be_bytes::<32>());
packed.extend_from_slice(&self.max_priority_fee_per_gas.to_be_bytes::<32>());
let paymaster_hash = keccak256(&self.paymaster_and_data);
packed.extend_from_slice(&paymaster_hash);
Ok(packed)
}
pub fn to_rpc_format(&self) -> serde_json::Value {
serde_json::json!({
"sender": self.sender,
"nonce": format!("0x{:x}", self.nonce),
"initCode": format!("0x{}", hex::encode(&self.init_code)),
"callData": format!("0x{}", hex::encode(&self.call_data)),
"callGasLimit": format!("0x{:x}", self.call_gas_limit),
"verificationGasLimit": format!("0x{:x}", self.verification_gas_limit),
"preVerificationGas": format!("0x{:x}", self.pre_verification_gas),
"maxFeePerGas": format!("0x{:x}", self.max_fee_per_gas),
"maxPriorityFeePerGas": format!("0x{:x}", self.max_priority_fee_per_gas),
"paymasterAndData": format!("0x{}", hex::encode(&self.paymaster_and_data)),
"signature": format!("0x{}", hex::encode(&self.signature)),
})
}
}
#[derive(Debug, Clone)]
pub struct SmartAccountModule {
config: SmartAccountConfig,
evm: EvmAdapter,
}
impl SmartAccountModule {
pub fn new(config: SmartAccountConfig, evm: EvmAdapter) -> Self {
Self { config, evm }
}
pub fn config(&self) -> &SmartAccountConfig {
&self.config
}
pub fn calculate_address(&self, owner: &[u8], salt: &[u8; 32]) -> Result<String> {
let factory = Address::from_str(&self.config.account_factory)
.map_err(|e| Error::InvalidConfig(format!("Invalid factory address: {}", e)))?;
let init_code = self.build_init_code_hash(owner)?;
let mut hasher = Keccak::v256();
hasher.update(&[0xff]);
hasher.update(factory.as_slice());
hasher.update(salt);
hasher.update(&init_code);
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
Ok(format!("0x{}", hex::encode(&hash[12..])))
}
fn build_init_code_hash(&self, owner: &[u8]) -> Result<[u8; 32]> {
let mut hasher = Keccak::v256();
hasher.update(owner);
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
Ok(hash)
}
pub async fn is_deployed(&self, address: &str) -> Result<bool> {
let result: String = self
.evm
.rpc
.request("eth_getCode", serde_json::json!([address, "latest"]))
.await?;
Ok(result != "0x" && result != "0x0")
}
pub async fn get_nonce(&self, address: &str, key: u64) -> Result<U256> {
let call_data = encode_get_nonce(address, key)?;
let result: String = self
.evm
.rpc
.request(
"eth_call",
serde_json::json!([{
"to": self.config.entry_point,
"data": format!("0x{}", hex::encode(&call_data)),
}, "latest"]),
)
.await?;
let result_bytes = hex::decode(result.strip_prefix("0x").unwrap_or(&result))
.map_err(|e| Error::ChainError(format!("Failed to decode nonce: {}", e)))?;
if result_bytes.len() < 32 {
return Err(Error::ChainError("Invalid nonce response".into()));
}
Ok(U256::from_be_slice(&result_bytes[..32]))
}
pub async fn build_user_operation(
&self,
sender: &str,
call_data: Vec<u8>,
) -> Result<UserOperation> {
let is_deployed = self.is_deployed(sender).await?;
let nonce = self.get_nonce(sender, 0).await?;
let gas_prices = self.evm.get_gas_prices().await?;
let mut user_op = UserOperation::new(sender, nonce, call_data).with_gas_prices(
gas_prices.medium.max_fee,
gas_prices.medium.max_priority_fee,
);
if !is_deployed {
tracing::warn!("Account not deployed, init_code should be provided");
}
if let Some(paymaster) = &self.config.paymaster
&& self.config.use_sponsored_gas
{
user_op = user_op.with_paymaster(paymaster, vec![])?;
}
Ok(user_op)
}
pub async fn estimate_gas(&self, user_op: &UserOperation) -> Result<GasEstimate> {
let result: serde_json::Value = self
.evm
.rpc
.request(
"eth_estimateUserOperationGas",
serde_json::json!([user_op.to_rpc_format(), self.config.entry_point]),
)
.await?;
let call_gas = parse_u256(&result["callGasLimit"])?;
let verification_gas = parse_u256(&result["verificationGasLimit"])?;
let pre_verification_gas = parse_u256(&result["preVerificationGas"])?;
Ok(GasEstimate {
call_gas_limit: call_gas,
verification_gas_limit: verification_gas,
pre_verification_gas,
})
}
pub async fn send_user_operation(&self, user_op: &UserOperation) -> Result<String> {
let result: String = self
.evm
.rpc
.request(
"eth_sendUserOperation",
serde_json::json!([user_op.to_rpc_format(), self.config.entry_point]),
)
.await?;
Ok(result)
}
pub async fn get_user_operation_receipt(
&self,
user_op_hash: &str,
) -> Result<Option<UserOperationReceipt>> {
let result: Option<serde_json::Value> = self
.evm
.rpc
.request(
"eth_getUserOperationReceipt",
serde_json::json!([user_op_hash]),
)
.await?;
match result {
Some(receipt) => Ok(Some(UserOperationReceipt {
user_op_hash: user_op_hash.to_string(),
sender: receipt["sender"].as_str().unwrap_or_default().to_string(),
nonce: parse_u256(&receipt["nonce"]).unwrap_or(U256::ZERO),
success: receipt["success"].as_bool().unwrap_or(false),
actual_gas_cost: parse_u256(&receipt["actualGasCost"]).unwrap_or(U256::ZERO),
actual_gas_used: parse_u256(&receipt["actualGasUsed"]).unwrap_or(U256::ZERO),
tx_hash: receipt["receipt"]["transactionHash"]
.as_str()
.unwrap_or_default()
.to_string(),
block_number: receipt["receipt"]["blockNumber"]
.as_str()
.and_then(|s| u64::from_str_radix(s.strip_prefix("0x").unwrap_or(s), 16).ok())
.unwrap_or(0),
})),
None => Ok(None),
}
}
pub async fn wait_for_user_operation(
&self,
user_op_hash: &str,
timeout_secs: u64,
) -> Result<UserOperationReceipt> {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(timeout_secs);
loop {
if start.elapsed() > timeout {
return Err(Error::Timeout(format!(
"UserOperation {} not confirmed within {} seconds",
user_op_hash, timeout_secs
)));
}
if let Some(receipt) = self.get_user_operation_receipt(user_op_hash).await? {
return Ok(receipt);
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
pub fn encode_execute(&self, to: &str, value: U256, data: &[u8]) -> Result<Vec<u8>> {
let to_addr = Address::from_str(to)
.map_err(|e| Error::InvalidConfig(format!("Invalid address: {}", e)))?;
let selector = [0xb6, 0x1d, 0x27, 0xf6];
let mut encoded = selector.to_vec();
encoded.extend_from_slice(&[0u8; 12]);
encoded.extend_from_slice(to_addr.as_slice());
encoded.extend_from_slice(&value.to_be_bytes::<32>());
let offset: u64 = 96;
encoded.extend_from_slice(&U256::from(offset).to_be_bytes::<32>());
encoded.extend_from_slice(&U256::from(data.len()).to_be_bytes::<32>());
encoded.extend_from_slice(data);
let padding = (32 - (data.len() % 32)) % 32;
encoded.extend_from_slice(&vec![0u8; padding]);
Ok(encoded)
}
pub fn encode_execute_batch(
&self,
targets: &[&str],
values: &[U256],
data: &[&[u8]],
) -> Result<Vec<u8>> {
if targets.len() != values.len() || targets.len() != data.len() {
return Err(Error::InvalidConfig("Array lengths must match".into()));
}
let selector = [0x47, 0xe1, 0xda, 0x2a];
let mut encoded = selector.to_vec();
let offset_1: u64 = 96;
let offset_2: u64 = offset_1 + 32 + (targets.len() as u64 * 32);
let offset_3: u64 = offset_2 + 32 + (values.len() as u64 * 32);
encoded.extend_from_slice(&U256::from(offset_1).to_be_bytes::<32>());
encoded.extend_from_slice(&U256::from(offset_2).to_be_bytes::<32>());
encoded.extend_from_slice(&U256::from(offset_3).to_be_bytes::<32>());
encoded.extend_from_slice(&U256::from(targets.len()).to_be_bytes::<32>());
for target in targets {
let addr = Address::from_str(target)
.map_err(|e| Error::InvalidConfig(format!("Invalid address: {}", e)))?;
encoded.extend_from_slice(&[0u8; 12]);
encoded.extend_from_slice(addr.as_slice());
}
encoded.extend_from_slice(&U256::from(values.len()).to_be_bytes::<32>());
for value in values {
encoded.extend_from_slice(&value.to_be_bytes::<32>());
}
encoded.extend_from_slice(&U256::from(data.len()).to_be_bytes::<32>());
Ok(encoded)
}
pub fn sign_user_operation(
&self,
user_op: &mut UserOperation,
signature: &Signature,
) -> Result<()> {
let mut sig_bytes = Vec::with_capacity(65);
sig_bytes.extend_from_slice(&signature.r);
sig_bytes.extend_from_slice(&signature.s);
sig_bytes.push(signature.v());
user_op.signature = sig_bytes;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GasEstimate {
pub call_gas_limit: U256,
pub verification_gas_limit: U256,
pub pre_verification_gas: U256,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserOperationReceipt {
pub user_op_hash: String,
pub sender: String,
pub nonce: U256,
pub success: bool,
pub actual_gas_cost: U256,
pub actual_gas_used: U256,
pub tx_hash: String,
pub block_number: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionKeyConfig {
pub session_key: String,
pub valid_after: u64,
pub valid_until: u64,
pub allowed_targets: Vec<String>,
pub max_value_per_tx: Option<U256>,
pub max_total_value: Option<U256>,
pub allowed_selectors: Vec<[u8; 4]>,
}
impl SessionKeyConfig {
pub fn new(session_key: impl Into<String>, valid_for_secs: u64) -> Self {
let now = chrono::Utc::now().timestamp() as u64;
Self {
session_key: session_key.into(),
valid_after: now,
valid_until: now + valid_for_secs,
allowed_targets: vec![],
max_value_per_tx: None,
max_total_value: None,
allowed_selectors: vec![],
}
}
pub fn with_targets(mut self, targets: Vec<String>) -> Self {
self.allowed_targets = targets;
self
}
pub fn with_max_value_per_tx(mut self, max: U256) -> Self {
self.max_value_per_tx = Some(max);
self
}
pub fn with_selectors(mut self, selectors: Vec<[u8; 4]>) -> Self {
self.allowed_selectors = selectors;
self
}
pub fn is_valid(&self) -> bool {
let now = chrono::Utc::now().timestamp() as u64;
now >= self.valid_after && now <= self.valid_until
}
pub fn encode(&self) -> Result<Vec<u8>> {
let session_key = Address::from_str(&self.session_key)
.map_err(|e| Error::InvalidConfig(format!("Invalid session key: {}", e)))?;
let mut encoded = Vec::new();
encoded.extend_from_slice(&[0u8; 12]);
encoded.extend_from_slice(session_key.as_slice());
encoded.extend_from_slice(&U256::from(self.valid_after).to_be_bytes::<32>());
encoded.extend_from_slice(&U256::from(self.valid_until).to_be_bytes::<32>());
Ok(encoded)
}
}
fn keccak256(data: &[u8]) -> [u8; 32] {
let mut hasher = Keccak::v256();
hasher.update(data);
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
hash
}
fn encode_get_nonce(address: &str, key: u64) -> Result<Vec<u8>> {
let addr = Address::from_str(address)
.map_err(|e| Error::InvalidConfig(format!("Invalid address: {}", e)))?;
let selector = [0x35, 0x56, 0x7e, 0x1a];
let mut encoded = selector.to_vec();
encoded.extend_from_slice(&[0u8; 12]);
encoded.extend_from_slice(addr.as_slice());
encoded.extend_from_slice(&U256::from(key).to_be_bytes::<32>());
Ok(encoded)
}
fn parse_u256(value: &serde_json::Value) -> Result<U256> {
let s = value
.as_str()
.ok_or_else(|| Error::ChainError("Expected hex string".into()))?;
let s = s.strip_prefix("0x").unwrap_or(s);
if s.is_empty() || s == "0" {
return Ok(U256::ZERO);
}
let bytes = hex::decode(format!("{:0>64}", s))
.map_err(|e| Error::ChainError(format!("Failed to decode U256: {}", e)))?;
Ok(U256::from_be_slice(&bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_operation_creation() {
let user_op = UserOperation::new(
"0x742d35Cc6634C0532925a3b844Bc9e7595f4e123",
U256::from(0),
vec![0xb6, 0x1d, 0x27, 0xf6], );
assert_eq!(user_op.sender, "0x742d35Cc6634C0532925a3b844Bc9e7595f4e123");
assert_eq!(user_op.nonce, U256::ZERO);
}
#[test]
fn test_user_operation_hash() {
let user_op = UserOperation::new(
"0x742d35Cc6634C0532925a3b844Bc9e7595f4e123",
U256::from(0),
vec![],
)
.with_gas_limits(100000, 100000, 21000)
.with_gas_prices(50_000_000_000, 2_000_000_000);
let hash = user_op.hash(ENTRY_POINT_V06, 1).unwrap();
assert_eq!(hash.len(), 32);
}
#[test]
fn test_session_key_validity() {
let config = SessionKeyConfig::new(
"0x742d35Cc6634C0532925a3b844Bc9e7595f4e123",
3600, );
assert!(config.is_valid());
}
#[test]
fn test_smart_account_config() {
let config = SmartAccountConfig::with_default_entry_point_v06(
"0x9406Cc6185a346906296840746125a0E44976454",
);
assert_eq!(config.entry_point, ENTRY_POINT_V06);
assert!(!config.use_sponsored_gas);
let config_with_paymaster =
config.with_paymaster("0x1234567890123456789012345678901234567890");
assert!(config_with_paymaster.use_sponsored_gas);
}
#[test]
fn test_encode_execute() {
let config = SmartAccountConfig::with_default_entry_point_v06(
"0x9406Cc6185a346906296840746125a0E44976454",
);
let evm = EvmAdapter::new(super::super::EvmConfig::ethereum_mainnet()).unwrap();
let module = SmartAccountModule::new(config, evm);
let encoded = module
.encode_execute(
"0x742d35Cc6634C0532925a3b844Bc9e7595f4e123",
U256::from(1_000_000_000_000_000_000u128), &[],
)
.unwrap();
assert_eq!(&encoded[..4], &[0xb6, 0x1d, 0x27, 0xf6]);
}
}