From a7d02b3a5528759770164455dd599ea1b483a2f2 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 11 Sep 2024 13:41:20 -0700 Subject: [PATCH 1/5] feat: test Safe against Sepolia and Sepolia --- .env.template | 3 +- .github/workflows/ci.yml | 8 +- README.md | 33 ++++- crates/yttrium/Cargo.toml | 4 + .../yttrium/src/transaction/send/safe_test.rs | 127 +++++++++++++++--- justfile | 13 +- 6 files changed, 155 insertions(+), 33 deletions(-) diff --git a/.env.template b/.env.template index 1802628c..cf1f95de 100644 --- a/.env.template +++ b/.env.template @@ -3,6 +3,7 @@ BUNDLER_API_KEY="" BUNDLER_BASE_URL="" RPC_API_KEY="" RPC_BASE_URL="" +FAUCET_MNEMONIC="" PIMLICO_API_KEY="" PIMLICO_BUNDLER_URL="" -PIMLICO_RPC_URL="" \ No newline at end of file +PIMLICO_RPC_URL="" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d844fbb..073779c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,9 +29,9 @@ jobs: - 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 build --workspace --all-features --all-targets - - run: cargo test --all-features --lib --bins - # - run: cargo clippy --workspace --all-features --all-targets -- -D warnings + - run: cargo build --workspace --features=full --all-targets + - run: cargo test --features=full --lib --bins + # - run: cargo clippy --workspace --features=full --all-targets -- -D warnings - run: cargo +nightly fmt --all -- --check udeps: @@ -65,7 +65,7 @@ jobs: # - run: rustup target add wasm32-unknown-unknown # - run: git submodule update --init --recursive # - run: make setup-thirdparty - # - run: cargo build --workspace --all-features --lib --bins --target wasm32-unknown-unknown --exclude=ffi + # - run: cargo build --workspace --features=full --lib --bins --target wasm32-unknown-unknown --exclude=ffi build_swift_and_test: name: Swift Package - latest diff --git a/README.md b/README.md index 4cffa4e2..d3d9beec 100644 --- a/README.md +++ b/README.md @@ -87,4 +87,35 @@ After installing the dependencies, clone the repository and run the following co make setup ``` -This will fetch the third party dependencies and build the project, including the Swift bindings. \ No newline at end of file +This will fetch the third party dependencies and build the project, including the Swift bindings. + +### Devloop + +During normal development you can use the `just devloop` command to test your code both during development and before comitting/pushing. This is handy as it runs as many checks as possible and fixes any issues (such as formatting) automatically. + +This command does not require any configuration. + +```bash +just devloop +``` + +TODO: make this setup anvil automatically + +### Specific tests + +Some tests require some configuration (such as funds on Sepolia). For these, supply `FAUCET_MNEMONIC` and add some funds on the account. + +#### Pimlico/Sepolia + +```bash +just test-pimlico-api +``` + +Required environment variables: + +```text +FAUCET_MNEMONIC +PIMLICO_API_KEY +PIMLICO_BUNDLER_URL +PIMLICO_RPC_URL +``` diff --git a/crates/yttrium/Cargo.toml b/crates/yttrium/Cargo.toml index c0fd8305..3db33d4b 100644 --- a/crates/yttrium/Cargo.toml +++ b/crates/yttrium/Cargo.toml @@ -4,6 +4,10 @@ version.workspace = true edition.workspace = true rust-version.workspace = true +[features] +full = [] +test_pimlico_api = [] + [dependencies] # Ethereum alloy = { git = "https://github.com/alloy-rs/alloy", version = "0.3.2", features = [ diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 13d582cb..40193102 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -62,13 +62,15 @@ mod tests { use alloy::{ consensus::{SignableTransaction, TxEip7702}, dyn_abi::{DynSolValue, Eip712Domain}, - network::{Ethereum, TxSignerSync}, + network::{Ethereum, EthereumWallet, TransactionBuilder, TxSignerSync}, primitives::{ aliases::U48, Address, Bytes, FixedBytes, Uint, U128, U256, U64, }, providers::{ - ext::AnvilApi, PendingTransactionConfig, Provider, ReqwestProvider, + ext::AnvilApi, PendingTransactionConfig, Provider, ProviderBuilder, + ReqwestProvider, }, + rpc::types::TransactionRequest, signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, sol, sol_types::{SolCall, SolValue}, @@ -81,9 +83,8 @@ mod tests { owner: LocalSigner, address: Option
, authorization_list: Option>, + config: Config, ) -> 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; @@ -449,11 +450,40 @@ mod tests { Ok(user_operation_hash) } - #[tokio::test] - async fn test_send_transaction() -> eyre::Result<()> { - let rpc_url = Config::local().endpoints.rpc.base_url; - let rpc_url: reqwest::Url = rpc_url.parse()?; - let provider = ReqwestProvider::::new_http(rpc_url); + async fn use_faucet( + provider: ReqwestProvider, + faucet: LocalSigner, + amount: U256, + to: Address, + ) -> eyre::Result<()> { + if amount > U256::from(20) { + panic!("You probably don't need that much"); + } + println!("address: {}", faucet.address()); + + ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::new(faucet)) + .on_provider(provider.clone()) + .send_transaction( + TransactionRequest::default().with_to(to).with_value(amount), + ) + .await? + .watch() + .await?; + let balance = provider.get_balance(to).await?; + assert_eq!(balance, amount); + + Ok(()) + } + + async fn test_send_transaction( + config: Config, + faucet: LocalSigner, + ) -> eyre::Result<()> { + let provider = ReqwestProvider::::new_http( + config.endpoints.rpc.base_url.parse()?, + ); let destination = LocalSigner::random(); let balance = provider.get_balance(destination.address()).await?; @@ -466,22 +496,34 @@ mod tests { ) .await; - provider.anvil_set_balance(sender_address, U256::from(100)).await?; + use_faucet( + provider.clone(), + faucet.clone(), + U256::from(2), + sender_address, + ) + .await?; + let transaction = vec![Execution { target: destination.address(), value: Uint::from(1), callData: Bytes::new(), }]; - let transaction_hash = - send_transaction(transaction, owner.clone(), None, None).await?; + let transaction_hash = send_transaction( + transaction, + owner.clone(), + None, + None, + config.clone(), + ) + .await?; println!("Transaction sent: {}", transaction_hash); let balance = provider.get_balance(destination.address()).await?; assert_eq!(balance, Uint::from(1)); - provider.anvil_set_balance(sender_address, U256::from(100)).await?; let transaction = vec![Execution { target: destination.address(), value: Uint::from(1), @@ -489,7 +531,7 @@ mod tests { }]; let transaction_hash = - send_transaction(transaction, owner, None, None).await?; + send_transaction(transaction, owner, None, None, config).await?; println!("Transaction sent: {}", transaction_hash); @@ -499,12 +541,43 @@ mod tests { Ok(()) } + async fn anvil_faucet(config: Config) -> LocalSigner { + let faucet = LocalSigner::random(); + let provider = ReqwestProvider::::new_http( + config.endpoints.rpc.base_url.parse().unwrap(), + ); + provider.anvil_set_balance(faucet.address(), U256::MAX).await.unwrap(); + faucet + } + + #[tokio::test] + async fn test_send_transaction_local() { + let config = Config::local(); + let faucet = anvil_faucet(config.clone()).await; + test_send_transaction(config, faucet).await.unwrap(); + } + + #[tokio::test] + #[cfg(feature = "test_pimlico_api")] + async fn test_send_transaction_pimlico() { + let config = Config::pimlico(); + let faucet = MnemonicBuilder::::default() + .phrase( + std::env::var("FAUCET_MNEMONIC") + .expect("You've not set the FAUCET_MNEMONIC"), + ) + .build() + .unwrap(); + test_send_transaction(config, faucet).await.unwrap(); + } + #[tokio::test] #[ignore] async fn test_send_transaction_7702() -> eyre::Result<()> { - let rpc_url = Config::local().endpoints.rpc.base_url; - let rpc_url: reqwest::Url = rpc_url.parse()?; - let provider = ReqwestProvider::::new_http(rpc_url); + let config = Config::local(); + let provider = ReqwestProvider::::new_http( + config.endpoints.rpc.base_url.parse()?, + ); let destination = LocalSigner::random(); let balance = provider.get_balance(destination.address()).await?; @@ -558,6 +631,7 @@ mod tests { Some(authority.address()), Some(authorization_list.clone()), // None, + config.clone(), ) .await?; println!("Transaction sent: {}", transaction_hash); @@ -585,6 +659,7 @@ mod tests { // None, // Some(authorization_list.clone()), None, + config, ) .await?; @@ -599,9 +674,10 @@ mod tests { #[tokio::test] #[ignore] async fn test_send_transaction_7702_vanilla_bundler() -> eyre::Result<()> { - let rpc_url = Config::local().endpoints.rpc.base_url; - let rpc_url: reqwest::Url = rpc_url.parse()?; - let provider = ReqwestProvider::::new_http(rpc_url); + let config = Config::local(); + let provider = ReqwestProvider::::new_http( + config.endpoints.rpc.base_url.parse()?, + ); let destination = LocalSigner::random(); let balance = provider.get_balance(destination.address()).await?; @@ -615,8 +691,14 @@ mod tests { .await; let transaction = vec![]; - let transaction_hash = - send_transaction(transaction, owner.clone(), None, None).await?; + let transaction_hash = send_transaction( + transaction, + owner.clone(), + None, + None, + config.clone(), + ) + .await?; println!("Transaction sent: {}", transaction_hash); let authority = LocalSigner::random(); @@ -721,6 +803,7 @@ mod tests { owner, Some(authority.address()), None, + config, ) .await?; diff --git a/justfile b/justfile index ea371fe8..f56e12b9 100644 --- a/justfile +++ b/justfile @@ -7,11 +7,14 @@ setup: devloop: build test fmt udeps test: - cargo test --all-features --lib --bins + cargo test --features=full --lib --bins + +test-pimlico-api: + cargo test --features=test_pimlico_api --lib --bins clippy: - cargo clippy --workspace --all-features --all-targets -- -D warnings - # cargo clippy --workspace --all-features --lib --bins --target wasm32-unknown-unknown --exclude=ffi -- -D warnings + cargo clippy --workspace --features=full --all-targets -- -D warnings + # cargo clippy --workspace --features=full --lib --bins --target wasm32-unknown-unknown --exclude=ffi -- -D warnings fmt: cargo +nightly fmt --all @@ -21,5 +24,5 @@ udeps: # TODO remove `build` in-favor of `clippy` when clippy passes build: - cargo build --workspace --all-features --all-targets - # cargo build --workspace --all-features --lib --bins --target wasm32-unknown-unknown --exclude=ffi + cargo build --workspace --features=full --all-targets + # cargo build --workspace--features=full --lib --bins --target wasm32-unknown-unknown --exclude=ffi From eeec9f3bedf513dabbd75848d2524d2bf548e796 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 11 Sep 2024 13:51:08 -0700 Subject: [PATCH 2/5] chore: comment --- crates/yttrium/src/transaction/send/safe_test.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 40193102..63546b7b 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -457,6 +457,9 @@ mod tests { to: Address, ) -> eyre::Result<()> { if amount > U256::from(20) { + // Basic check (which we can tune) to make sure we don't use + // excessive amounts (e.g. 0.1) of test ETH. It is not infinite, so + // we should use the minimum amount necessary. panic!("You probably don't need that much"); } println!("address: {}", faucet.address()); From b511989275d9d916d40fde5da0c1ca292ca4795a Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 11 Sep 2024 13:52:19 -0700 Subject: [PATCH 3/5] chore: prefer assert --- crates/yttrium/src/transaction/send/safe_test.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 63546b7b..ea52bd6c 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -456,13 +456,10 @@ mod tests { amount: U256, to: Address, ) -> eyre::Result<()> { - if amount > U256::from(20) { - // Basic check (which we can tune) to make sure we don't use - // excessive amounts (e.g. 0.1) of test ETH. It is not infinite, so - // we should use the minimum amount necessary. - panic!("You probably don't need that much"); - } - println!("address: {}", faucet.address()); + // Basic check (which we can tune) to make sure we don't use excessive + // amounts (e.g. 0.1) of test ETH. It is not infinite, so we should use + // the minimum amount necessary. + assert!(amount < U256::from(20), "You probably don't need that much"); ProviderBuilder::new() .with_recommended_fillers() From 5831cbaeae4ad76ef4a5c9988afbab562ab2713f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 12 Sep 2024 07:22:04 -0700 Subject: [PATCH 4/5] chore: add missing deps --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d3d9beec..79709f6b 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ To contribute to this project, ensure you have the following dependencies instal - `rustc` - `swiftc` and Xcode - `foundry` +- `make` +- `just` +- `npm` +- `pnpm` +- `yarn` ### Setup From aa97347d96a153feabbb3a709bf1b62e4325a233 Mon Sep 17 00:00:00 2001 From: Chris Smith <1979423+chris13524@users.noreply.github.com> Date: Thu, 12 Sep 2024 08:23:15 -0700 Subject: [PATCH 5/5] chore: Safe Swift refactor (#21) --- crates/ffi/src/account_client.rs | 1 + crates/ffi/src/lib.rs | 1 + crates/yttrium/src/account_client.rs | 21 +- crates/yttrium/src/transaction/send.rs | 24 +- .../yttrium/src/transaction/send/safe_test.rs | 733 +++++++++--------- .../swift/Sources/Yttrium/AccountClient.swift | 18 +- .../Sources/Yttrium/AccountClient7702.swift | 18 +- .../Yttrium/AccountClientProtocol.swift | 2 +- 8 files changed, 433 insertions(+), 385 deletions(-) diff --git a/crates/ffi/src/account_client.rs b/crates/ffi/src/account_client.rs index 8eee30ce..3cdccdb7 100644 --- a/crates/ffi/src/account_client.rs +++ b/crates/ffi/src/account_client.rs @@ -83,6 +83,7 @@ impl FFIAccountClient { config.chain_id.clone(), config.config.into(), service, + config.safe, ); account_client } diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs index 3124b04c..6c0b35f9 100644 --- a/crates/ffi/src/lib.rs +++ b/crates/ffi/src/lib.rs @@ -47,6 +47,7 @@ mod ffi { pub chain_id: u64, pub config: FFIConfig, pub signer_type: String, + pub safe: bool, } enum FFIStringResult { diff --git a/crates/yttrium/src/account_client.rs b/crates/yttrium/src/account_client.rs index e4484cea..c1e8e140 100644 --- a/crates/yttrium/src/account_client.rs +++ b/crates/yttrium/src/account_client.rs @@ -1,6 +1,7 @@ use crate::config::Config; use crate::private_key_service::PrivateKeyService; use crate::sign_service::SignService; +use crate::transaction::send::safe_test; use crate::transaction::{send::send_transaction, Transaction}; use alloy::primitives::Address; use alloy::signers::local::PrivateKeySigner; @@ -47,6 +48,7 @@ pub struct AccountClient { chain_id: u64, config: Config, signer: Signer, + safe: bool, } impl AccountClient { @@ -61,6 +63,7 @@ impl AccountClient { chain_id, config: config.clone(), signer: Signer::Native(Arc::new(Mutex::new(sign_service))), + safe: false, } } @@ -69,6 +72,7 @@ impl AccountClient { chain_id: u64, config: Config, private_key_service: PrivateKeyService, + safe: bool, ) -> Self { Self { owner, @@ -77,6 +81,7 @@ impl AccountClient { signer: Signer::PrivateKey(Arc::new(Mutex::new( private_key_service, ))), + safe, } } @@ -98,6 +103,7 @@ impl AccountClient { signer: Signer::PrivateKey(Arc::new(Mutex::new( private_key_service, ))), + safe: false, } } @@ -111,6 +117,7 @@ impl AccountClient { self.chain_id.clone(), self.config.clone(), self.signer.clone(), + self.safe, ) .await } @@ -136,6 +143,7 @@ impl AccountClient { self.chain_id.clone(), self.config.clone(), self.signer.clone(), + self.safe, ) .await } @@ -162,6 +170,7 @@ impl AccountClient { chain_id: 0, config: Config::local(), signer: Signer::Native(Arc::new(Mutex::new(SignService::mock()))), + safe: false, } } } @@ -171,6 +180,7 @@ pub async fn get_address_with_signer( chain_id: u64, config: Config, signer: Signer, + safe: bool, ) -> eyre::Result { match signer { Signer::PrivateKey(private_key_service) => { @@ -185,6 +195,7 @@ pub async fn get_address_with_signer( chain_id, config, private_key_signer, + safe, ) .await } @@ -199,11 +210,15 @@ pub async fn get_address_with_private_key_signer( chain_id: u64, config: Config, signer: PrivateKeySigner, + safe: bool, ) -> eyre::Result { use crate::smart_accounts::simple_account::sender_address::get_sender_address_with_signer; - let sender_address = - get_sender_address_with_signer(config, chain_id, signer).await?; + let sender_address = if safe { + safe_test::get_address(signer, config).await? + } else { + get_sender_address_with_signer(config, chain_id, signer).await? + }; Ok(sender_address.to_string()) } @@ -239,6 +254,7 @@ mod tests { chain_id, config, private_key_service, + false, ); let transaction = Transaction::new_from_strings( @@ -278,6 +294,7 @@ mod tests { chain_id, config, private_key_service, + false, ); let sender_address = account_client.get_address().await?; diff --git a/crates/yttrium/src/transaction/send.rs b/crates/yttrium/src/transaction/send.rs index 66e53187..a41e09df 100644 --- a/crates/yttrium/src/transaction/send.rs +++ b/crates/yttrium/src/transaction/send.rs @@ -1,3 +1,4 @@ +use crate::smart_accounts::safe::Execution; use crate::transaction::send::simple_account_test::send_transaction_with_signer; use crate::{ config::Config, transaction::Transaction, user_operation::UserOperationV07, @@ -5,7 +6,7 @@ use crate::{ use alloy::signers::local::PrivateKeySigner; use core::fmt; -mod safe_test; +pub mod safe_test; pub mod send_tests; pub mod simple_account_test; @@ -50,6 +51,7 @@ pub async fn send_transaction( chain_id: u64, config: Config, signer: Signer, + safe: bool, ) -> eyre::Result { match signer { Signer::PrivateKey(private_key_service) => { @@ -65,6 +67,7 @@ pub async fn send_transaction( chain_id, config, private_key_signer, + safe, ) .await } @@ -80,12 +83,27 @@ pub async fn send_transaction_with_private_key_signer( chain_id: u64, config: Config, private_key_signer: PrivateKeySigner, + safe: bool, ) -> eyre::Result { let signer = private_key_signer; - let user_operation_hash = + let user_operation_hash = if safe { + safe_test::send_transaction( + vec![Execution { + target: transaction.to, + value: transaction.value, + callData: transaction.data, + }], + signer, + None, + None, + config, + ) + .await? + } else { send_transaction_with_signer(transaction, config, chain_id, signer) - .await?; + .await? + }; println!("user_operation_hash: {:?}", user_operation_hash); diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index ea52bd6c..184c884c 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -1,5 +1,37 @@ -use crate::user_operation::UserOperationV07; +use crate::{ + bundler::{ + client::BundlerClient, + config::BundlerConfig, + pimlico::{ + client::BundlerClient as PimlicoBundlerClient, + paymaster::client::PaymasterClient, + }, + }, + config::Config, + smart_accounts::{ + nonce::get_nonce, + safe::{ + factory_data, get_account_address, 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}, + }, + user_operation::{Authorization, UserOperationV07}, +}; +use alloy::{ + dyn_abi::{DynSolValue, Eip712Domain}, + network::Ethereum, + primitives::{aliases::U48, Address, Bytes, FixedBytes, Uint, U128, U256}, + providers::{Provider, ReqwestProvider}, + signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, + sol, + sol_types::{SolCall, SolValue}, +}; use core::fmt; +use serde_json::json; +use std::{ops::Not, str::FromStr}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UserOperationEstimated(UserOperationV07); @@ -34,205 +66,168 @@ impl fmt::Display for SentUserOperationHash { } } -#[cfg(test)] -mod tests { - use crate::{ - bundler::{ - client::BundlerClient, - config::BundlerConfig, - pimlico::{ - client::BundlerClient as PimlicoBundlerClient, - paymaster::client::PaymasterClient, - }, - }, - chain::ChainId, - config::Config, - smart_accounts::{ - nonce::get_nonce, - safe::{ - factory_data, get_account_address, 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}, - }, - user_operation::{Authorization, UserOperationV07}, - }; - use alloy::{ - consensus::{SignableTransaction, TxEip7702}, - dyn_abi::{DynSolValue, Eip712Domain}, - network::{Ethereum, EthereumWallet, TransactionBuilder, TxSignerSync}, - primitives::{ - aliases::U48, Address, Bytes, FixedBytes, Uint, U128, U256, U64, - }, - providers::{ - ext::AnvilApi, PendingTransactionConfig, Provider, ProviderBuilder, - ReqwestProvider, - }, - rpc::types::TransactionRequest, - signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, - sol, - sol_types::{SolCall, SolValue}, - }; - use serde_json::json; - use std::{ops::Not, str::FromStr}; - - async fn send_transaction( - execution_calldata: Vec, - owner: LocalSigner, - address: Option
, - authorization_list: Option>, - config: Config, - ) -> eyre::Result { - let bundler_base_url = config.endpoints.bundler.base_url; - let paymaster_base_url = config.endpoints.paymaster.base_url; +pub async fn get_address( + owner: LocalSigner, + config: Config, +) -> eyre::Result
{ + let rpc_url = config.endpoints.rpc.base_url; + let rpc_url: reqwest::Url = rpc_url.parse()?; + let provider = ReqwestProvider::::new_http(rpc_url); - let bundler_client = - BundlerClient::new(BundlerConfig::new(bundler_base_url.clone())); + let owners = Owners { owners: vec![owner.address()], threshold: 1 }; - let pimlico_client: PimlicoBundlerClient = PimlicoBundlerClient::new( - BundlerConfig::new(bundler_base_url.clone()), - ); + Ok(get_account_address(provider.clone(), owners.clone()).await) +} - let chain = crate::chain::Chain::ETHEREUM_SEPOLIA_V07; - let entry_point_config = chain.entry_point_config(); +pub async fn send_transaction( + execution_calldata: Vec, + owner: LocalSigner, + address: Option
, + authorization_list: Option>, + config: Config, +) -> eyre::Result { + let bundler_base_url = config.endpoints.bundler.base_url; + let paymaster_base_url = config.endpoints.paymaster.base_url; - let chain_id = chain.id.eip155_chain_id(); + let bundler_client = + BundlerClient::new(BundlerConfig::new(bundler_base_url.clone())); - let entry_point_address = entry_point_config.address(); + let pimlico_client: PimlicoBundlerClient = + PimlicoBundlerClient::new(BundlerConfig::new(bundler_base_url.clone())); - let rpc_url = config.endpoints.rpc.base_url; + let chain = crate::chain::Chain::ETHEREUM_SEPOLIA_V07; + let entry_point_config = chain.entry_point_config(); - let rpc_url: reqwest::Url = rpc_url.parse()?; - let provider = ReqwestProvider::::new_http(rpc_url); + let chain_id = chain.id.eip155_chain_id(); - let safe_factory_address_primitives: Address = - SAFE_PROXY_FACTORY_ADDRESS; - let safe_factory_address = - FactoryAddress::new(safe_factory_address_primitives); + let entry_point_address = entry_point_config.address(); - let owners = Owners { owners: vec![owner.address()], threshold: 1 }; + let rpc_url = config.endpoints.rpc.base_url; - let factory_data_value = factory_data(owners.clone()).abi_encode(); + let rpc_url: reqwest::Url = rpc_url.parse()?; + let provider = ReqwestProvider::::new_http(rpc_url); - let contract_address = - get_account_address(provider.clone(), owners.clone()).await; - let account_address = if let Some(address) = address { - address - } else { - contract_address - }; + let safe_factory_address_primitives: Address = SAFE_PROXY_FACTORY_ADDRESS; + let safe_factory_address = + FactoryAddress::new(safe_factory_address_primitives); - let call_type = if execution_calldata.len() == 1 { - CallType::Call - } else { - CallType::BatchCall - }; - 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 owners = Owners { owners: vec![owner.address()], threshold: 1 }; + + let factory_data_value = factory_data(owners.clone()).abi_encode(); + + let contract_address = + get_account_address(provider.clone(), owners.clone()).await; + let account_address = + if let Some(address) = address { address } else { contract_address }; + + let call_type = if execution_calldata.len() == 1 { + CallType::Call + } else { + CallType::BatchCall + }; + 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 mode = DynSolValue::Tuple(vec![ - DynSolValue::Uint(Uint::from(call_type.as_byte()), 8), - DynSolValue::Uint(Uint::from(revert_on_error as u8), 8), - DynSolValue::Bytes(vec![0u8; 4]), - DynSolValue::Bytes(selector.to_vec()), - DynSolValue::Bytes(context.to_vec()), - ]) - .abi_encode_packed(); - - let call_data = Safe7579::executeCall { - mode: FixedBytes::from_slice(&mode), - executionCalldata: execution_calldata.abi_encode_packed().into(), + let mode = DynSolValue::Tuple(vec![ + DynSolValue::Uint(Uint::from(call_type.as_byte()), 8), + DynSolValue::Uint(Uint::from(revert_on_error as u8), 8), + DynSolValue::Bytes(vec![0u8; 4]), + DynSolValue::Bytes(selector.to_vec()), + DynSolValue::Bytes(context.to_vec()), + ]) + .abi_encode_packed(); + + let call_data = Safe7579::executeCall { + mode: FixedBytes::from_slice(&mode), + executionCalldata: execution_calldata.abi_encode_packed().into(), + } + .abi_encode() + .into(); + + let deployed = provider.get_code_at(account_address).await?.len() > 0; + println!("Deployed: {}", deployed); + // 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(); - - let deployed = provider.get_code_at(account_address).await?.len() > 0; - println!("Deployed: {}", deployed); - // 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() - }; - - let gas_price = - pimlico_client.estimate_user_operation_gas_price().await?; - - assert!(gas_price.fast.max_fee_per_gas > U256::from(1)); - - let nonce = get_nonce( - &provider, - &SimpleAccountAddress::new(account_address), - &entry_point_address, - ) - .await?; + .into() + }; - let user_op = UserOperationV07 { - sender: account_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, - // authorization_list: None, - signature: Bytes::from_str( - crate::smart_accounts::safe::DUMMY_SIGNATURE_HEX - .strip_prefix("0x") - .unwrap(), - )?, - }; + let gas_price = pimlico_client.estimate_user_operation_gas_price().await?; + + assert!(gas_price.fast.max_fee_per_gas > U256::from(1)); + + let nonce = get_nonce( + &provider, + &SimpleAccountAddress::new(account_address), + &entry_point_address, + ) + .await?; + + let user_op = UserOperationV07 { + sender: account_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, + // authorization_list: None, + signature: Bytes::from_str( + crate::smart_accounts::safe::DUMMY_SIGNATURE_HEX + .strip_prefix("0x") + .unwrap(), + )?, + }; - if let Some(authorization_list) = authorization_list { - let response = reqwest::Client::new() + if let Some(authorization_list) = authorization_list { + let response = reqwest::Client::new() .post(bundler_base_url.clone()) .json(&json!({ "jsonrpc": "2.0", @@ -246,209 +241,213 @@ mod tests { .send() .await .unwrap(); - let success = response.status().is_success(); - println!("response: {:?}", response.text().await); + let success = response.status().is_success(); + println!("response: {:?}", response.text().await); - assert!(success); - } + assert!(success); + } - let paymaster_client = PaymasterClient::new(BundlerConfig::new( - paymaster_base_url.clone(), - )); + 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?; + let sponsor_user_op_result = paymaster_client + .sponsor_user_operation_v07( + &user_op.clone().into(), + &entry_point_address, + None, + ) + .await?; - 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 - }; + let sponsored_user_op = { + let s = sponsor_user_op_result.clone(); + let mut op = user_op.clone(); - let valid_after = U48::from(0); - let valid_until = U48::from(0); - - sol!( - struct SafeOp { - 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; - } - ); + 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 + }; - // TODO handle panic - fn coerce_u256_to_u128(u: U256) -> U128 { - U128::from(u) + let valid_after = U48::from(0); + let valid_until = U48::from(0); + + sol!( + struct SafeOp { + 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; } + ); - let message = SafeOp { - safe: account_address, - callData: sponsored_user_op.call_data.clone(), - nonce: sponsored_user_op.nonce, - initCode: deployed - .not() - .then(|| { - [ + // TODO handle panic + fn coerce_u256_to_u128(u: U256) -> U128 { + U128::from(u) + } + + let message = SafeOp { + safe: account_address, + callData: sponsored_user_op.call_data.clone(), + nonce: sponsored_user_op.nonce, + initCode: deployed + .not() + .then(|| { + [ + sponsored_user_op.clone().factory.unwrap().to_vec().into(), + sponsored_user_op.clone().factory_data.unwrap(), + ] + .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(), + coerce_u256_to_u128( sponsored_user_op - .clone() - .factory - .unwrap() - .to_vec() - .into(), - sponsored_user_op.clone().factory_data.unwrap(), - ] - .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(), - coerce_u256_to_u128( - sponsored_user_op - .paymaster_verification_gas_limit - .unwrap_or(Uint::from(0)), - ) - .to_be_bytes_vec(), - coerce_u256_to_u128( - sponsored_user_op - .paymaster_post_op_gas_limit - .unwrap_or(Uint::from(0)), - ) - .to_be_bytes_vec(), + .paymaster_verification_gas_limit + .unwrap_or(Uint::from(0)), + ) + .to_be_bytes_vec(), + coerce_u256_to_u128( sponsored_user_op - .paymaster_data - .clone() - .unwrap_or(Bytes::new()) - .to_vec(), - ] - .concat() - .into() - }) - .unwrap_or(Bytes::new()), - validAfter: valid_after, - validUntil: valid_until, - entryPoint: entry_point_address.to_address(), - }; + .paymaster_post_op_gas_limit + .unwrap_or(Uint::from(0)), + ) + .to_be_bytes_vec(), + sponsored_user_op + .paymaster_data + .clone() + .unwrap_or(Bytes::new()) + .to_vec(), + ] + .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_4337_MODULE_ADDRESS - }; + let erc7579_launchpad_address = true; + let verifying_contract = if erc7579_launchpad_address && !deployed { + sponsored_user_op.sender + } else { + SAFE_4337_MODULE_ADDRESS + }; - // TODO loop per-owner - let signature = owner.sign_typed_data_sync( - &message, - &Eip712Domain { - chain_id: Some(Uint::from(chain_id)), - verifying_contract: Some(verifying_contract), - ..Default::default() - }, - )?; - // TODO sort by (lowercase) owner address not signature data - 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?; + // TODO loop per-owner + let signature = owner.sign_typed_data_sync( + &message, + &Eip712Domain { + chain_id: Some(Uint::from(chain_id)), + verifying_contract: Some(verifying_contract), + ..Default::default() + }, + )?; + // TODO sort by (lowercase) owner address not signature data + 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); + println!("Received User Operation hash: {:?}", user_operation_hash); - println!("Querying for receipts..."); + println!("Querying for receipts..."); - let receipt = bundler_client - .wait_for_user_operation_receipt(user_operation_hash.clone()) - .await?; + 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 - ); + let tx_hash = receipt.receipt.transaction_hash; + println!( + "SAFE UserOperation included: https://sepolia.etherscan.io/tx/{}", + tx_hash + ); + + // Some extra calls to wait for/get the actual transaction. But these + // aren't required since eth_getUserOperationReceipt already waits + // let tx_hash = FixedBytes::from_slice( + // &hex::decode(tx_hash.strip_prefix("0x").unwrap()).unwrap(), + // ); + // let pending_txn = provider + // .watch_pending_transaction(PendingTransactionConfig::new(tx_hash)) + // .await?; + // pending_txn.await.unwrap(); + // let transaction = provider.get_transaction_by_hash(tx_hash).await?; + // println!("Transaction included: {:?}", transaction); + // let transaction_receipt = + // provider.get_transaction_receipt(tx_hash).await?; + // println!("Transaction receipt: {:?}", transaction_receipt); + + Ok(user_operation_hash) +} - // Some extra calls to wait for/get the actual transaction. But these - // aren't required since eth_getUserOperationReceipt already waits - // let tx_hash = FixedBytes::from_slice( - // &hex::decode(tx_hash.strip_prefix("0x").unwrap()).unwrap(), - // ); - // let pending_txn = provider - // .watch_pending_transaction(PendingTransactionConfig::new(tx_hash)) - // .await?; - // pending_txn.await.unwrap(); - // let transaction = provider.get_transaction_by_hash(tx_hash).await?; - // println!("Transaction included: {:?}", transaction); - // let transaction_receipt = - // provider.get_transaction_receipt(tx_hash).await?; - // println!("Transaction receipt: {:?}", transaction_receipt); - - Ok(user_operation_hash) - } +#[cfg(test)] +mod tests { + use crate::chain::ChainId; + + use super::*; + use alloy::{ + consensus::{SignableTransaction, TxEip7702}, + network::{EthereumWallet, TransactionBuilder, TxSignerSync}, + primitives::U64, + providers::{ext::AnvilApi, PendingTransactionConfig, ProviderBuilder}, + rpc::types::TransactionRequest, + }; async fn use_faucet( provider: ReqwestProvider, diff --git a/platforms/swift/Sources/Yttrium/AccountClient.swift b/platforms/swift/Sources/Yttrium/AccountClient.swift index e49630e2..8eccefcb 100644 --- a/platforms/swift/Sources/Yttrium/AccountClient.swift +++ b/platforms/swift/Sources/Yttrium/AccountClient.swift @@ -15,14 +15,16 @@ public final class AccountClient: AccountClientProtocol { ownerAddress: String, entryPoint: String, chainId: Int, - config: Config + config: Config, + safe: Bool ) { self.init( ownerAddress: ownerAddress, entryPoint: entryPoint, chainId: chainId, config: config, - signerType: .privateKey + signerType: .privateKey, + safe: safe ) } @@ -31,13 +33,15 @@ public final class AccountClient: AccountClientProtocol { entryPoint: String, chainId: Int, config: Config, - signerType: SignerType + signerType: SignerType, + safe: Bool ) { let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( owner_address: ownerAddress.intoRustString(), chain_id: UInt64(chainId), config: config.ffi, - signer_type: signerType.toRustString() + signer_type: signerType.toRustString(), + safe: safe ) self.ownerAddress = ownerAddress self.chainId = chainId @@ -50,13 +54,15 @@ public final class AccountClient: AccountClientProtocol { entryPoint: String, chainId: Int, config: Config, - signer: Signer + signer: Signer, + safe: Bool ) { let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( owner_address: ownerAddress.intoRustString(), chain_id: UInt64(chainId), config: config.ffi, - signer_type: signer.signerType.toRustString() + signer_type: signer.signerType.toRustString(), + safe: safe ) self.ownerAddress = ownerAddress self.chainId = chainId diff --git a/platforms/swift/Sources/Yttrium/AccountClient7702.swift b/platforms/swift/Sources/Yttrium/AccountClient7702.swift index 524b0c29..a17501db 100644 --- a/platforms/swift/Sources/Yttrium/AccountClient7702.swift +++ b/platforms/swift/Sources/Yttrium/AccountClient7702.swift @@ -15,14 +15,16 @@ public final class AccountClient7702 { ownerAddress: String, entryPoint: String, chainId: Int, - config: Config + config: Config, + safe: Bool ) { self.init( ownerAddress: ownerAddress, entryPoint: entryPoint, chainId: chainId, config: config, - signerType: .privateKey + signerType: .privateKey, + safe: safe ) } @@ -31,13 +33,15 @@ public final class AccountClient7702 { entryPoint: String, chainId: Int, config: Config, - signerType: SignerType + signerType: SignerType, + safe: Bool ) { let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( owner_address: ownerAddress.intoRustString(), chain_id: UInt64(chainId), config: config.ffi, - signer_type: signerType.toRustString() + signer_type: signerType.toRustString(), + safe: safe ) self.ownerAddress = ownerAddress self.chainId = chainId @@ -50,13 +54,15 @@ public final class AccountClient7702 { entryPoint: String, chainId: Int, config: Config, - signer: Signer + signer: Signer, + safe: Bool ) { let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( owner_address: ownerAddress.intoRustString(), chain_id: UInt64(chainId), config: config.ffi, - signer_type: signer.signerType.toRustString() + signer_type: signer.signerType.toRustString(), + safe: safe ) self.ownerAddress = ownerAddress self.chainId = chainId diff --git a/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift b/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift index 6723b928..e37945ae 100644 --- a/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift +++ b/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift @@ -23,7 +23,7 @@ public protocol AccountClientProtocol { var chainId: Int { get } - init(ownerAddress: String, entryPoint: String, chainId: Int, config: Config) + init(ownerAddress: String, entryPoint: String, chainId: Int, config: Config, safe: Bool) func register(privateKey: String)