From 7e832cade39fc5e96c907646bb2fdacd4ee17252 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 3 Sep 2024 13:48:13 -0700 Subject: [PATCH] fix: docker service dependencies --- .github/workflows/ci.yml | 7 +- crates/yttrium/contracts/Account7702.sol | 21 ++ crates/yttrium/src/test_helpers.rs | 20 ++ .../yttrium/src/transaction/send/safe_test.rs | 330 ++++++++++++++++++ 4 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 crates/yttrium/contracts/Account7702.sol create mode 100644 crates/yttrium/src/test_helpers.rs create mode 100644 crates/yttrium/src/transaction/send/safe_test.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ed3e78..6b02ec3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,12 @@ jobs: - run: rustup update stable && rustup default stable - run: rustup toolchain install nightly -c rustfmt - run: git submodule update --init --recursive - - run: make setup-thirdparty local-infra-forked + - run: make setup-thirdparty + - run: docker-compose up -d + working-directory: test/scripts/forked_state + - run: while ! nc -z 127.0.0.1 8545; do sleep 1; done + - run: while ! nc -z 127.0.0.1 4337; do sleep 1; done + - run: while ! nc -z 127.0.0.1 3000; do sleep 1; done - run: cargo test --all-features --lib --bins # - run: cargo clippy --workspace --all-features --all-targets -- -D warnings # - run: cargo +nightly fmt --all -- --check diff --git a/crates/yttrium/contracts/Account7702.sol b/crates/yttrium/contracts/Account7702.sol new file mode 100644 index 0000000..0ac0fd7 --- /dev/null +++ b/crates/yttrium/contracts/Account7702.sol @@ -0,0 +1,21 @@ +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/src/test_helpers.rs b/crates/yttrium/src/test_helpers.rs new file mode 100644 index 0000000..f136fa4 --- /dev/null +++ b/crates/yttrium/src/test_helpers.rs @@ -0,0 +1,20 @@ +fn format_foundry_dir(path: &str) -> String { + format!( + "{}/../../../../.foundry/{}", + std::env::var("OUT_DIR").unwrap(), + path + ) +} + +pub fn spawn_anvil() -> (AnvilInstance, String, ReqwestProvider, SigningKey) { + let anvil = Anvil::at(format_foundry_dir("bin/anvil")).spawn(); + let rpc_url = anvil.endpoint(); + let provider = ReqwestProvider::::new_http(anvil.endpoint_url()); + let private_key = anvil.keys().first().unwrap().clone(); + ( + anvil, + rpc_url, + provider, + SigningKey::from_bytes(&private_key.to_bytes()).unwrap(), + ) +} 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..831c97d --- /dev/null +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -0,0 +1,330 @@ +use crate::{ + sign_service::SignService, transaction::Transaction, + user_operation::UserOperationV07, +}; +use core::fmt; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct UserOperationEstimated(UserOperationV07); + +impl Into for UserOperationEstimated { + fn into(self) -> UserOperationV07 { + self.0 + } +} + +#[derive(Debug, Clone)] +pub struct SignedUserOperation(UserOperationV07); + +impl Into for SignedUserOperation { + fn into(self) -> UserOperationV07 { + self.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) + } +} + +pub async fn send_transaction( + sign_service: Arc>, + transaction: Transaction, +) -> eyre::Result { + let _ = sign_service.try_lock()?; + todo!("Calling send_transaction with transaction: {transaction:?}") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + bundler::{ + client::BundlerClient, + config::BundlerConfig, + pimlico::{ + client::BundlerClient as PimlicoBundlerClient, + paymaster::client::PaymasterClient, + }, + }, + entry_point::get_sender_address::get_sender_address_v07, + signer::sign_user_operation_v07_with_ecdsa, + smart_accounts::{ + nonce::get_nonce, + simple_account::{ + create_account::SimpleAccountCreate, factory::FactoryAddress, + SimpleAccountAddress, SimpleAccountExecute, + }, + }, + user_operation::UserOperationV07, + }; + use alloy::{ + network::EthereumWallet, + primitives::{Address, Bytes, U256}, + providers::ProviderBuilder, + signers::local::{ + coins_bip39::English, MnemonicBuilder, PrivateKeySigner, + }, + }; + use std::str::FromStr; + + const MNEMONIC_PHRASE: &str = + "test test test test test test test test test test test junk"; + + async fn send_transaction( + sign_service: Arc>, + transaction: Transaction, + ) -> eyre::Result { + let sign_service = sign_service.clone(); + let sign_service = sign_service.lock().await; + + 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 (ethereum_wallet, alloy_signer) = { + let phrase = MNEMONIC_PHRASE.to_string(); + let index: u32 = 0; + + let local_signer = { + let local_signer_result = MnemonicBuilder::::default() + .phrase(phrase.clone()) + .index(index)? + .build(); + let local_signer = match local_signer_result { + Ok(signer) => signer, + Err(e) => { + println!("Error creating signer: {}", e); + let local_signer: PrivateKeySigner = phrase.parse()?; + local_signer + } + }; + local_signer + }; + let ethereum_wallet = EthereumWallet::from(local_signer.clone()); + (ethereum_wallet, local_signer) + }; + + let rpc_url: reqwest::Url = rpc_url.parse()?; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(ethereum_wallet.clone()) + .on_http(rpc_url); + + let simple_account_factory_address_primitives: Address = + "0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985".parse()?; + let simple_account_factory_address = + FactoryAddress::new(simple_account_factory_address_primitives); + + let owner = ethereum_wallet.clone().default_signer(); + let owner_address = owner.address(); + let sign_service_owner = sign_service.owner(); + assert_eq!( + owner_address, sign_service_owner, + "Owner addresses don't match, should be {:?}, is {:?}", + owner_address, sign_service_owner + ); + + let factory_data_call = SimpleAccountCreate::new_u64(owner_address, 2); + + let factory_data_value = factory_data_call.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, + simple_account_factory_address.clone().into(), + factory_data_value.clone().into(), + entry_point_address.clone(), + ) + .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 call_data = SimpleAccountExecute::new(to, value, data); + let call_data_encoded = call_data.encode(); + let call_data_value_hex = hex::encode(call_data_encoded.clone()); + let call_data_value_hex_prefixed = format!("0x{}", call_data_value_hex); + + println!("Generated callData: {:?}", call_data_value_hex_prefixed); + + // 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: Some(simple_account_factory_address.to_address()), + factory_data: Some(factory_data_value.into()), + call_data: Bytes::from_str(&call_data_value_hex)?, + 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 signed_user_op = sign_user_operation_v07_with_ecdsa( + &sponsored_user_op.clone(), + &entry_point_address.to_address(), + chain_id, + alloy_signer, + )?; + + 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 mnemonic = MNEMONIC_PHRASE.to_string(); + + let sign_service = + crate::sign_service::SignService::mock_with_mnemonic( + mnemonic.clone(), + ) + .await; + + let sign_service_arc = Arc::new(Mutex::new(sign_service)); + + let transaction_hash = + send_transaction(sign_service_arc, transaction).await?; + + println!("Transaction sent: {}", transaction_hash); + + Ok(()) + } +}