zeus-userop-kit 0.1.0

ERC-4337 user operation builder and bundler client for EntryPoint 0.7 and 0.8
use alloy_primitives::{Address, Bytes, U256, address, aliases::U192, bytes};
use alloy_provider::{Provider, network::Ethereum};
use alloy_rpc_types::Authorization;

use alloy_sol_types::{Eip712Domain, SolCall};
use serde::{Deserialize, Serialize};

use crate::{
   abis::entry_point::EntryPoint,
   entry_point::{ENTRY_POINT_08, entry_point_08_domain},
};

/// eth-infinitism Simple7702Account implementation used as EIP-7702 delegate target.
pub const SIMPLE_7702_ACCOUNT: Address = address!("0xe6Cae83BdE06E4c305530e199D7217f42808555B");

/// Creates a simple smart account.
///
/// Defaults to the v0.8 EntryPoint and the eth-infinitism Simple7702Account implementation at
/// [`SIMPLE_7702_ACCOUNT`].
#[derive(Clone)]
pub struct SimpleSmartAccount<P> {
   owner: Address,
   chain_id: u64,
   provider: P,

   implementation: Address,
   entry_point: Address,
   domain: Eip712Domain,
   dummy_signature: Bytes,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Call {
   pub target: Address,
   pub value: U256,
   pub data: Bytes,
}

impl<P> SimpleSmartAccount<P>
where
   P: Provider<Ethereum>,
{
   pub fn new(owner: Address, chain_id: u64, provider: P) -> Self {
      let implementation = SIMPLE_7702_ACCOUNT;
      let entry_point = ENTRY_POINT_08;
      let domain = entry_point_08_domain(chain_id);
      let dummy_signature = bytes!(
         "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
      );

      Self {
         owner,
         chain_id,
         provider,
         implementation,
         entry_point,
         domain,
         dummy_signature,
      }
   }

   pub fn entry_point(&self) -> Address {
      self.entry_point
   }

   pub fn domain(&self) -> Eip712Domain {
      self.domain.clone()
   }

   pub fn address(&self) -> Address {
      self.owner
   }

   pub async fn nonce(&self) -> Result<U256, anyhow::Error> {
      let call = EntryPoint::getNonceCall::new((self.owner, U192::from(0)));
      let calldata = call.abi_encode();

      use alloy_rpc_types::TransactionRequest;
      let tx = TransactionRequest::default().to(self.entry_point).input(calldata.into());

      let result = self.provider.call(tx).await?;
      // Decode the single U256 return value
      let nonce =
         <EntryPoint::getNonceCall as alloy_sol_types::SolCall>::abi_decode_returns(&result)?;
      Ok(nonce)
   }

   pub async fn authorization(&self) -> Result<Authorization, anyhow::Error> {
      let nonce = self.owner_nonce().await?;

      Ok(Authorization {
         chain_id: U256::from(self.chain_id),
         address: self.implementation,
         nonce,
      })
   }

   pub fn dummy_signature(&self) -> Bytes {
      self.dummy_signature.clone()
   }

   pub fn encode_call_data(&self, call_data: Vec<Call>) -> Bytes {
      if call_data.is_empty() {
         // If no calls, return empty data to save gas.
         return Bytes::new();
      }

      let calls = call_data
         .into_iter()
         .map(|call| abi::BaseAccount::Call {
            target: call.target,
            value: call.value,
            data: call.data,
         })
         .collect();

      abi::BaseAccount::executeBatchCall::new((calls,)).abi_encode().into()
   }

   /// Gets the nonce for the owner address
   async fn owner_nonce(&self) -> Result<u64, anyhow::Error> {
      let nonce: u64 = self.provider.get_transaction_count(self.owner).await? as u64;
      Ok(nonce)
   }
}

mod abi {
   use alloy_sol_types::sol;

   sol!(
       contract BaseAccount {
           struct Call {
               address target;
               uint256 value;
               bytes data;
           }

           function executeBatch(Call[] calldata calls) external;
       }

   );
}