diff --git a/.gitmodules b/.gitmodules index 84ec85f..cd675a5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "crates/yttrium/safe-modules"] path = crates/yttrium/safe-modules url = https://github.com/safe-global/safe-modules +[submodule "crates/yttrium/safe7579"] + path = crates/yttrium/safe7579 + url = https://github.com/rhinestonewtf/safe7579 diff --git a/crates/yttrium/Cargo.toml b/crates/yttrium/Cargo.toml index a2b1ca6..ade879b 100644 --- a/crates/yttrium/Cargo.toml +++ b/crates/yttrium/Cargo.toml @@ -13,6 +13,7 @@ alloy = { git = "https://github.com/alloy-rs/alloy", rev = "b000e16", features = "node-bindings", "rpc-types-trace", "signer-mnemonic", + "eip712", ] } # foundry-block-explorers = "0.2.3" getrandom = { version = "0.2", features = ["js"] } diff --git a/crates/yttrium/contracts/Account7702.sol b/crates/yttrium/contracts/Account7702.sol deleted file mode 100644 index 0ac0fd7..0000000 --- a/crates/yttrium/contracts/Account7702.sol +++ /dev/null @@ -1,21 +0,0 @@ -pragma solidity ^0.8.20; - -contract Account7702 { - constructor() {} // TODO need owner - - struct Call { - bytes data; - address to; - uint256 value; - bytes signature; // TODO proper type - } - - function execute(Call[] calldata calls) external payable { - // TODO how to authenticate signture - for (uint256 i = 0; i < calls.length; i++) { - Call memory call = calls[i]; - (bool success, ) = call.to.call{value: call.value}(call.data); - require(success, "call reverted"); - } - } -} diff --git a/crates/yttrium/safe7579 b/crates/yttrium/safe7579 new file mode 160000 index 0000000..144eb33 --- /dev/null +++ b/crates/yttrium/safe7579 @@ -0,0 +1 @@ +Subproject commit 144eb335f65997b6ac701e6390a274b3ca31c031 diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs index 2702f7d..74b9ecd 100644 --- a/crates/yttrium/src/smart_accounts/safe.rs +++ b/crates/yttrium/src/smart_accounts/safe.rs @@ -1,8 +1,7 @@ use alloy::{ - dyn_abi::DynSolValue, - primitives::{address, Address, Bytes, U256}, + primitives::{address, keccak256, Address, Bytes, Uint, U256}, sol, - sol_types::SolCall, + sol_types::{SolCall, SolValue}, }; sol!( @@ -17,27 +16,69 @@ sol!( sol!( #[allow(clippy::too_many_arguments)] #[allow(missing_docs)] - #[sol(rpc)] + #[sol(rpc, abi)] Safe, "safe-smart-account/build/artifacts/contracts/Safe.sol/Safe.json" ); -// https://github.com/WalletConnect/secure-web3modal/blob/c19a1e7b21c6188261728f4d521a17f94da4f055/src/core/SmartAccountSdk/utils.ts#L178 -// https://github.com/WalletConnect/secure-web3modal/blob/c19a1e7b21c6188261728f4d521a17f94da4f055/src/core/SmartAccountSdk/constants.ts#L24 -const SEPOLIA_SAFE_ERC_7579_LAUNCHPAD_ADDRESS: Address = +sol!( + #[allow(clippy::too_many_arguments)] + #[allow(missing_docs)] + #[sol(rpc)] + Safe7579Launchpad, + "safe7579/artifacts/Safe7579Launchpad.json" +); + +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + Safe7579, + "safe7579/artifacts/Safe7579.json" +); + +// Had to copy from safe7579/artifacts/interfaces/IERC7579Account.json +// This struct doesn't seem to be in generated ABIs +sol!( + #[allow(missing_docs)] + #[sol(rpc, abi)] + struct Execution { + address target; + uint256 value; + bytes callData; + } +); + +// https://github.com/WalletConnect/secure-web3modal/blob/f1d16f973a313e598d124a0e4751aee12d5de628/src/core/SmartAccountSdk/utils.ts#L180 +pub const SAFE_ERC_7579_LAUNCHPAD_ADDRESS: Address = address!("EBe001b3D534B9B6E2500FB78E67a1A137f561CE"); -const SEPOLIA_SAFE_4337_MODULE_ADDRESS: Address = +// https://github.com/WalletConnect/secure-web3modal/blob/f1d16f973a313e598d124a0e4751aee12d5de628/src/core/SmartAccountSdk/utils.ts#L181 +// https://docs.pimlico.io/permissionless/how-to/accounts/use-erc7579-account +// https://docs.safe.global/advanced/erc-7579/tutorials/7579-tutorial +pub const SAFE_4337_MODULE_ADDRESS: Address = address!("3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2"); +// // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L436 +pub const SEPOLIA_SAFE_ERC_7579_SINGLETON_ADDRESS: Address = + address!("41675C099F32341bf84BFc5382aF534df5C7461a"); + +// https://github.com/safe-global/safe-modules-deployments/blob/d6642d90659de19e54bb4a20d646b30bd0a51885/src/assets/safe-4337-module/v0.3.0/safe-4337-module.json#L7 +// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L432 +// const SEPOLIA_SAFE_4337_MODULE_ADDRESS: Address = +// address!("75cf11467937ce3F2f357CE24ffc3DBF8fD5c226"); + // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L438C36-L438C76 -const SAFE_MULTI_SEND_ADDRESS: Address = - address!("38869bf66a61cF6bDB996A6aE40D5853Fd43B526"); +// Only used for non-ERC-7579 accounts +// const SAFE_MULTI_SEND_ADDRESS: Address = +// address!("38869bf66a61cF6bDB996A6aE40D5853Fd43B526"); // https://github.com/safe-global/safe-modules-deployments/blob/d6642d90659de19e54bb4a20d646b30bd0a51885/src/assets/safe-4337-module/v0.3.0/safe-module-setup.json#L7 // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L431 -const SAFE_MODULE_SETUP_ADDRESS: Address = +const _SAFE_MODULE_SETUP_ADDRESS: Address = address!("2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"); +pub const SAFE_PROXY_FACTORY_ADDRESS: Address = + address!("4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"); + sol!( #[allow(missing_docs)] #[sol(rpc)] @@ -55,60 +96,51 @@ sol!( // https://github.com/WalletConnect/secure-web3modal/blob/c19a1e7b21c6188261728f4d521a17f94da4f055/src/core/SmartAccountSdk/constants.ts#L10 // const APPKIT_SALT: U256 = U256::from_str("zg3ijy0p46"); -fn encode_internal_transaction( - to: Address, - data: Vec, - value: U256, - operation: bool, -) -> Bytes { - // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L486 - DynSolValue::Tuple(vec![ - DynSolValue::Uint(U256::from(if operation { 1 } else { 0 }), 8), - DynSolValue::Address(to), - DynSolValue::Uint(value, 256), - DynSolValue::Uint(U256::from(data.len()), 256), - DynSolValue::Bytes(data), - ]) - .abi_encode() - .into() +pub fn init_data() -> Safe7579Launchpad::initSafe7579Call { + Safe7579Launchpad::initSafe7579Call { + safe7579: SAFE_4337_MODULE_ADDRESS, + executors: vec![], + fallbacks: vec![], + hooks: vec![], + attesters: vec![], + threshold: 0, + } +} + +#[derive(Debug, Clone)] +pub struct Owners { + pub owners: Vec
, + pub threshold: u8, } -fn init_code_call_data( - owner: Address, +pub fn factory_data( + owners: Owners, ) -> SafeProxyFactory::createProxyWithNonceCall { - // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L714C31-L714C46 - let enable_modules = SafeModuleSetup::enableModulesCall { - modules: vec![SEPOLIA_SAFE_4337_MODULE_ADDRESS], - } - .abi_encode(); - - // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L486 - let txn = encode_internal_transaction( - SAFE_MODULE_SETUP_ADDRESS, - enable_modules, - U256::ZERO, - true, - ); // TODO join any setupTransactions - - let multi_send_call_data = - MultiSend::multiSendCall { transactions: txn }.abi_encode().into(); - - // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L728 - let initializer = Safe::setupCall { - _owners: vec![owner], - _threshold: U256::from(1), - to: SAFE_MULTI_SEND_ADDRESS, - data: multi_send_call_data, - fallbackHandler: SAFE_MODULE_SETUP_ADDRESS, - paymentToken: Address::ZERO, - payment: U256::ZERO, - paymentReceiver: Address::ZERO, + let init_hash = keccak256( + Safe7579Launchpad::InitData { + singleton: SEPOLIA_SAFE_ERC_7579_SINGLETON_ADDRESS, + owners: owners.owners, + threshold: Uint::from(owners.threshold), + setupTo: SAFE_ERC_7579_LAUNCHPAD_ADDRESS, + setupData: init_data().abi_encode().into(), + safe7579: SAFE_4337_MODULE_ADDRESS, + validators: vec![], + callData: Bytes::new(), + } + .abi_encode(), + ); + + let initializer = Safe7579Launchpad::preValidationSetupCall { + initHash: init_hash, + to: Address::ZERO, + preInit: Bytes::new(), } .abi_encode() .into(); + // https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L840 SafeProxyFactory::createProxyWithNonceCall { - _singleton: SEPOLIA_SAFE_ERC_7579_LAUNCHPAD_ADDRESS, + _singleton: SAFE_ERC_7579_LAUNCHPAD_ADDRESS, initializer, saltNonce: U256::ZERO, } diff --git a/crates/yttrium/src/transaction/send.rs b/crates/yttrium/src/transaction/send.rs index c8eab62..f93202b 100644 --- a/crates/yttrium/src/transaction/send.rs +++ b/crates/yttrium/src/transaction/send.rs @@ -6,6 +6,7 @@ use core::fmt; use std::sync::Arc; use tokio::sync::Mutex; +mod safe_test; mod simple_account_test; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs new file mode 100644 index 0000000..8009159 --- /dev/null +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -0,0 +1,457 @@ +use crate::user_operation::UserOperationV07; +use core::fmt; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct UserOperationEstimated(UserOperationV07); + +impl From for UserOperationV07 { + fn from(val: UserOperationEstimated) -> Self { + val.0 + } +} + +#[derive(Debug, Clone)] +pub struct SignedUserOperation(UserOperationV07); + +impl From for UserOperationV07 { + fn from(val: SignedUserOperation) -> Self { + val.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct SentUserOperationHash(String); + +impl From for String { + fn from(user_operation_hash: SentUserOperationHash) -> Self { + user_operation_hash.0 + } +} + +impl fmt::Display for SentUserOperationHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + bundler::{ + client::BundlerClient, + config::BundlerConfig, + pimlico::{ + client::BundlerClient as PimlicoBundlerClient, + paymaster::client::PaymasterClient, + }, + }, + entry_point::get_sender_address::get_sender_address_v07, + smart_accounts::{ + nonce::get_nonce, + safe::{ + factory_data, Execution, Owners, Safe7579, Safe7579Launchpad, + SAFE_4337_MODULE_ADDRESS, SAFE_ERC_7579_LAUNCHPAD_ADDRESS, + SAFE_PROXY_FACTORY_ADDRESS, + SEPOLIA_SAFE_ERC_7579_SINGLETON_ADDRESS, + }, + simple_account::{factory::FactoryAddress, SimpleAccountAddress}, + }, + transaction::Transaction, + user_operation::UserOperationV07, + }; + use alloy::{ + dyn_abi::{DynSolValue, Eip712Domain}, + network::EthereumWallet, + primitives::{Address, Bytes, FixedBytes, Uint, U128, U256}, + providers::ProviderBuilder, + signers::{local::LocalSigner, SignerSync}, + sol, + sol_types::{SolCall, SolValue}, + }; + use std::{ops::Not, str::FromStr}; + + async fn send_transaction( + transaction: Transaction, + ) -> eyre::Result { + let config = crate::config::Config::local(); + + let bundler_base_url = config.endpoints.bundler.base_url; + let paymaster_base_url = config.endpoints.paymaster.base_url; + + let bundler_client = + BundlerClient::new(BundlerConfig::new(bundler_base_url.clone())); + + let pimlico_client: PimlicoBundlerClient = PimlicoBundlerClient::new( + BundlerConfig::new(bundler_base_url.clone()), + ); + + let chain = crate::chain::Chain::ETHEREUM_SEPOLIA_V07; + let entry_point_config = chain.entry_point_config(); + + let chain_id = chain.id.eip155_chain_id()?; + + let entry_point_address = entry_point_config.address(); + + let rpc_url = config.endpoints.rpc.base_url; + + // Create a provider + + let alloy_signer = LocalSigner::random(); + let ethereum_wallet = EthereumWallet::new(alloy_signer.clone()); + + let rpc_url: reqwest::Url = rpc_url.parse()?; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(ethereum_wallet.clone()) + .on_http(rpc_url); + + let safe_factory_address_primitives: Address = + SAFE_PROXY_FACTORY_ADDRESS; + let safe_factory_address = + FactoryAddress::new(safe_factory_address_primitives); + + let owner = ethereum_wallet.clone().default_signer(); + let owner_address = owner.address(); + let owners = Owners { owners: vec![owner_address], threshold: 1 }; + + let factory_data_value = factory_data(owners.clone()).abi_encode(); + + let factory_data_value_hex = hex::encode(factory_data_value.clone()); + + let factory_data_value_hex_prefixed = + format!("0x{}", factory_data_value_hex); + + println!( + "Generated factory_data: {:?}", + factory_data_value_hex_prefixed.clone() + ); + + // 5. Calculate the sender address + + let sender_address = get_sender_address_v07( + &provider, + safe_factory_address.into(), + factory_data_value.clone().into(), + entry_point_address, + ) + .await?; + + println!("Calculated sender address: {:?}", sender_address); + + let to: Address = transaction.to.parse()?; + let value: alloy::primitives::Uint<256, 4> = + transaction.value.parse()?; + let data_hex = transaction.data.strip_prefix("0x").unwrap(); + let data: Bytes = Bytes::from_str(data_hex)?; + + let execution_calldata = + vec![Execution { target: to, value, callData: data }]; + + let call_type = if execution_calldata.len() > 1 { + CallType::BatchCall + } else { + CallType::Call + }; + let revert_on_error = false; + let selector = [0u8; 4]; + let context = [0u8; 22]; + + enum CallType { + Call, + BatchCall, + #[allow(dead_code)] + DelegateCall, + } + impl CallType { + fn as_byte(&self) -> u8 { + match self { + CallType::Call => 0x00, + CallType::BatchCall => 0x01, + CallType::DelegateCall => 0xff, + } + } + } + + let mut mode = Vec::with_capacity(32); + mode.push(call_type.as_byte()); + mode.push(if revert_on_error { 0x01 } else { 0x00 }); + mode.extend_from_slice(&[0u8; 4]); + mode.extend_from_slice(&selector); + mode.extend_from_slice(&context); + let mode = FixedBytes::from_slice(&mode); + + let call_data = Safe7579::executeCall { + mode, + executionCalldata: execution_calldata.abi_encode().into(), + } + .abi_encode() + .into(); + + let deployed = false; + // permissionless: signerToSafeSmartAccount -> encodeCallData + let call_data = if deployed { + call_data + } else { + Safe7579Launchpad::setupSafeCall { + initData: Safe7579Launchpad::InitData { + singleton: SEPOLIA_SAFE_ERC_7579_SINGLETON_ADDRESS, + owners: owners.owners, + threshold: U256::from(owners.threshold), + setupTo: SAFE_ERC_7579_LAUNCHPAD_ADDRESS, + setupData: Safe7579Launchpad::initSafe7579Call { + safe7579: SAFE_4337_MODULE_ADDRESS, + executors: vec![], + fallbacks: vec![], + hooks: vec![], + attesters: vec![], + threshold: 0, + } + .abi_encode() + .into(), + safe7579: SAFE_4337_MODULE_ADDRESS, + callData: call_data, + validators: vec![], + }, + } + .abi_encode() + .into() + }; + + // Fill out remaining UserOperation values + + let gas_price = + pimlico_client.estimate_user_operation_gas_price().await?; + + assert!(gas_price.fast.max_fee_per_gas > U256::from(1)); + + println!("Gas price: {:?}", gas_price); + + let nonce = get_nonce( + &provider, + &SimpleAccountAddress::new(sender_address), + &entry_point_address, + ) + .await?; + + let user_op = UserOperationV07 { + sender: sender_address, + nonce: U256::from(nonce), + factory: deployed.not().then(|| safe_factory_address.to_address()), + factory_data: deployed.not().then(|| factory_data_value.into()), + call_data, + call_gas_limit: U256::from(0), + verification_gas_limit: U256::from(0), + pre_verification_gas: U256::from(0), + max_fee_per_gas: gas_price.fast.max_fee_per_gas, + max_priority_fee_per_gas: gas_price.fast.max_priority_fee_per_gas, + paymaster: None, + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, + paymaster_data: None, + signature: Bytes::from_str( + crate::smart_accounts::simple_account::DUMMY_SIGNATURE_HEX + .strip_prefix("0x") + .unwrap(), + )?, + }; + + let paymaster_client = PaymasterClient::new(BundlerConfig::new( + paymaster_base_url.clone(), + )); + + let sponsor_user_op_result = paymaster_client + .sponsor_user_operation_v07( + &user_op.clone().into(), + &entry_point_address, + None, + ) + .await?; + + println!("sponsor_user_op_result: {:?}", sponsor_user_op_result); + + let sponsored_user_op = { + let s = sponsor_user_op_result.clone(); + let mut op = user_op.clone(); + + op.call_gas_limit = s.call_gas_limit; + op.verification_gas_limit = s.verification_gas_limit; + op.pre_verification_gas = s.pre_verification_gas; + op.paymaster = Some(s.paymaster); + op.paymaster_verification_gas_limit = + Some(s.paymaster_verification_gas_limit); + op.paymaster_post_op_gas_limit = + Some(s.paymaster_post_op_gas_limit); + op.paymaster_data = Some(s.paymaster_data); + + op + }; + + println!("Received paymaster sponsor result: {:?}", sponsored_user_op); + + // Sign the UserOperation + + let valid_after = 0; + let valid_until = 0; + + sol!( + struct SafeOpEntrypointV7Message { + address safe; + uint256 nonce; + bytes initCode; + bytes callData; + uint128 verificationGasLimit; + uint128 callGasLimit; + uint256 preVerificationGas; + uint128 maxPriorityFeePerGas; + uint128 maxFeePerGas; + bytes paymasterAndData; + uint48 validAfter; + uint48 validUntil; + address entryPoint; + } + ); + + // TODO handle panic + fn coerce_u256_to_u128(u: U256) -> U128 { + U128::from(u) + } + + let message = SafeOpEntrypointV7Message { + safe: sender_address, + callData: sponsored_user_op.call_data.clone(), + nonce: sponsored_user_op.nonce, + initCode: deployed + .not() + .then(|| { + [ + sponsored_user_op.clone().factory_data.unwrap(), + sponsored_user_op + .clone() + .factory + .unwrap() + .to_vec() + .into(), + ] + .concat() + .into() + }) + .unwrap_or(Bytes::new()), + maxFeePerGas: u128::from_be_bytes( + coerce_u256_to_u128(sponsored_user_op.max_fee_per_gas) + .to_be_bytes(), + ), + maxPriorityFeePerGas: u128::from_be_bytes( + coerce_u256_to_u128(sponsored_user_op.max_priority_fee_per_gas) + .to_be_bytes(), + ), + preVerificationGas: sponsored_user_op.pre_verification_gas, + verificationGasLimit: u128::from_be_bytes( + coerce_u256_to_u128(sponsored_user_op.verification_gas_limit) + .to_be_bytes(), + ), + callGasLimit: u128::from_be_bytes( + coerce_u256_to_u128(sponsored_user_op.call_gas_limit) + .to_be_bytes(), + ), + // signerToSafeSmartAccount -> getPaymasterAndData + paymasterAndData: sponsored_user_op + .paymaster + .map(|paymaster| { + [ + paymaster.to_vec(), + sponsored_user_op + .paymaster_verification_gas_limit + .unwrap_or(Uint::from(0)) + .to_be_bytes_vec(), // TODO does this need to be padded (more)? + sponsored_user_op + .paymaster_post_op_gas_limit + .unwrap_or(Uint::from(0)) + .to_be_bytes_vec(), // TODO does this need to be padded (more)? + sponsored_user_op + .paymaster_post_op_gas_limit + .unwrap_or(Uint::from(0)) + .to_be_bytes_vec(), // TODO does this need to be not-padded? + ] + .concat() + .into() + }) + .unwrap_or(Bytes::new()), + validAfter: valid_after, + validUntil: valid_until, + entryPoint: entry_point_address.to_address(), + }; + + let erc7579_launchpad_address = true; + let verifying_contract = if erc7579_launchpad_address && !deployed { + sponsored_user_op.sender + } else { + SAFE_ERC_7579_LAUNCHPAD_ADDRESS + }; + + // TODO loop per-owner + let signature = alloy_signer.sign_typed_data_sync( + &message, + &Eip712Domain { + chain_id: Some(Uint::from(chain_id)), + verifying_contract: Some(verifying_contract), + ..Default::default() + }, + )?; + let mut signatures = + [signature].iter().map(|sig| sig.as_bytes()).collect::>(); + signatures.sort(); + let signature_bytes = signatures.concat(); + + let signature = DynSolValue::Tuple(vec![ + DynSolValue::Uint(Uint::from(valid_after), 48), + DynSolValue::Uint(Uint::from(valid_until), 48), + DynSolValue::Bytes(signature_bytes), + ]) + .abi_encode_packed() + .into(); + let signed_user_op = + UserOperationV07 { signature, ..sponsored_user_op }; + + println!("Generated signature: {:?}", signed_user_op.signature); + + let user_operation_hash = bundler_client + .send_user_operation( + entry_point_address.to_address(), + signed_user_op.clone(), + ) + .await?; + + println!("Received User Operation hash: {:?}", user_operation_hash); + + // let receipt = bundler_client + // .get_user_operation_receipt(user_operation_hash.clone()) + // .await?; + + // println!("Received User Operation receipt: {:?}", receipt); + + // println!("Querying for receipts..."); + + // let receipt = bundler_client + // .wait_for_user_operation_receipt(user_operation_hash.clone()) + // .await?; + + // let tx_hash = receipt.receipt.transaction_hash; + // println!( + // "UserOperation included: https://sepolia.etherscan.io/tx/{}", + // tx_hash + // ); + + Ok(user_operation_hash) + } + + #[tokio::test] + async fn test_send_transaction() -> eyre::Result<()> { + let transaction = Transaction::mock(); + + let transaction_hash = send_transaction(transaction).await?; + + println!("Transaction sent: {}", transaction_hash); + + Ok(()) + } +} diff --git a/test/scripts/forked_state/docker-compose.yaml b/test/scripts/forked_state/docker-compose.yaml index 1e52b1c..d9f5aa3 100644 --- a/test/scripts/forked_state/docker-compose.yaml +++ b/test/scripts/forked_state/docker-compose.yaml @@ -3,7 +3,7 @@ services: image: ghcr.io/foundry-rs/foundry:nightly-f6208d8db68f9acbe4ff8cd76958309efb61ea0b restart: unless-stopped ports: ["8545:8545"] - entrypoint: [ "anvil", "--fork-url", "https://gateway.tenderly.co/public/sepolia", "--host", "0.0.0.0", "--block-time", "0.1", "--gas-price", "1" ] + entrypoint: [ "anvil", "--fork-url", "https://gateway.tenderly.co/public/sepolia", "--host", "0.0.0.0" ] platform: linux/amd64/v8 mock-paymaster: