diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d583fc01..7bfc71e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 - run: rustup update stable && rustup default stable - run: rustup toolchain install nightly -c rustfmt - run: git submodule update --init --recursive - run: make setup-thirdparty + - run: docker compose up -d + working-directory: test/scripts/forked_state + - run: while ! curl localhost:8545/health; do sleep 1; done + - run: while ! curl localhost:4337/health; do sleep 1; done + - run: while ! curl localhost:3000/ping; 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 @@ -33,6 +41,9 @@ jobs: - debug steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 - run: rustup update stable && rustup default stable - run: git submodule update --init --recursive - run: make setup-thirdparty diff --git a/.gitmodules b/.gitmodules index fc5418d9..84ec85fd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "crates/yttrium/src/contracts"] path = crates/yttrium/src/contracts url = https://github.com/eth-infinitism/account-abstraction.git +[submodule "crates/yttrium/safe-smart-account"] + path = crates/yttrium/safe-smart-account + url = https://github.com/safe-global/safe-smart-account +[submodule "crates/yttrium/safe-modules"] + path = crates/yttrium/safe-modules + url = https://github.com/safe-global/safe-modules diff --git a/Makefile b/Makefile index 1a34dcc4..010ac529 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,8 @@ fetch-thirdparty: setup-thirdparty: cd crates/yttrium/src/contracts/ && yarn install --frozen-lockfile --immutable && yarn compile + cd crates/yttrium/safe-smart-account/ && npm install + cd crates/yttrium/safe-modules/ && pnpm install build-ios-bindings: sh crates/ffi/build-rust-ios.sh diff --git a/crates/ffi/src/config.rs b/crates/ffi/src/config.rs index 03125fb2..40793a55 100644 --- a/crates/ffi/src/config.rs +++ b/crates/ffi/src/config.rs @@ -14,6 +14,7 @@ impl Into for ffi::FFIEndpoints { yttrium::config::Endpoints { rpc: self.rpc.into(), bundler: self.bundler.into(), + paymaster: self.paymaster.into(), } } } diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs index d9aeaba4..e57e7913 100644 --- a/crates/ffi/src/lib.rs +++ b/crates/ffi/src/lib.rs @@ -31,6 +31,7 @@ mod ffi { pub struct FFIEndpoints { pub rpc: FFIEndpoint, pub bundler: FFIEndpoint, + pub paymaster: FFIEndpoint, } #[derive(Debug, Clone)] diff --git a/crates/yttrium/Cargo.toml b/crates/yttrium/Cargo.toml index 192c91f6..e3b40c77 100644 --- a/crates/yttrium/Cargo.toml +++ b/crates/yttrium/Cargo.toml @@ -37,3 +37,7 @@ hex = "0.4.3" [dev-dependencies] # mocking wiremock = "0.6.0" + +[build-dependencies] +alloy-primitives = { version = "0.7.0" } +serde_json = "1" diff --git a/crates/yttrium/build.rs b/crates/yttrium/build.rs new file mode 100644 index 00000000..a0f859da --- /dev/null +++ b/crates/yttrium/build.rs @@ -0,0 +1,107 @@ +use { + // serde_json::Value, + std::process::{Command, Stdio}, +}; + +fn main() { + build_contracts(); +} + +const CONTRACTS_DIR: &str = "crates/yttrium/safe-smart-account/contracts"; + +fn build_contracts() { + println!("cargo::rerun-if-changed={CONTRACTS_DIR}"); + install_foundry(); + compile_contracts(&format!("{CONTRACTS_DIR}/proxies")); + // extract_bytecodes(); +} + +fn format_foundry_dir(path: &str) -> String { + format!( + "{}/../../../../.foundry/{}", + std::env::var("OUT_DIR").unwrap(), + path + ) +} + +fn install_foundry() { + let bin_finished_flag = format_foundry_dir("bin/.finished"); + if std::fs::metadata(&bin_finished_flag).is_ok() { + return; + } + + let bin_folder = format_foundry_dir("bin"); + std::fs::remove_dir_all(&bin_folder).ok(); + std::fs::create_dir_all(&bin_folder).unwrap(); + let output = Command::new("bash") + .args(["-c", &format!("curl https://raw.githubusercontent.com/foundry-rs/foundry/e0ea59cae26d945445d9cf21fdf22f4a18ac5bb2/foundryup/foundryup | FOUNDRY_DIR={} bash", format_foundry_dir(""))]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + println!("foundryup status: {:?}", output.status); + let stdout = String::from_utf8(output.stdout).unwrap(); + println!("foundryup stdout: {stdout:?}"); + let stderr = String::from_utf8(output.stderr).unwrap(); + println!("foundryup stderr: {stderr:?}"); + assert!(output.status.success()); + + std::fs::write(bin_finished_flag, "").unwrap(); +} + +fn compile_contracts(contracts_dir: &str) { + let output = Command::new(format_foundry_dir("bin/forge")) + .args([ + "build", + &format!("--contracts={contracts_dir}"), + "--skip=test", + "--cache-path", + &format_foundry_dir("forge/cache"), + "--out", + &format_foundry_dir("forge/out"), + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + println!("forge status: {:?}", output.status); + let stdout = String::from_utf8(output.stdout).unwrap(); + println!("forge stdout: {stdout:?}"); + let stderr = String::from_utf8(output.stderr).unwrap(); + println!("forge stderr: {stderr:?}"); + assert!(output.status.success()); +} + +// const ERC6492_FILE: &str = "forge/out/Erc6492.sol/ValidateSigOffchain.json"; +// const ERC6492_BYTECODE_FILE: &str = "forge/out/Erc6492.sol/ValidateSigOffchain.bytecode"; +// const ERC1271_MOCK_FILE: &str = "forge/out/Erc1271Mock.sol/Erc1271Mock.json"; +// const ERC1271_MOCK_BYTECODE_FILE: &str = "forge/out/Erc1271Mock.sol/Erc1271Mock.bytecode"; +// fn extract_bytecodes() { +// extract_bytecode( +// &format_foundry_dir(ERC6492_FILE), +// &format_foundry_dir(ERC6492_BYTECODE_FILE), +// ); +// extract_bytecode( +// &format_foundry_dir(ERC1271_MOCK_FILE), +// &format_foundry_dir(ERC1271_MOCK_BYTECODE_FILE), +// ); +// } + +// fn extract_bytecode(input_file: &str, output_file: &str) { +// let contents = serde_json::from_slice::(&std::fs::read(input_file).unwrap()).unwrap(); +// let bytecode = contents +// .get("bytecode") +// .unwrap() +// .get("object") +// .unwrap() +// .as_str() +// .unwrap() +// .strip_prefix("0x") +// .unwrap(); +// let bytecode = alloy_primitives::hex::decode(bytecode).unwrap(); +// std::fs::write(output_file, bytecode).unwrap(); +// } diff --git a/crates/yttrium/contracts/Account7702.sol b/crates/yttrium/contracts/Account7702.sol new file mode 100644 index 00000000..0ac0fd72 --- /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/safe-modules b/crates/yttrium/safe-modules new file mode 160000 index 00000000..bc76ffd2 --- /dev/null +++ b/crates/yttrium/safe-modules @@ -0,0 +1 @@ +Subproject commit bc76ffd27a8a0c15c255ffcb61707bf7a30085eb diff --git a/crates/yttrium/safe-smart-account b/crates/yttrium/safe-smart-account new file mode 160000 index 00000000..c266ffc2 --- /dev/null +++ b/crates/yttrium/safe-smart-account @@ -0,0 +1 @@ +Subproject commit c266ffc2d75c8d6cd545cf24e54444b069793e53 diff --git a/crates/yttrium/src/config.rs b/crates/yttrium/src/config.rs index e8461fca..f271ec3e 100644 --- a/crates/yttrium/src/config.rs +++ b/crates/yttrium/src/config.rs @@ -3,6 +3,7 @@ use std::env; const LOCAL_RPC_URL: &str = "http://localhost:8545"; const LOCAL_BUNDLER_URL: &str = "http://localhost:4337"; +const LOCAL_PAYMASTER_URL: &str = "http://localhost:3000"; #[derive(Clone, Debug, PartialEq)] pub struct Config { @@ -23,6 +24,7 @@ impl Config { pub struct Endpoints { pub rpc: Endpoint, pub bundler: Endpoint, + pub paymaster: Endpoint, } impl Endpoints { @@ -45,13 +47,14 @@ impl Endpoints { Endpoint { api_key, base_url } }; - Endpoints { rpc, bundler } + Endpoints { rpc, paymaster: bundler.clone(), bundler } } pub fn local() -> Self { Endpoints { rpc: Endpoint::local_rpc(), bundler: Endpoint::local_bundler(), + paymaster: Endpoint::local_paymaster(), } } @@ -73,7 +76,7 @@ impl Endpoints { Endpoint { api_key: api_key.clone(), base_url } }; - Endpoints { rpc, bundler } + Endpoints { rpc, paymaster: bundler.clone(), bundler } } } @@ -97,4 +100,11 @@ impl Endpoint { api_key: "".to_string(), } } + + pub fn local_paymaster() -> Self { + Endpoint { + base_url: LOCAL_PAYMASTER_URL.to_string(), + api_key: "".to_string(), + } + } } diff --git a/crates/yttrium/src/smart_accounts.rs b/crates/yttrium/src/smart_accounts.rs index ffcd84ae..03862da5 100644 --- a/crates/yttrium/src/smart_accounts.rs +++ b/crates/yttrium/src/smart_accounts.rs @@ -1,2 +1,3 @@ pub mod nonce; +pub mod safe; pub mod simple_account; diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs new file mode 100644 index 00000000..2702f7d5 --- /dev/null +++ b/crates/yttrium/src/smart_accounts/safe.rs @@ -0,0 +1,115 @@ +use alloy::{ + dyn_abi::DynSolValue, + primitives::{address, Address, Bytes, U256}, + sol, + sol_types::SolCall, +}; + +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + SafeProxyFactory, + "safe-smart-account/build/artifacts/contracts/proxies/SafeProxyFactory.sol/SafeProxyFactory.json" + // "../../target/.foundry/forge/out/SafeProxyFactory.sol/SafeProxyFactory.json" + // concat!(env!("OUT_DIR"), "/../../../../.foundry/forge/out/SafeProxyFactory.sol/SafeProxyFactory.json") +); + +sol!( + #[allow(clippy::too_many_arguments)] + #[allow(missing_docs)] + #[sol(rpc)] + 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 = + address!("EBe001b3D534B9B6E2500FB78E67a1A137f561CE"); +const SEPOLIA_SAFE_4337_MODULE_ADDRESS: Address = + address!("3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2"); + +// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L438C36-L438C76 +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 = + address!("2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"); + +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + SafeModuleSetup, + "safe-modules/modules/4337/build/artifacts/contracts/SafeModuleSetup.sol/SafeModuleSetup.json" +); + +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + MultiSend, + "safe-smart-account/build/artifacts/contracts/libraries/MultiSend.sol/MultiSend.json" +); + +// 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() +} + +fn init_code_call_data( + owner: Address, +) -> 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, + } + .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, + initializer, + saltNonce: U256::ZERO, + } +} diff --git a/crates/yttrium/src/test_helpers.rs b/crates/yttrium/src/test_helpers.rs new file mode 100644 index 00000000..f136fa47 --- /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.rs b/crates/yttrium/src/transaction/send.rs index fcdd8df8..c8eab62f 100644 --- a/crates/yttrium/src/transaction/send.rs +++ b/crates/yttrium/src/transaction/send.rs @@ -6,6 +6,8 @@ use core::fmt; use std::sync::Arc; use tokio::sync::Mutex; +mod simple_account_test; + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UserOperationEstimated(UserOperationV07); diff --git a/crates/yttrium/src/transaction/send/simple_account_test.rs b/crates/yttrium/src/transaction/send/simple_account_test.rs new file mode 100644 index 00000000..fd0f32fd --- /dev/null +++ b/crates/yttrium/src/transaction/send/simple_account_test.rs @@ -0,0 +1,272 @@ +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, + signer::sign_user_operation_v07_with_ecdsa, + smart_accounts::{ + nonce::get_nonce, + simple_account::{ + create_account::SimpleAccountCreate, factory::FactoryAddress, + SimpleAccountAddress, SimpleAccountExecute, + }, + }, + transaction::Transaction, + user_operation::UserOperationV07, + }; + use alloy::{ + network::EthereumWallet, + primitives::{Address, Bytes, U256}, + providers::ProviderBuilder, + signers::local::LocalSigner, + }; + use std::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 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 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.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 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 transaction_hash = send_transaction(transaction).await?; + + println!("Transaction sent: {}", transaction_hash); + + Ok(()) + } +} diff --git a/platforms/swift/Sources/Yttrium/AccountClient.swift b/platforms/swift/Sources/Yttrium/AccountClient.swift index 371eb6fc..568efee1 100644 --- a/platforms/swift/Sources/Yttrium/AccountClient.swift +++ b/platforms/swift/Sources/Yttrium/AccountClient.swift @@ -30,6 +30,10 @@ public final class AccountClient: AccountClientProtocol { bundler: .init( api_key: "".intoRustString(), base_url: "https://localhost:4337".intoRustString() // TODO + ), + paymaster: .init( + api_key: "".intoRustString(), + base_url: "https://localhost:4337".intoRustString() // TODO ) ) ) diff --git a/platforms/swift/Sources/Yttrium/AccountClient7702.swift b/platforms/swift/Sources/Yttrium/AccountClient7702.swift index d90286a2..f74dfb5d 100644 --- a/platforms/swift/Sources/Yttrium/AccountClient7702.swift +++ b/platforms/swift/Sources/Yttrium/AccountClient7702.swift @@ -30,6 +30,10 @@ public final class AccountClient7702 { bundler: .init( api_key: "".intoRustString(), base_url: "https://localhost:4337".intoRustString() // TODO + ), + paymaster: .init( + api_key: "".intoRustString(), + base_url: "https://localhost:4337".intoRustString() // TODO ) ) ) diff --git a/test/scripts/forked_state/docker-compose.yaml b/test/scripts/forked_state/docker-compose.yaml index 1551a97b..1e52b1ca 100644 --- a/test/scripts/forked_state/docker-compose.yaml +++ b/test/scripts/forked_state/docker-compose.yaml @@ -1,12 +1,14 @@ services: anvil: image: ghcr.io/foundry-rs/foundry:nightly-f6208d8db68f9acbe4ff8cd76958309efb61ea0b + restart: unless-stopped ports: ["8545:8545"] - entrypoint: [ "anvil", "--chain-id", "31337", "--fork-url", "https://gateway.tenderly.co/public/sepolia", "--host", "0.0.0.0", "--block-time", "0.1", "--gas-price", "1", "--accounts", "100", "--silent" ] + entrypoint: [ "anvil", "--fork-url", "https://gateway.tenderly.co/public/sepolia", "--host", "0.0.0.0", "--block-time", "0.1", "--gas-price", "1" ] platform: linux/amd64/v8 mock-paymaster: image: ghcr.io/pimlicolabs/mock-verifying-paymaster:main + restart: unless-stopped ports: ["3000:3000"] environment: - ALTO_RPC=http://alto:4337 @@ -14,7 +16,8 @@ services: alto: image: ghcr.io/pimlicolabs/mock-alto-bundler:main + restart: unless-stopped ports: ["4337:4337"] environment: - ANVIL_RPC=http://anvil:8545 - - SKIP_DEPLOYMENTS=true \ No newline at end of file + - SKIP_DEPLOYMENTS=true