diff --git a/Cargo.lock b/Cargo.lock
index c96971897e..8d65cfbf9b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -995,6 +995,7 @@ dependencies = [
"mm2_core",
"mm2_db",
"mm2_err_handle",
+ "mm2_eth",
"mm2_event_stream",
"mm2_io",
"mm2_metamask",
diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml
index e84ed0e373..5d9da8d638 100644
--- a/mm2src/coins/Cargo.toml
+++ b/mm2src/coins/Cargo.toml
@@ -79,6 +79,7 @@ mm2_event_stream = { path = "../mm2_event_stream" }
mm2_io = { path = "../mm2_io" }
mm2_metrics = { path = "../mm2_metrics" }
mm2_net = { path = "../mm2_net" }
+mm2_eth = { path = "../mm2_eth" }
mm2_number = { path = "../mm2_number"}
mm2_p2p = { path = "../mm2_p2p", default-features = false }
mm2_rpc = { path = "../mm2_rpc" }
diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs
index 7261d4a24c..42a01a86c0 100644
--- a/mm2src/coins/eth.rs
+++ b/mm2src/coins/eth.rs
@@ -152,7 +152,7 @@ pub use eth_utils::{
};
use eth_utils::{
get_conf_param_or_from_plaform_coin, get_function_input_data, get_function_name, ESTIMATE_GAS_MULT,
- GAS_PRICE_ADJUST, MAX_ETH_TX_TYPE_SUPPORTED, SWAP_GAS_FEE_POLICY,
+ GAS_PRICE_ADJUST, MAX_ETH_TX_TYPE_SUPPORTED, SWAP_GAS_FEE_POLICY, EIP4337_URL,
};
pub use rlp;
@@ -162,6 +162,8 @@ cfg_native! {
pub mod eth_balance_events;
mod eth_rpc;
+mod eip4337_rpc;
+use eip4337_rpc::Eip4337Rpc;
#[cfg(test)]
mod eth_tests;
#[cfg(target_arch = "wasm32")]
@@ -196,6 +198,7 @@ use eth_swap_v2::{extract_id_from_tx_data, EthPaymentType, PaymentMethod, SpendT
pub mod eth_utils;
pub mod tron;
+mod eth_abstraction;
/// Default timeout to wait for eth rpc request to complete
pub(crate) const ETH_RPC_REQUEST_TIMEOUT_S: Duration = Duration::from_secs(30);
@@ -930,6 +933,7 @@ pub struct EthCoinImpl {
fallback_swap_contract: Option
,
contract_supports_watchers: bool,
web3_instances: AsyncMutex>,
+ eip4337_rpc: Option,
decimals: u8,
history_sync_state: Mutex,
required_confirmations: AtomicU64,
@@ -6769,6 +6773,11 @@ pub async fn eth_coin_from_conf_and_request(
get_conf_param_or_from_plaform_coin(ctx, conf, &coin_type, SWAP_GAS_FEE_POLICY)?.unwrap_or_default();
let swap_gas_fee_policy: SwapGasFeePolicy =
json::from_value(req["swap_gas_fee_policy"].clone()).unwrap_or(swap_gas_fee_policy_default);
+ let eip4337_rpc = if let Ok(uri_str) = json::from_value::(req[EIP4337_URL].clone()) {
+ Some(try_s!(uri_str.parse()))
+ } else {
+ None
+ };
let coin = EthCoinImpl {
priv_key_policy: key_pair,
@@ -6784,6 +6793,7 @@ pub async fn eth_coin_from_conf_and_request(
decimals,
ticker: ticker.into(),
web3_instances: AsyncMutex::new(web3_instances),
+ eip4337_rpc,
history_sync_state: Mutex::new(initial_history_state),
swap_gas_fee_policy: Mutex::new(swap_gas_fee_policy),
max_eth_tx_type,
@@ -7730,6 +7740,7 @@ impl EthCoin {
fallback_swap_contract: self.fallback_swap_contract,
contract_supports_watchers: self.contract_supports_watchers,
web3_instances: AsyncMutex::new(self.web3_instances.lock().await.clone()),
+ eip4337_rpc: None,
decimals: self.decimals,
history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()),
required_confirmations: AtomicU64::new(
diff --git a/mm2src/coins/eth/eip4337_rpc.rs b/mm2src/coins/eth/eip4337_rpc.rs
new file mode 100644
index 0000000000..e60c84dd9f
--- /dev/null
+++ b/mm2src/coins/eth/eip4337_rpc.rs
@@ -0,0 +1,60 @@
+use super::*;
+use crate::eth::web3_transport::http_transport::de_rpc_response;
+use crate::hd_wallet::AddrToString;
+// use alloy::rpc::types::eth::erc4337::{SendUserOperation, SendUserOperationResponse};
+// use alloy::sol_types::eip712_domain;
+use mm2_net::transport::{slurp_post_json, SlurpResult};
+use serde_json::{json, Value};
+//use url::Url;
+
+pub(crate) struct Eip4337Rpc;
+
+impl Eip4337Rpc {
+
+ async fn call_api(rpc_uri: &Uri, method: &str, params: &Value) -> SlurpResult {
+ let req = json!({
+ "jsonrpc": "2.0",
+ "method": method,
+ "params": params,
+ "id": 1
+ });
+ println!("Eip4337Rpc req: {}", serde_json::to_string_pretty(&req).unwrap());
+ //post_json(self.api_url.as_str(), serde_json::to_string(&req)?).await
+ slurp_post_json(&rpc_uri.to_string(), serde_json::to_string(&req)?).await
+ }
+
+ pub(crate) async fn send_user_operation(rpc_uri: &Uri, user_op: Value, entry_point: Address) -> Result {
+ let result = Self::call_api(
+ rpc_uri,
+ "eth_sendUserOperation",
+ &json!([
+ &user_op,
+ entry_point.addr_to_string()
+ ])
+ ).await;
+ decode_rpc_result(result, rpc_uri)
+ }
+}
+
+
+
+fn decode_rpc_result(result: SlurpResult, uri: &Uri) -> Result {
+ match result {
+ Ok((status, _, body)) => {
+ if !status.is_success() {
+ return Err(web3::Error::Transport(web3::error::TransportError::Code(status.as_u16())));
+ }
+
+ match de_rpc_response(body, &uri.to_string()) {
+ Ok(val) => Ok(val),
+ Err(err) => Err(web3::Error::InvalidResponse(
+ format!("Server: '{}', error: {}", uri, err)
+ )),
+ }
+ },
+ Err(err) => Err(web3::Error::Transport(
+ web3::error::TransportError::Message(
+ format!("Server: '{}', error: {}", uri, err.get_inner())
+ ))),
+ }
+}
\ No newline at end of file
diff --git a/mm2src/coins/eth/eth_abstraction.rs b/mm2src/coins/eth/eth_abstraction.rs
new file mode 100644
index 0000000000..9fcfa1ad7f
--- /dev/null
+++ b/mm2src/coins/eth/eth_abstraction.rs
@@ -0,0 +1,569 @@
+//? EVM account abstraction support
+
+use super::*;
+use bitcoin_hashes::hex::ToHex;
+use ethabi::Function;
+use mm2_eth::eip712::{Eip712, EIP712_DOMAIN, ObjectType, PropertyType};
+use mm2_eth::eip712_encode::hash_typed_data;
+// use alloy::rpc::types::eth::erc4337::PackedUserOperation;
+//use alloy::sol_types::{eip712_domain, sol};
+//use alloy::primitives;
+/*use alloy::{
+ primitives, //::{address, keccak256, U256},
+ rpc::types::eth::erc4337::{PackedUserOperation, SendUserOperation},
+ // signers::{local::PrivateKeySigner, Signer},
+ sol,
+ sol_types::{eip712_domain, SolStruct},
+};*/
+use lazy_static::lazy_static;
+
+const PACKED_USEROP_PRIMARY_TYPE: &str = "PackedUserOperation";
+
+const EIP7702_MAGIC: [u8; 1] = [0x05];
+
+const EIP7702_INITCODE_MARKER: [u8; 2] = [0x77, 0x02];
+
+
+
+lazy_static! {
+
+ /// Pimlico paymaster contract v0.7 address
+ static ref PIMLICO_ERC20_PAYMASTER_0_7: Address =
+ Address::from_str("0x777777777777AeC03fd955926DbF81597e66834C").expect("Address::from_str valid");
+
+ static ref PIMLICO_ERC20_PAYMASTER_0_8: Address =
+ Address::from_str("0x888888888888Ec68A58AB8094Cc1AD20Ba3D2402").expect("Address::from_str valid");
+
+ static ref SAFE_EIP_7702_PROXY: Address =
+ Address::from_str("0xE60EcE6588DCcFb7373538034963B4D20a280DB0").expect("Address::from_str valid"); // NOTE: experimental, not audited contract
+
+ static ref SIMPLE_EIP_7702_SMART_ACCOUNT: Address =
+ Address::from_str("0xe6Cae83BdE06E4c305530e199D7217f42808555B").expect("Address::from_str valid");
+
+ static ref EIP_4337_ENTRY_POINT_7: Address =
+ Address::from_str("0x0000000071727De22E5E9d8BAf0edAc6f37da032").expect("Address::from_str valid");
+
+ static ref EIP_4337_ENTRY_POINT_8: Address =
+ Address::from_str("0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108").expect("Address::from_str valid");
+
+ static ref EIP712_DOMAIN_TYPES: ObjectType = Eip712Domain::build_types();
+
+ static ref PACKED_USEROP_TYPES: Vec = PackedUserOperation::build_types();
+
+ //static ref EIP7702_INITCODE_MARKER_PADDED_RIGHT: Vec = [EIP7702_INITCODE_MARKER.as_slice(), [0u8; 18].as_slice()].concat();
+ static ref EIP7702_INITCODE_MARKER_PADDED_RIGHT: Address =
+ Address::from_str("0x7702000000000000000000000000000000000000").expect("Address::from_str valid");
+
+ //static ref EIP7702_INITCODE_MARKER_PADDED_STR: String = "0x".to_owned() + &hex::encode(EIP7702_INITCODE_MARKER.as_slice());
+ static ref EIP7702_INITCODE_MARKER_PADDED: Address =
+ Address::from_str("0x0000000000000000000000000000000000007702").expect("Address::from_str valid");
+
+ static ref EIP_4337_GET_NONCE: Function = serde_json::from_value(json!({
+ "inputs":[
+ {"internalType":"address","name":"sender","type":"address"},
+ {"internalType":"uint192","name":"key","type":"uint192"}
+ ],
+ "name":"getNonce",
+ "outputs":[
+ {"internalType":"uint256","name":"nonce","type":"uint256"}
+ ],
+ "stateMutability":"view",
+ "type":"function"
+ })).expect("valid EIP-4337 getNonce ABI");
+}
+
+/// Struct to build Eip-712 domain separator
+#[derive(Debug, Deserialize, Serialize)]
+pub(crate) struct Eip712Domain {
+ name: String,
+ version: String,
+ #[serde(rename = "chainId")]
+ chain_id: U256,
+ #[serde(rename = "verifyingContract")]
+ verifying_contract: Address,
+}
+
+impl Eip712Domain {
+ fn build_types() -> ObjectType {
+ let mut domain_types = ObjectType::new(EIP712_DOMAIN);
+ domain_types.property("name", PropertyType::String);
+ domain_types.property("version", PropertyType::String);
+ domain_types.property("chainId", PropertyType::Uint256);
+ domain_types.property("verifyingContract", PropertyType::Address);
+ domain_types
+ }
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PackedUserOperationTyped {
+ sender: Address,
+ nonce: U256,
+ init_code: Bytes,
+ call_data: Bytes,
+ account_gas_limits: Bytes,
+ pre_verification_gas: U256,
+ gas_fees: Bytes,
+ paymaster_and_data: Bytes,
+ //eip_7702_auth: Option,
+}
+
+/// PackedUserOperation in the spec: Entry Point V0.7
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PackedUserOperation {
+ /// The account making the operation.
+ pub sender: Address,
+ /// Prevents message replay attacks and serves as a randomizing element for initial user
+ /// registration.
+ pub nonce: U256,
+ /// Deployer contract address: Required exclusively for deploying new accounts that don't yet
+ /// exist on the blockchain.
+ pub factory: Option, // Address
+ /// Factory data for the account creation process, applicable only when using a deployer
+ /// contract.
+ pub factory_data: Option,
+ /// The call data.
+ pub call_data: Bytes,
+ /// The gas limit for the call.
+ pub call_gas_limit: U256,
+ /// The gas limit for the verification.
+ pub verification_gas_limit: U256,
+ /// Prepaid gas fee: Covers the bundler's costs for initial transaction validation and data
+ /// transmission.
+ pub pre_verification_gas: U256,
+ /// The maximum fee per gas.
+ pub max_fee_per_gas: U256,
+ /// The maximum priority fee per gas.
+ pub max_priority_fee_per_gas: U256,
+ /// Paymaster contract address: Needed if a third party is covering transaction costs; left
+ /// blank for self-funded accounts.
+ pub paymaster: Option,
+ /// The gas limit for the paymaster verification.
+ pub paymaster_verification_gas_limit: Option,
+ /// The gas limit for the paymaster post-operation.
+ pub paymaster_post_op_gas_limit: Option,
+ /// The paymaster data.
+ pub paymaster_data: Option, // TODO: remove?
+ /// The signature of the transaction.
+ pub signature: Bytes,
+ /// Authorization to delegate to smart contract
+ pub eip_7702_auth: Option,
+}
+
+impl PackedUserOperation {
+ fn build_types() -> Vec {
+ let mut userop_type = ObjectType::new(PACKED_USEROP_PRIMARY_TYPE);
+ userop_type.property("sender", PropertyType::Address);
+ userop_type.property("nonce", PropertyType::Uint256);
+ userop_type.property("initCode", PropertyType::Bytes);
+ userop_type.property("callData", PropertyType::Bytes);
+ userop_type.property("accountGasLimits", PropertyType::Bytes32);
+ userop_type.property("preVerificationGas", PropertyType::Uint256);
+ userop_type.property("gasFees", PropertyType::Bytes32);
+ userop_type.property("paymasterAndData", PropertyType::Bytes);
+ /*userop_type.property("eip7702Auth", PropertyType::Custom("eip7702Auth".to_string()));
+ let mut eip_7702_auth_type = ObjectType::new("eip7702Auth");
+ eip_7702_auth_type.property("address", PropertyType::Address);
+ eip_7702_auth_type.property("chainId", PropertyType::Uint256);
+ eip_7702_auth_type.property("nonce", PropertyType::Uint256);
+ eip_7702_auth_type.property("r", PropertyType::Uint256);
+ eip_7702_auth_type.property("s", PropertyType::Uint256);
+ eip_7702_auth_type.property("v", PropertyType::Uint256);
+ eip_7702_auth_type.property("yParity", PropertyType::Uint256);*/
+
+ vec![userop_type] //, eip_7702_auth_type]
+ }
+
+ fn build_struct(&self) -> PackedUserOperationTyped {
+ /*let mut init_code: Vec = self.factory.map(|addr| addr.to_bytes()).unwrap_or_default();
+ if let Some(factory_data) = &self.factory_data {
+ init_code.extend_from_slice(&factory_data.0)
+ };*/
+ PackedUserOperationTyped {
+ sender: self.sender,
+ nonce: self.nonce,
+ init_code: SIMPLE_EIP_7702_SMART_ACCOUNT.as_bytes().into(), // SIMPLE_EIP_7702_SMART_ACCOUNT.as_bytes().into(), //init_code.into(),
+ call_data: self.call_data.clone(),
+ account_gas_limits: make_bytes32_from_two(self.verification_gas_limit, self.call_gas_limit),
+ pre_verification_gas: self.pre_verification_gas,
+ gas_fees: make_bytes32_from_two(self.max_priority_fee_per_gas, self.max_fee_per_gas),
+ paymaster_and_data: Default::default(), //PIMLICO_ERC20_PAYMASTER_0_7.as_bytes().into(),
+ //eip_7702_auth: self.eip_7702_auth.clone(),
+ }
+ }
+}
+
+/// Eip-7702 autorisation
+#[derive(Debug)]
+struct Eip7702Authorization {
+ address: Address,
+ chain_id: U256,
+ nonce: u64,
+}
+
+impl rlp::Encodable for Eip7702Authorization {
+ fn rlp_append(&self, s: &mut RlpStream) {
+ s.begin_list(3);
+ s.append(&self.chain_id);
+ s.append(&self.address);
+ s.append(&self.nonce);
+ }
+}
+
+impl Eip7702Authorization {
+ fn to_rlp(&self) -> Vec {
+ let mut stream = RlpStream::new();
+ self.rlp_append(&mut stream);
+ if stream.is_finished() {
+ Vec::from(stream.out())
+ } else {
+ warn!("RlpStream was not finished; returning an empty Vec as a fail-safe.");
+ vec![]
+ }
+ }
+
+ fn as_msg(&self) -> [u8; 32] {
+ let mut msg = EIP7702_MAGIC.to_vec();
+ msg.append(&mut self.to_rlp());
+ keccak256(&msg).take()
+ }
+}
+
+
+/// Eip7702 signed autorisation
+/// NOTE: fields must be serialized as hex prefixed with "0x" and be in camelCase, according to the pimlico API docs
+#[derive(Clone, Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignedAuthorization {
+ address: Address,
+ chain_id: U256,
+ nonce: U256,
+ r: U256,
+ s: U256,
+ //v: U256,
+ y_parity: U256,
+}
+
+impl SignedAuthorization {
+ fn new(auth: Eip7702Authorization, sig: Signature) -> Self {
+ Self {
+ address: auth.address,
+ chain_id: auth.chain_id,
+ nonce: auth.nonce.into(),
+ r: if sig.r().len() == 32 { U256::from(sig.r()) } else { U256::zero() },
+ s: if sig.s().len() == 32 { U256::from(sig.s()) } else { U256::zero() },
+ //v: (sig.v() + 27).into(),
+ y_parity: sig.v().into(),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SendUserOperationResponse {
+ /// The hash of the user operation.
+ pub user_op_hash: Bytes,
+}
+
+impl EthCoin {
+ /// Build, sign and submit a PackedUserOperation that delegates `sender` (this coin's account)
+ /// to the provided `safe_contract` by calling the Safe contract's delegate/enable method.
+ ///
+ /// Notes:
+ /// - This implementation uses the `PackedUserOperation` type to serialize the userOp.
+ /// - It attempts to estimate gas for the call via `estimate_gas_for_contract_call_if_conf` and
+ /// falls back to configured gas limits from this coin.
+ /// - The method signs the user operation using the local `KeyPair` when available. For external
+ /// signers (WalletConnect / MetaMask) the call will currently return an Err indicating the
+ /// external path should be implemented if needed.
+ async fn delegate_to_safe_account(
+ &self,
+ safe_contract: Address,
+ delegate_call_data: Bytes,
+ ) -> Result {
+
+ let paymaster = *PIMLICO_ERC20_PAYMASTER_0_7;
+ let eip4337_rpc = self.eip4337_rpc.as_ref().ok_or("AA RPC not initialized".to_string())?;
+ // Determine sender address (this coin's account address)
+ let my_address_str = self.my_address()
+ .map_err(|e| format!("Failed to get my_address: {e}"))?;
+ let my_address = Address::from_str(&my_address_str)
+ .map_err(|e| format!("Failed to parse my_address: {e}"))?;
+ let address_lock = self.get_address_lock(my_address).await;
+ let _nonce_lock = address_lock.lock().await;
+ let (nonce, _) = self
+ .clone()
+ .get_addr_nonce(my_address)
+ .compat()
+ .await
+ .map_err(|e| format!("Failed to get nonce: {e}"))?;
+ drop(_nonce_lock);
+
+ /*let eip7702_address_lock = self.get_address_lock(*SAFE_EIP_7702_PROXY).await;
+ let eip7702_nonce_lock = address_lock.lock().await;
+ let (eip7702_nonce, _) = self
+ .clone()
+ .get_addr_nonce(*SAFE_EIP_7702_PROXY)
+ .compat()
+ .await
+ .map_err(|e| format!("Failed to get nonce: {e}"))?;
+ drop(eip7702_nonce_lock);*/
+
+ let get_nonce_data = EIP_4337_GET_NONCE.encode_input(&[Token::Address(my_address), Token::Uint(U256::from(0))])
+ .map_err(|err| err.to_string())?;
+ let res = self
+ .call_request(my_address, *EIP_4337_ENTRY_POINT_8, None, Some(get_nonce_data.into()), BlockNumber::Latest)
+ .await
+ .map_err(|err| err.to_string())?;
+ let outputs = EIP_4337_GET_NONCE.decode_output(&res.0).map_err(|err| err.to_string())?;
+ let eip4337_nonce = match outputs.get(0) {
+ Some(Token::Uint(val)) => *val,
+ _ => return Err("could not decode getNonce response".to_string()),
+ };
+
+ // Estimate gas for the contract call if allowed by config
+ /*let call_req = web3::types::CallRequest {
+ from: Some(sender),
+ to: Some(safe_contract),
+ gas: None,
+ gas_price: None,
+ value: Some(U256::zero()),
+ data: Some(delegate_call_data.clone()),
+ transaction_type: None,
+ access_list: None,
+ max_priority_fee_per_gas: None,
+ max_fee_per_gas: None,
+ };*/
+
+ let estimated_gas_opt = U256::from(250_000u64);
+ /*let estimated_gas_opt = None;
+ let estimated_gas_opt = match self.estimate_gas_for_contract_call_if_conf(safe_contract, delegate_call_data.0.clone()).await {
+ Ok(v) => v,
+ Err(e) => {
+ // Log and continue with None so we fall back to gas_limit
+ debug!("estimate_gas_for_contract_call_if_conf failed: {e}");
+ None
+ }
+ };*/
+
+
+ let verification_gas_limit = U256::from(100_000u64); // conservative default
+ // Choose callGasLimit: network estimate or configured fallback
+ let call_gas_limit = U256::zero(); // estimated_gas_opt;
+ let (max_fee_per_gas, max_priority_fee_per_gas) = (U256::from(100_000_000_000u64), U256::from(2_000_000_000u64));
+ //let (max_fee_per_gas, max_priority_fee_per_gas) = (U256::zero(), U256::zero());
+
+ let chain_id = self.chain_id().ok_or("No chain id".to_string())?.into();
+ // Delegate to Safe Proxy experimental smart contract
+ let eip7702_delegate_auth = Eip7702Authorization {
+ address: *SIMPLE_EIP_7702_SMART_ACCOUNT, //*SAFE_EIP_7702_PROXY,
+ chain_id,
+ nonce: nonce.as_u64(), // TODO: may panic
+ };
+
+ // Build EIP-712 domain and hash userOp.
+ let domain = Eip712Domain {
+ name: "ERC4337".to_owned(),
+ version: "1".to_owned(),
+ chain_id,
+ verifying_contract: *EIP_4337_ENTRY_POINT_8, //paymaster,
+ //salt: keccak256(b"test").as_fixed_bytes().into(),
+ };
+
+ // Prepare basic PackedUserOperation fields.
+ // We fill common fields and leave paymasterAndData empty.
+ let mut user_op_packed = PackedUserOperation {
+ sender: Address::from_str(&my_address_str).map_err(|e| format!("Failed to parse my_address: {e}"))?,
+ nonce: eip4337_nonce,
+ factory: Some([0x77, 0x02].to_vec().into()), // Some(*EIP7702_INITCODE_MARKER_PADDED_RIGHT),
+ factory_data: None,
+ call_data: delegate_call_data,
+ call_gas_limit,
+ verification_gas_limit,
+ pre_verification_gas: U256::from(210_000u64),
+ max_fee_per_gas,
+ max_priority_fee_per_gas,
+ paymaster: None, // Some(paymaster),
+ paymaster_verification_gas_limit: None, //Some(U256::from(21_000u64)),
+ paymaster_post_op_gas_limit: None, //Some(U256::from(21_000u64)),
+ paymaster_data: Some(Bytes::default()),
+ signature: Bytes::default(),
+ eip_7702_auth: None,
+ };
+
+ // Sign the PackedUserOperation according to EIP-712
+ match self.priv_key_policy {
+ EthPrivKeyPolicy::Iguana(ref key_pair)
+ | EthPrivKeyPolicy::HDWallet {
+ activated_key: ref key_pair,
+ ..
+ } => {
+
+
+ /*let packed_to_sign = PackedUserOperationToSign {
+ sender: packed.sender,
+ nonce: packed.nonce,
+ initCode: Bytes::default(),
+ callData: packed.call_data.clone(),
+ accountGasLimits: make_byte32_from_two(packed.verification_gas_limit, packed.call_gas_limit),
+ preVerificationGas: packed.pre_verification_gas,
+ gasFees: make_byte32_from_two(packed.max_priority_fee_per_gas, packed.max_fee_per_gas),
+ paymasterAndData: PIMLICO_ERC20_PAYMASTER_0_7.as_bytes().into(),
+ };*/
+
+ // NOTE: alloy helpers may expose a method like `PackedUserOperation::hash_eip712`.
+ // If not, the crate compile will point to the correct API and can be adjusted.
+ //let userop_hash = packed_to_sign.eip712_signing_hash(&domain);
+ //.map_err(|e| format!("Failed to compute userop EIP712 hash: {e}"))?;
+
+ let auth_sig = sign(key_pair.secret(), &eip7702_delegate_auth.as_msg().into()).map_err(|e| format!("Signing failed: {e}"))?;
+ let signed_auth = SignedAuthorization::new(eip7702_delegate_auth, auth_sig);
+ user_op_packed.eip_7702_auth = Some(signed_auth);
+ let user_op_typed_data = user_op_hash_eip712_request(domain, user_op_packed.build_struct());
+ let user_op_typed_hash = hash_typed_data(user_op_typed_data)
+ .map_err(|e| format!("Typed hash failed: {e}"))?;
+
+ println!("user_op_typed_hash={}", user_op_typed_hash.to_hex());
+ // Sign the hash with KeyPair
+ let sig = sign(key_pair.secret(), &user_op_typed_hash).map_err(|e| format!("Signing failed: {e}"))?.into_electrum();
+ //println!("signature: r={:?} s={:?} v={} is_low_s={}", sig.r(), sig.s(), sig.v(), sig.is_low_s());
+ user_op_packed.signature = sig.to_vec().into();
+ },
+ EthPrivKeyPolicy::Trezor
+ | EthPrivKeyPolicy::WalletConnect { .. } => return Err("External signers (WalletConnect/Trezor) are not implemented for userOps yet".into()),
+ #[cfg(target_arch = "wasm32")]
+ EthPrivKeyPolicy::Metamask(_) => return Err("MetaMask is not implemented for userOps yet".into()),
+ }
+
+
+
+ // Submit via eth_sendUserOperation RPC. Many bundlers accept params: [userOp, entryPoint]
+ // We try to call `eth_sendUserOperation` with the packed userop serialized to JSON.
+ let userop_j = serde_json::to_value(&user_op_packed).map_err(|e| format!("Failed to serialize userop: {e}"))?;
+
+ // Use configured entry point if present, otherwise omit (many bundlers accept two-arg form)
+ //let entry_point = helpers::serialize(&self.entry_point_address);
+
+ // Call the RPC and return the result. Wrap web3::Error into String for simplicity.
+ //let rpc_params = vec![userop_json, serde_json::Value::String(self.entry_point_address.to_string())];
+
+ let resp_json = Eip4337Rpc::send_user_operation(eip4337_rpc, userop_j, *EIP_4337_ENTRY_POINT_8)
+ .await
+ .map_err(|err| format!("RPC error sending userOp: {err}"))?;
+ /*let user_op_resp = serde_json::from_value::(resp_j)
+ .map_err(|err| format!("Error parsing userOp RPC response: {err}"))?;
+ let user_op_hash = H256::from_str(&hex::encode(user_op_resp.user_op_hash.0))
+ .map_err(|err| format!("Error parsing userOp hash in RPC response: {err}"))?;*/
+ let user_op_resp = resp_json.as_str()
+ .ok_or(format!("Error parsing userOpHash response: expected string"))?;
+ let user_op_hash = H256::from_str(user_op_resp)
+ .map_err(|err| format!("Error parsing userOpHash response: {err}"))?;
+ Ok(user_op_hash)
+ }
+}
+
+pub(crate) fn user_op_hash_eip712_request(
+ domain: Eip712Domain,
+ req: PackedUserOperationTyped,
+) -> Eip712 {
+ let mut v_types = vec![EIP712_DOMAIN_TYPES.deref()];
+ v_types.extend(PACKED_USEROP_TYPES.deref());
+ let types = v_types
+ .iter()
+ .map(|&object_type| (object_type.name.clone(), object_type.properties.clone()))
+ .collect();
+ Eip712 {
+ types,
+ domain,
+ primary_type: PACKED_USEROP_PRIMARY_TYPE.to_string(),
+ message: req,
+ }
+}
+
+fn make_init_code(_delegate_address: Address) -> Bytes {
+ todo!()
+}
+
+fn make_bytes32_from_two(u1: U256, u2: U256) -> Bytes {
+ let mut u1_bytes = [0_u8; 32];
+ let mut u2_bytes = [0_u8; 32];
+ let mut bytes_32 = [0_u8; 32];
+ u1.to_big_endian(&mut u1_bytes);
+ u2.to_big_endian(&mut u2_bytes);
+ bytes_32[0..16].copy_from_slice(&u1_bytes[16..32]);
+ bytes_32[16..32].copy_from_slice(&u2_bytes[16..32]);
+ bytes_32.into()
+}
+
+
+mod tests {
+ use super::*;
+ use crate::lp_coininit;
+ use common::block_on;
+ use ethabi::Address;
+ use mm2_core::mm_ctx::MmCtxBuilder;
+ use mm2_test_helpers::for_tests::{ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES};
+
+ #[test]
+ fn sepolia_delegate_to_safe() {
+ const ETH: &str = "ETH";
+ const ETH_TOKEN: &str = "JST";
+
+ let conf = json!({
+ "coins": [{
+ "coin": "ETH",
+ "name": "ethereum-sepolia",
+ "fname": "Ethereum",
+ "rpcport": 80,
+ "mm2": 1,
+ "sign_message_prefix": "Ethereum Signed Message:\n",
+ "required_confirmations": 3,
+ "avg_blocktime": 15,
+ "protocol": {
+ "type": "ETH",
+ "protocol_data": {
+ "chain_id": ETH_SEPOLIA_CHAIN_ID
+ }
+ },
+ "derivation_path": "m/44'/60'"
+ },{
+ "coin": "JST",
+ "name": "Just Token",
+ "fname": "jst",
+ "rpcport": 80,
+ "mm2": 1,
+ "avg_blocktime": 15,
+ "required_confirmations": 3,
+ "decimals": 18,
+ "protocol": {
+ "type": "ERC20",
+ "protocol_data": {
+ "platform": "ETH",
+ "contract_address": "0x948BF5172383F1Bc0Fdf3aBe0630b855694A5D2c"
+ }
+ },
+ "derivation_path": "m/44'/60'"
+ }]
+ });
+
+ let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc();
+ CryptoCtx::init_with_iguana_passphrase(
+ ctx.clone(),
+ "hen garden proud labor donkey cluster shield jazz worry category pelican immune body letter green badge face more apology smile estate ridge fall armor", // TODO: don't push
+ )
+ .unwrap();
+
+ let eth_params = json!({
+ "urls": ETH_SEPOLIA_NODES,
+ "eip4337_url": "https://api.pimlico.io/v2/11155111/rpc?apikey=PIMLICO-KEY-HERE",
+ "swap_contract_address": "0x9130b257d37a52e52f21054c4da3450c72f595ce",
+ });
+ let coin = match block_on(lp_coininit(&ctx, ETH, ð_params)).unwrap() {
+ MmCoinEnum::EthCoin(coin) => coin,
+ _ => panic!("incorrect coin type"),
+ };
+
+ let res = block_on(coin.delegate_to_safe_account(Address::zero(), Bytes::from(Vec::new())));
+ println!("res={:?}", res);
+ }
+}
\ No newline at end of file
diff --git a/mm2src/coins/eth/eth_utils.rs b/mm2src/coins/eth/eth_utils.rs
index c2c67644e0..fdfdffa54d 100644
--- a/mm2src/coins/eth/eth_utils.rs
+++ b/mm2src/coins/eth/eth_utils.rs
@@ -18,6 +18,8 @@ pub(super) const GAS_PRICE_ADJUST: &str = "gas_price_adjust";
pub(super) const ESTIMATE_GAS_MULT: &str = "estimate_gas_mult";
/// Coin config parameter name for the default eth swap gas fee policy
pub(super) const SWAP_GAS_FEE_POLICY: &str = "swap_gas_fee_policy";
+/// Bundler service url
+pub(super) const EIP4337_URL: &str = "eip4337_url";
pub(crate) mod nonce_sequencer {
use super::*;
diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs
index 42337fe356..599cb1e642 100644
--- a/mm2src/coins/eth/for_tests.rs
+++ b/mm2src/coins/eth/for_tests.rs
@@ -96,6 +96,7 @@ pub(crate) fn eth_coin_from_keypair(
contract_supports_watchers: false,
ticker,
web3_instances: AsyncMutex::new(web3_instances),
+ eip4337_rpc: None,
ctx: ctx.weak(),
required_confirmations: 1.into(),
swap_gas_fee_policy: Mutex::new(swap_gas_fee_policy),
diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs
index 702946dbbe..275e7b85ae 100644
--- a/mm2src/coins/eth/v2_activation.rs
+++ b/mm2src/coins/eth/v2_activation.rs
@@ -539,6 +539,7 @@ impl EthCoin {
decimals,
ticker,
web3_instances: AsyncMutex::new(self.web3_instances.lock().await.clone()),
+ eip4337_rpc: None,
history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()),
swap_gas_fee_policy: Mutex::new(swap_gas_fee_policy),
max_eth_tx_type,
@@ -635,6 +636,7 @@ impl EthCoin {
fallback_swap_contract: self.fallback_swap_contract,
contract_supports_watchers: self.contract_supports_watchers,
web3_instances: AsyncMutex::new(self.web3_instances.lock().await.clone()),
+ eip4337_rpc: None,
decimals: self.decimals,
history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()),
swap_gas_fee_policy: Mutex::new(swap_gas_fee_policy),
@@ -779,6 +781,7 @@ pub async fn eth_coin_from_conf_and_request_v2(
decimals: ETH_DECIMALS,
ticker: ticker.to_string(),
web3_instances: AsyncMutex::new(web3_instances),
+ eip4337_rpc: None,
history_sync_state: Mutex::new(HistorySyncState::NotEnabled),
swap_gas_fee_policy: Mutex::new(swap_gas_fee_policy),
max_eth_tx_type,
diff --git a/mm2src/mm2_eth/src/eip712.rs b/mm2src/mm2_eth/src/eip712.rs
index bca185225f..3d3760da6a 100644
--- a/mm2src/mm2_eth/src/eip712.rs
+++ b/mm2src/mm2_eth/src/eip712.rs
@@ -5,7 +5,7 @@ use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
-pub(crate) const EIP712_DOMAIN: &str = "EIP712Domain";
+pub const EIP712_DOMAIN: &str = "EIP712Domain";
pub(crate) type CustomTypes = HashMap>;
@@ -36,7 +36,7 @@ pub(crate) type CustomTypes = HashMap>;
/// let mut mail_type = ObjectType::new("Mail");
/// mail_type.property("message", PropertyType::String);
/// mail_type.property("from", PropertyType::Custom("Person".into()));
-/// mail_type.property_array("to", PropertyType::Custom("Person".into()));
+/// mail_type.property("to", PropertyType::Custom("Person".into()));
///
/// let mut person_type = ObjectType::new("Person");
/// person_type.property("address", PropertyType::Address);
@@ -85,6 +85,7 @@ pub enum PropertyType {
String,
Uint256,
Address,
+ Bytes,
Bytes32,
Custom(String),
}
@@ -96,6 +97,7 @@ impl fmt::Display for PropertyType {
PropertyType::String => write!(f, "string"),
PropertyType::Uint256 => write!(f, "uint256"),
PropertyType::Address => write!(f, "address"),
+ PropertyType::Bytes => write!(f, "bytes"),
PropertyType::Bytes32 => write!(f, "bytes32"),
PropertyType::Custom(custom) => write!(f, "{custom}"),
}
@@ -109,6 +111,7 @@ impl FromStr for PropertyType {
let property_type = match s {
"bool" => PropertyType::Bool,
"string" => PropertyType::String,
+ "bytes" => PropertyType::Bytes,
"uint256" => PropertyType::Uint256,
"address" => PropertyType::Address,
"bytes32" => PropertyType::Bytes32,
diff --git a/mm2src/mm2_eth/src/eip712_encode.rs b/mm2src/mm2_eth/src/eip712_encode.rs
index a83873e94b..0d9e60df05 100644
--- a/mm2src/mm2_eth/src/eip712_encode.rs
+++ b/mm2src/mm2_eth/src/eip712_encode.rs
@@ -80,6 +80,7 @@ fn encode_data(
match data_type {
PropertyType::Bool => encode_bool(data, field_name),
PropertyType::String => encode_string(data, field_name),
+ PropertyType::Bytes => encode_bytes(data, field_name),
PropertyType::Uint256 => encode_u256(data, field_name),
PropertyType::Address => encode_address(data, field_name),
PropertyType::Bytes32 => encode_bytes32(data, field_name),
@@ -114,12 +115,13 @@ fn encode_bytes32(value: &Json, field_name: Option<&str>) -> Result> {
let string = value
.as_str()
.ok_or_else(|| expected_type_error("bytes32", value, field_name))?;
- check_hex(string, field_name)?;
+ println!("encode_bytes32 string={}", string);
+ check_hex_prefix(&string, field_name)?;
+ check_hex_len(&string[2..], field_name, 32)?;
- let bytes = hex::decode(string).map_err(|e| decode_error(e, field_name))?;
- let hash = keccak256(&bytes).to_vec();
+ let bytes = hex::decode(&string[2..]).map_err(|e| decode_error(e, field_name))?;
- Ok(encode(&[Token::FixedBytes(hash)]))
+ Ok(encode(&[Token::FixedBytes(bytes)]))
}
fn encode_string(value: &Json, field_name: Option<&str>) -> Result> {
@@ -131,6 +133,17 @@ fn encode_string(value: &Json, field_name: Option<&str>) -> Result> {
Ok(encode(&[Token::FixedBytes(hash)]))
}
+fn encode_bytes(value: &Json, field_name: Option<&str>) -> Result> {
+ let string = value
+ .as_str()
+ .ok_or_else(|| expected_type_error("bytes", value, field_name))?;
+ check_hex_prefix(string, field_name)?;
+ let bytes = hex::decode(&string[2..]).map_err(|e| decode_error(e, field_name))?;
+ let hash = keccak256(bytes.as_ref()).to_vec();
+
+ Ok(encode(&[Token::FixedBytes(hash)]))
+}
+
fn encode_bool(value: &Json, field_name: Option<&str>) -> Result> {
let bin = value
.as_bool()
@@ -154,7 +167,7 @@ fn encode_u256(value: &Json, field_name: Option<&str>) -> Result> {
let string = value
.as_str()
.ok_or_else(|| expected_type_error("str(u256)", value, field_name))?;
- check_hex(string, field_name)?;
+ check_hex_prefix(string, field_name)?;
let uint = U256::from_str(&string[2..]).map_err(|e| decode_error(e, field_name))?;
Ok(encode(&[Token::Uint(uint)]))
@@ -224,7 +237,7 @@ fn build_dependencies<'a>(data_type: &'a str, custom_types: &'a CustomTypes) ->
Some(deps)
}
-fn check_hex(string: &str, field_name: Option<&str>) -> Result<()> {
+fn check_hex_prefix(string: &str, field_name: Option<&str>) -> Result<()> {
if string.len() >= 2 && &string[..2] == "0x" {
return Ok(());
}
@@ -238,6 +251,20 @@ fn check_hex(string: &str, field_name: Option<&str>) -> Result<()> {
))
}
+fn check_hex_len(string: &str, field_name: Option<&str>, len: usize) -> Result<()> {
+ if string.len() / 2 == len {
+ return Ok(());
+ }
+
+ Err(decode_error(
+ format!(
+ "Expected size of {}",
+ len
+ ),
+ field_name,
+ ))
+}
+
fn expected_type_error(expected: &str, found: &Json, field_name: Option<&str>) -> Error {
decode_error(format!("Expected '{expected}' type, found '{found}'"), field_name)
}