diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a54e90c..1d844fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,21 +51,21 @@ jobs: - run: cargo install cargo-udeps - run: cargo +nightly udeps --workspace - build_wasm: - name: Build on WASM - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 - - uses: pnpm/action-setup@v4 - with: - version: 9 - - run: rustup update stable && rustup default stable - - 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 + # build_wasm: + # name: Build on WASM + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - name: Run sccache-cache + # uses: mozilla-actions/sccache-action@v0.0.4 + # - uses: pnpm/action-setup@v4 + # with: + # version: 9 + # - run: rustup update stable && rustup default stable + # - 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 build_swift_and_test: name: Swift Package - latest @@ -89,5 +89,21 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_15.4.app - name: Build ${{ matrix.config }} run: make CONFIG=${{ matrix.config }} build-swift-apple-platforms - - name: Run ${{ matrix.config }} tests - run: make CONFIG=${{ matrix.config }} test-swift-apple-platforms + # - name: Install Docker + # run: | + # HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask docker + # sudo /Applications/Docker.app/Contents/MacOS/Docker --unattended --install-privileged-components + # open -a /Applications/Docker.app --args --unattended --accept-license + # echo "We are waiting for Docker to be up and running. It can take over 2 minutes..." + # while ! /Applications/Docker.app/Contents/Resources/bin/docker info &>/dev/null; do sleep 1; done + # - name: Start test infrastructure + # run: docker compose up --debug + # working-directory: test/scripts/forked_state + # - name: Wait for local RPC to be ready + # run: while ! curl localhost:8545/health; do sleep 1; done + # - name: Wait for local bundler to be ready + # run: while ! curl localhost:4337/health; do sleep 1; done + # - name: Wait for local paymaster to be ready + # run: while ! curl localhost:3000/ping; do sleep 1; done + # - name: Run ${{ matrix.config }} tests + # run: make CONFIG=${{ matrix.config }} test-swift-apple-platforms diff --git a/Package.swift b/Package.swift index 4f43bcc..7de7322 100644 --- a/Package.swift +++ b/Package.swift @@ -15,19 +15,24 @@ let package = Package( targets: ["Yttrium"]), ], dependencies: [ - .package(path: "crates/ffi/YttriumCore") + .package(path: "crates/ffi/YttriumCore"), + .package(url: "https://github.com/thebarndog/swift-dotenv.git", from: "2.0.0") ], targets: [ .target( name: "Yttrium", dependencies: [ - "YttriumCore" + "YttriumCore", + .product(name: "SwiftDotenv", package: "swift-dotenv") ], path: "platforms/swift/Sources/Yttrium") , .testTarget( name: "YttriumTests", - dependencies: ["Yttrium"], + dependencies: [ + "Yttrium" , + .product(name: "SwiftDotenv", package: "swift-dotenv") + ], path: "platforms/swift/Tests/YttriumTests"), ] ) diff --git a/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/SignServiceFFI.swift b/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/NativeSignerFFI.swift similarity index 66% rename from crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/SignServiceFFI.swift rename to crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/NativeSignerFFI.swift index c43485d..c99bb22 100644 --- a/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/SignServiceFFI.swift +++ b/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/NativeSignerFFI.swift @@ -26,20 +26,16 @@ public final class Signers { } } -public enum SignerError: Error { - case unknown -} - public typealias OnSign = (String) -> Result -public final class Signer { +public final class NativeSigner: Identifiable { - public let signerId: SignerId + public let id: SignerId private let onSign: OnSign - public init(signerId: SignerId, onSign: @escaping OnSign) { - self.signerId = signerId + public init(id: SignerId, onSign: @escaping OnSign) { + self.id = id self.onSign = onSign } @@ -51,43 +47,49 @@ public final class Signer { public struct SignerId: Hashable, CustomStringConvertible, RawRepresentable { public var rawValue: String { - "\(account)-\(chainId)" + "\(signerType)-\(account)-\(chainId)" } public var description: String { rawValue } + public let signerType: SignerType public let account: String public let chainId: Int - public init(account: String, chainId: Int) { + public init(signerType: SignerType, account: String, chainId: Int) { + self.signerType = signerType self.account = account self.chainId = chainId } public init?(rawValue: String) { let idComponents = rawValue.split(separator: "-") - guard idComponents.count == 2 else { + guard idComponents.count == 3 else { return nil } - let account = String(idComponents[0]) - guard let chainId = Int(idComponents[1]) else { + guard let signerType = SignerType(rawValue: String(idComponents[0])) else { return nil } + let account = String(idComponents[1]) + guard let chainId = Int(idComponents[2]) else { + return nil + } + self.signerType = signerType self.account = account self.chainId = chainId } } -public final class SignerServiceFFI { +public final class NativeSignerFFI { - public let signer: Signer + public let signer: NativeSigner public init(signer_id: RustString) { let idString = signer_id.toString() let signerId = SignerId(rawValue: idString)! - self.signer = Signers.shared.signer(id: signerId)! + self.signer = Signers.shared.signer(id: signerId)!.nativeSigner! } public func sign(message: RustString) -> FFIStringResult { @@ -96,17 +98,3 @@ public final class SignerServiceFFI { .ffi } } - -extension String: Error {} - -extension Result where Success == String, Failure == String { - - public var ffi: FFIStringResult { - switch self { - case .success(let value): - return .Ok(value.intoRustString()) - case .failure(let error): - return .Err(error.localizedDescription.intoRustString()) - } - } -} diff --git a/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/PrivateKeySignerFFI.swift b/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/PrivateKeySignerFFI.swift new file mode 100644 index 0000000..feb9e0f --- /dev/null +++ b/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/PrivateKeySignerFFI.swift @@ -0,0 +1,31 @@ +import Foundation + +public final class PrivateKeySignerFFI { + + public let signerId: SignerId + + private let pK: String + + public init(signer_id: RustString) { + let idString = signer_id.toString() + let signerId = SignerId(rawValue: idString)! + self.signerId = signerId + self.pK = Signers.shared.signer(id: signerId)!.privateKeySigner!.privateKey + } + + public func private_key() -> FFIStringResult { + .Ok(pK.intoRustString()) + } +} + +public struct PrivateKeySigner: Identifiable { + + public let id: SignerId + + public let privateKey: String + + public init(id: SignerId, privateKey: String) { + self.id = id + self.privateKey = privateKey + } +} diff --git a/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/Signer.swift b/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/Signer.swift new file mode 100644 index 0000000..c40797b --- /dev/null +++ b/crates/ffi/YttriumCore/Sources/YttriumCore/SwiftFFI/Signer.swift @@ -0,0 +1,69 @@ +import Foundation + +public enum Signer { + case native(NativeSigner) + case privateKey(PrivateKeySigner) + + public var signerType: SignerType { + switch self { + case .native: + return .native + case .privateKey: + return .privateKey + } + } + + public var signerId: SignerId { + switch self { + case .native(let native): + return native.id + case .privateKey(let privateKey): + return privateKey.id + } + } + + public var privateKeySigner: PrivateKeySigner? { + switch self { + case .native: + return nil + case .privateKey(let privateKey): + return privateKey + } + } + + public var nativeSigner: NativeSigner? { + switch self { + case .native(let native): + return native + case .privateKey: + return nil + } + } +} + +public enum SignerType: String, Codable { + case native = "Native" + case privateKey = "PrivateKey" + + public func toRustString() -> RustString { + rawValue.intoRustString() + } +} + +public enum SignerError: Error { + case unknown +} + +extension String: Error {} + +extension Result where Success == String, Failure == String { + + public var ffi: FFIStringResult { + switch self { + case .success(let value): + return .Ok(value.intoRustString()) + case .failure(let error): + return .Err(error.localizedDescription.intoRustString()) + } + } +} diff --git a/crates/ffi/src/account_client.rs b/crates/ffi/src/account_client.rs index 8b40998..8eee30c 100644 --- a/crates/ffi/src/account_client.rs +++ b/crates/ffi/src/account_client.rs @@ -1,17 +1,17 @@ use super::ffi; -use ffi::FFIAccountClientConfig; -use ffi::FFIError; -use sign_service::SignService; -use yttrium::account_client::AccountClient; -use yttrium::error::YttriumError; -use yttrium::sign_service; -use yttrium::sign_service::address_from_string; -use yttrium::transaction::Transaction; +use super::ffi::{FFIAccountClientConfig, FFIError}; +use yttrium::{ + account_client::{AccountClient, SignerType}, + error::YttriumError, + private_key_service::PrivateKeyService, + sign_service::{address_from_string, SignService}, + transaction::Transaction, +}; pub struct FFIAccountClient { pub owner_address: String, - pub chain_id: i64, - account_client: yttrium::account_client::AccountClient, + pub chain_id: u64, + account_client: AccountClient, } impl FFIAccountClient { @@ -28,30 +28,65 @@ impl FFIAccountClient { let owner_address = config.owner_address.clone(); let chain_id = config.chain_id.clone(); - let signer_id = format!("{}-{}", owner_address, chain_id); + let signer_type = config.signer_type.clone(); + let signer_id = + format!("{}-{}-{}", signer_type, owner_address, chain_id); - let sign_fn = Box::new(move |message: String| { - let signer_service = ffi::SignerServiceFFI::new(signer_id.clone()); - let sign = signer_service.sign(message); - let result = match sign { - ffi::FFIStringResult::Ok(signed_message) => Ok(signed_message), - ffi::FFIStringResult::Err(error) => { - Err(YttriumError { message: error }) - } - }; - result - }); + let signer = SignerType::from(signer_type).unwrap(); - let owner = address_from_string(&owner_address).unwrap(); - - let signer = SignService::new(sign_fn, owner); - - let account_client = AccountClient::new( - config.owner_address.clone(), - config.chain_id.clone(), - config.config.into(), - signer, - ); + let account_client = match signer { + SignerType::Native => { + let sign_fn = Box::new(move |message: String| { + let signer_service = + ffi::NativeSignerFFI::new(signer_id.clone()); + let sign = signer_service.sign(message); + let result = match sign { + ffi::FFIStringResult::Ok(signed_message) => { + Ok(signed_message) + } + ffi::FFIStringResult::Err(error) => { + Err(YttriumError { message: error }) + } + }; + result + }); + let owner = address_from_string(&owner_address).unwrap(); + let signer = SignService::new(sign_fn, owner); + let account_client = AccountClient::new_with_sign_service( + config.owner_address.clone(), + config.chain_id.clone(), + config.config.into(), + signer, + ); + account_client + } + SignerType::PrivateKey => { + let private_key_fn = Box::new(move || { + let private_key_service = + ffi::PrivateKeySignerFFI::new(signer_id.clone()); + let private_key = private_key_service.private_key(); + let result = match private_key { + ffi::FFIStringResult::Ok(private_key) => { + Ok(private_key) + } + ffi::FFIStringResult::Err(error) => { + Err(YttriumError { message: error }) + } + }; + result + }); + let owner = address_from_string(&owner_address).unwrap(); + let service = PrivateKeyService::new(private_key_fn, owner); + let account_client = + AccountClient::new_with_private_key_service( + config.owner_address.clone(), + config.chain_id.clone(), + config.config.into(), + service, + ); + account_client + } + }; Self { owner_address: config.owner_address.clone(), @@ -60,17 +95,15 @@ impl FFIAccountClient { } } - pub fn chain_id(&self) -> i64 { + pub fn chain_id(&self) -> u64 { self.chain_id } pub async fn get_address(&self) -> Result { - // self.account_client - // .get_address() - // .await - // .map_err(|e| FFIError::Unknown(e.to_string())) - // TODO: Implement get_address - Ok("EXPECTED_ADDRESS".to_string()) + self.account_client + .get_address() + .await + .map_err(|e| FFIError::Unknown(e.to_string())) } pub async fn send_transaction( @@ -83,14 +116,25 @@ impl FFIAccountClient { .await .map_err(|e| FFIError::Unknown(e.to_string())) } + + pub fn sign_message_with_mnemonic( + &self, + message: String, + mnemonic: String, + ) -> Result { + self.account_client + .sign_message_with_mnemonic(message, mnemonic) + .map_err(|e| FFIError::Unknown(e.to_string())) + } } impl From for Transaction { fn from(transaction: ffi::FFITransaction) -> Self { - Self { - to: transaction._to, - value: transaction._value, - data: transaction._data, - } + Transaction::new_from_strings( + transaction._to, + transaction._value, + transaction._data, + ) + .unwrap() } } diff --git a/crates/ffi/src/account_client_eip7702.rs b/crates/ffi/src/account_client_eip7702.rs index 4b71214..97b93b9 100644 --- a/crates/ffi/src/account_client_eip7702.rs +++ b/crates/ffi/src/account_client_eip7702.rs @@ -10,7 +10,7 @@ use yttrium::transaction::Transaction; pub struct FFI7702AccountClient { pub owner_address: String, - pub chain_id: i64, + pub chain_id: u64, account_client: yttrium::eip7702::account_client::AccountClient, } @@ -31,7 +31,7 @@ impl FFI7702AccountClient { let signer_id = format!("{}-{}", owner_address, chain_id); let sign_fn = Box::new(move |message: String| { - let signer_service = ffi::SignerServiceFFI::new(signer_id.clone()); + let signer_service = ffi::NativeSignerFFI::new(signer_id.clone()); let sign = signer_service.sign(message); let result = match sign { ffi::FFIStringResult::Ok(signed_message) => Ok(signed_message), diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs index e57e791..3124b04 100644 --- a/crates/ffi/src/lib.rs +++ b/crates/ffi/src/lib.rs @@ -44,8 +44,9 @@ mod ffi { #[swift_bridge(swift_repr = "struct")] pub struct FFIAccountClientConfig { pub owner_address: String, - pub chain_id: i64, + pub chain_id: u64, pub config: FFIConfig, + pub signer_type: String, } enum FFIStringResult { @@ -63,7 +64,7 @@ mod ffi { #[swift_bridge(init)] fn new(config: FFIAccountClientConfig) -> FFIAccountClient; - pub fn chain_id(&self) -> i64; + pub fn chain_id(&self) -> u64; pub async fn get_address(&self) -> Result; @@ -71,6 +72,12 @@ mod ffi { &self, _transaction: FFITransaction, ) -> Result; + + pub fn sign_message_with_mnemonic( + &self, + message: String, + mnemonic: String, + ) -> Result; } extern "Rust" { @@ -86,11 +93,20 @@ mod ffi { } extern "Swift" { - type SignerServiceFFI; + type NativeSignerFFI; #[swift_bridge(init)] - fn new(signer_id: String) -> SignerServiceFFI; + fn new(signer_id: String) -> NativeSignerFFI; fn sign(&self, message: String) -> FFIStringResult; } + + extern "Swift" { + type PrivateKeySignerFFI; + + #[swift_bridge(init)] + fn new(signer_id: String) -> PrivateKeySignerFFI; + + fn private_key(&self) -> FFIStringResult; + } } diff --git a/crates/yttrium/src/account_client.rs b/crates/yttrium/src/account_client.rs index 929f233..e4484ce 100644 --- a/crates/yttrium/src/account_client.rs +++ b/crates/yttrium/src/account_client.rs @@ -1,21 +1,58 @@ use crate::config::Config; +use crate::private_key_service::PrivateKeyService; use crate::sign_service::SignService; use crate::transaction::{send::send_transaction, Transaction}; +use alloy::primitives::Address; +use alloy::signers::local::PrivateKeySigner; use std::sync::Arc; use tokio::sync::Mutex; +#[derive(Clone)] +pub enum SignerType { + PrivateKey, + Native, +} + +impl SignerType { + pub fn from(string: String) -> eyre::Result { + match string.as_str() { + "PrivateKey" => Ok(SignerType::PrivateKey), + "Native" => Ok(SignerType::Native), + _ => Err(eyre::Report::msg("Invalid signer type")), + } + } +} + +#[derive(Clone)] +pub enum Signer { + PrivateKey(Arc>), + Native(Arc>), +} + +impl Signer { + pub fn new_with_sign_service(sign_service: SignService) -> Self { + Self::Native(Arc::new(Mutex::new(sign_service))) + } + + pub fn new_with_private_key_service( + private_key_service: PrivateKeyService, + ) -> Self { + Self::PrivateKey(Arc::new(Mutex::new(private_key_service))) + } +} + #[allow(dead_code)] pub struct AccountClient { owner: String, - chain_id: i64, + chain_id: u64, config: Config, - sign_service: Arc>, + signer: Signer, } impl AccountClient { - pub fn new( + pub fn new_with_sign_service( owner: String, - chain_id: i64, + chain_id: u64, config: Config, sign_service: SignService, ) -> Self { @@ -23,16 +60,59 @@ impl AccountClient { owner, chain_id, config: config.clone(), - sign_service: Arc::new(Mutex::new(sign_service)), + signer: Signer::Native(Arc::new(Mutex::new(sign_service))), + } + } + + pub fn new_with_private_key_service( + owner: String, + chain_id: u64, + config: Config, + private_key_service: PrivateKeyService, + ) -> Self { + Self { + owner, + chain_id, + config: config.clone(), + signer: Signer::PrivateKey(Arc::new(Mutex::new( + private_key_service, + ))), + } + } + + pub fn new_with_private_key( + owner: String, + chain_id: u64, + config: Config, + private_key: String, + ) -> Self { + let owner_address: Address = owner.parse().unwrap(); + let private_key_service = PrivateKeyService::new( + Box::new(move || Ok(private_key.clone())), + owner_address, + ); + Self { + owner, + chain_id, + config: config.clone(), + signer: Signer::PrivateKey(Arc::new(Mutex::new( + private_key_service, + ))), } } - pub fn chain_id(&self) -> i64 { + pub fn chain_id(&self) -> u64 { self.chain_id } pub async fn get_address(&self) -> eyre::Result { - todo!("Implement get_address") + get_address_with_signer( + self.owner.clone(), + self.chain_id.clone(), + self.config.clone(), + self.signer.clone(), + ) + .await } pub async fn sign_message(&self, message: String) -> eyre::Result { @@ -50,7 +130,28 @@ impl AccountClient { &self, transaction: Transaction, ) -> eyre::Result { - send_transaction(self.sign_service.clone(), transaction).await + send_transaction( + transaction, + self.owner.clone(), + self.chain_id.clone(), + self.config.clone(), + self.signer.clone(), + ) + .await + } + + pub fn sign_message_with_mnemonic( + &self, + message: String, + mnemonic: String, + ) -> eyre::Result { + let sign_service = crate::sign_service::SignService::new_with_mnemonic( + mnemonic.clone(), + ); + + let signature = sign_service.sign(message)?; + + Ok(signature) } } @@ -60,7 +161,136 @@ impl AccountClient { owner: "".to_string(), chain_id: 0, config: Config::local(), - sign_service: Arc::new(Mutex::new(SignService::mock())), + signer: Signer::Native(Arc::new(Mutex::new(SignService::mock()))), } } } + +pub async fn get_address_with_signer( + owner: String, + chain_id: u64, + config: Config, + signer: Signer, +) -> eyre::Result { + match signer { + Signer::PrivateKey(private_key_service) => { + let private_key_service = private_key_service.clone(); + let private_key_service = private_key_service.lock().await; + let private_key_signer_key = + private_key_service.private_key().unwrap(); + let private_key_signer: PrivateKeySigner = + private_key_signer_key.parse().unwrap(); + get_address_with_private_key_signer( + owner, + chain_id, + config, + private_key_signer, + ) + .await + } + Signer::Native(sign_service) => { + todo!("Implement native signer support") + } + } +} + +pub async fn get_address_with_private_key_signer( + owner: String, + chain_id: u64, + config: Config, + signer: PrivateKeySigner, +) -> 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?; + + Ok(sender_address.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::private_key_service::PrivateKeyService; + + // mnemonic:`"test test test test test test test test test test test junk"` + // derived at `m/44'/60'/0'/0/0` + const PRIVATE_KEY_HEX: &str = + "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + + #[tokio::test] + async fn test_send_transaction_local() -> eyre::Result<()> { + let config = Config::local(); + + let owner_address = + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(); + let chain_id = 11155111; + + let private_key_hex = PRIVATE_KEY_HEX.to_string(); + + let private_key_service = PrivateKeyService::new( + Box::new(move || Ok(private_key_hex.clone())), + owner_address.parse().unwrap(), + ); + + let account_client = AccountClient::new_with_private_key_service( + owner_address, + chain_id, + config, + private_key_service, + ); + + let transaction = Transaction::new_from_strings( + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string(), + "0".to_string(), + "0x68656c6c6f".to_string(), + )?; + + let user_operation_hash = + account_client.send_transaction(transaction).await?; + + println!("user_operation_hash: {:?}", user_operation_hash); + + Ok(()) + } + + #[tokio::test] + async fn test_get_address_local() -> eyre::Result<()> { + let expected_address = + "0x75BD33d92EEAC5Fe41446fcF5953050d691E7fc9".to_string(); + + let config = Config::local(); + + let owner_address = + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(); + let chain_id = 11155111; + + let private_key_hex = PRIVATE_KEY_HEX.to_string(); + + let private_key_service = PrivateKeyService::new( + Box::new(move || Ok(private_key_hex.clone())), + owner_address.parse().unwrap(), + ); + + let account_client = AccountClient::new_with_private_key_service( + owner_address, + chain_id, + config, + private_key_service, + ); + + let sender_address = account_client.get_address().await?; + + println!("sender_address: {:?}", sender_address); + + eyre::ensure!( + sender_address == expected_address, + "Sender address {} does not match expected address {}", + sender_address, + expected_address + ); + + Ok(()) + } +} diff --git a/crates/yttrium/src/bundler/client.rs b/crates/yttrium/src/bundler/client.rs index 5e80ede..0e8fc36 100644 --- a/crates/yttrium/src/bundler/client.rs +++ b/crates/yttrium/src/bundler/client.rs @@ -97,12 +97,12 @@ impl BundlerClient { .json(&req_body) .send() .await?; - println!("post: {:?}", post); + println!("eth_estimateUserOperationGas post: {:?}", post); let res = post.text().await?; - println!("res: {:?}", res); + println!("eth_estimateUserOperationGas res: {:?}", res); let v = serde_json::from_str::>(&res)?; - println!("json: {:?}", v); + println!("eth_estimateUserOperationGas json: {:?}", v); let response: Response = v.into(); diff --git a/crates/yttrium/src/bundler/pimlico/client.rs b/crates/yttrium/src/bundler/pimlico/client.rs index c341554..159d513 100644 --- a/crates/yttrium/src/bundler/pimlico/client.rs +++ b/crates/yttrium/src/bundler/pimlico/client.rs @@ -38,12 +38,12 @@ impl BundlerClient { .json(&req_body) .send() .await?; - println!("post: {:?}", post); + println!("pimlico_getUserOperationGasPrice post: {:?}", post); let res = post.text().await?; - println!("res: {:?}", res); + println!("pimlico_getUserOperationGasPrice res: {:?}", res); let v = serde_json::from_str::>(&res)?; - println!("json: {:?}", v); + println!("pimlico_getUserOperationGasPrice json: {:?}", v); let response: Response = v.into(); diff --git a/crates/yttrium/src/bundler/pimlico/paymaster/client.rs b/crates/yttrium/src/bundler/pimlico/paymaster/client.rs index 64110d2..41d3dfe 100644 --- a/crates/yttrium/src/bundler/pimlico/paymaster/client.rs +++ b/crates/yttrium/src/bundler/pimlico/paymaster/client.rs @@ -54,14 +54,14 @@ impl PaymasterClient { .json(&req_body) .send() .await?; - println!("post: {:?}", post); + println!("pm_sponsorUserOperation post: {:?}", post); let res = post.text().await?; - println!("res: {:?}", res); + println!("pm_sponsorUserOperation res: {:?}", res); let v = serde_json::from_str::>( &res, )?; - println!("json: {:?}", v); + println!("pm_sponsorUserOperation json: {:?}", v); let response: Response = v.into(); diff --git a/crates/yttrium/src/chain.rs b/crates/yttrium/src/chain.rs index 59f5cda..08462e3 100644 --- a/crates/yttrium/src/chain.rs +++ b/crates/yttrium/src/chain.rs @@ -1,22 +1,21 @@ use crate::entry_point::{EntryPointConfig, EntryPointVersion}; use std::fmt; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct ChainId(&'static str); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ChainId(u64); impl ChainId { - pub const ETHEREUM_MAINNET: Self = Self::new_const("eip155:1"); + pub const ETHEREUM_MAINNET: Self = ChainId::new_eip155(1); - pub const ETHEREUM_SEPOLIA: Self = Self::new_const("eip155:11155111"); + pub const ETHEREUM_SEPOLIA: Self = Self::new_eip155(11155111); - pub const LOCAL_FOUNDRY_ETHEREUM_SEPOLIA: Self = - Self::new_const("eip155:31337"); + pub const LOCAL_FOUNDRY_ETHEREUM_SEPOLIA: Self = Self::new_eip155(31337); - const fn new_const(caip2_identifier: &'static str) -> Self { - Self(caip2_identifier) + pub const fn new_eip155(id: u64) -> Self { + Self(id) } - pub fn new(caip2_identifier: &'static str) -> eyre::Result { + pub fn new_caip2(caip2_identifier: &str) -> eyre::Result { let components = caip2_identifier.split(':').collect::>(); let prefix = components .get(0) @@ -28,39 +27,37 @@ impl ChainId { .ok_or_else(|| eyre::eyre!("Invalid CAIP2 chain identifier"))?; match prefix { "eip155" => { - let _: u64 = chain_id.parse()?; - Ok(Self(&caip2_identifier)) + let id: u64 = chain_id.parse()?; + Ok(Self(id)) } _ => Err(eyre::eyre!("Invalid EIP155 chain ID")), } } pub fn caip2_identifier(&self) -> String { - self.0.to_string() + format!("eip155:{}", self.0) } - pub fn eip155_chain_id(&self) -> eyre::Result { - let components = self.0.split(':').collect::>(); - let prefix = components - .get(0) - .map(ToOwned::to_owned) - .ok_or_else(|| eyre::eyre!("Invalid CAIP2 chain identifier"))?; - if prefix != "eip155" { - return Err(eyre::eyre!("Invalid EIP155 chain ID")); - } - let chain_id_string = components - .get(1) - .map(ToOwned::to_owned) - .ok_or_else(|| eyre::eyre!("Invalid CAIP2 chain identifier")) - .unwrap(); - let chain_id = chain_id_string.parse()?; - Ok(chain_id) + pub fn eip155_chain_id(&self) -> u64 { + self.0 + } +} + +impl From for ChainId { + fn from(id: u64) -> Self { + Self::new_eip155(id) } } -impl Into for ChainId { - fn into(self) -> String { - self.0.to_string() +impl From for u64 { + fn from(id: ChainId) -> Self { + id.0 + } +} + +impl From for String { + fn from(chain_id: ChainId) -> Self { + chain_id.caip2_identifier() } } @@ -70,7 +67,7 @@ impl fmt::Display for ChainId { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Chain { pub id: ChainId, pub entry_point_version: EntryPointVersion, @@ -78,6 +75,14 @@ pub struct Chain { } impl Chain { + pub fn new( + id: ChainId, + entry_point_version: EntryPointVersion, + name: &'static str, + ) -> Self { + Self { id, entry_point_version, name } + } + pub const ETHEREUM_MAINNET_V07: Self = Self { id: ChainId::ETHEREUM_MAINNET, entry_point_version: EntryPointVersion::V07, @@ -118,7 +123,7 @@ impl Chain { impl Chain { pub fn entry_point_config(&self) -> EntryPointConfig { EntryPointConfig { - chain_id: self.id, + chain_id: self.id.clone(), version: self.entry_point_version, } } @@ -128,6 +133,16 @@ impl Chain { } } +impl From for Chain { + fn from(chain_id: ChainId) -> Self { + Self { + id: chain_id, + entry_point_version: EntryPointVersion::V07, + name: "", + } + } +} + impl fmt::Display for Chain { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} ({})", self.name, self.id) diff --git a/crates/yttrium/src/eip7702/account_client.rs b/crates/yttrium/src/eip7702/account_client.rs index 497002a..80c917e 100644 --- a/crates/yttrium/src/eip7702/account_client.rs +++ b/crates/yttrium/src/eip7702/account_client.rs @@ -7,7 +7,7 @@ use tokio::sync::Mutex; #[allow(dead_code)] pub struct AccountClient { owner: String, - chain_id: i64, + chain_id: u64, config: Config, sign_service: Arc>, } @@ -15,7 +15,7 @@ pub struct AccountClient { impl AccountClient { pub fn new( owner: String, - chain_id: i64, + chain_id: u64, config: Config, sign_service: SignService, ) -> Self { diff --git a/crates/yttrium/src/entry_point/get_sender_address.rs b/crates/yttrium/src/entry_point/get_sender_address.rs index 14d38aa..d8bc81d 100644 --- a/crates/yttrium/src/entry_point/get_sender_address.rs +++ b/crates/yttrium/src/entry_point/get_sender_address.rs @@ -86,34 +86,37 @@ where let message = error_resp.message.clone(); println!("error_resp_message: {:?}", message); - let error_resp_data = error_resp.data.clone().unwrap(); + if let Some(error_resp_data) = error_resp.data.clone() { + println!("error_resp_data: {:?}", error_resp_data.clone()); - println!("error_resp_data: {:?}", error_resp_data.clone()); + let hex_value = + error_resp_data.get().split("\"").nth(1).unwrap(); - let hex_value = - error_resp_data.get().split("\"").nth(1).unwrap(); + let hex = hex_value.to_string(); - let hex = hex_value.to_string(); + let hex = hex.strip_prefix("0x").unwrap(); - let hex = hex.strip_prefix("0x").unwrap(); + let error_resp_data_bytes_bytes = + Bytes::from_str(hex).unwrap(); - let error_resp_data_bytes_bytes = Bytes::from_str(hex).unwrap(); + println!( + "error_resp_data_bytes_bytes: {:?}", + error_resp_data_bytes_bytes.clone() + ); - println!( - "error_resp_data_bytes_bytes: {:?}", - error_resp_data_bytes_bytes.clone() - ); + let decoded_data = SenderAddressResult::abi_decode( + &error_resp_data_bytes_bytes, + true, + )?; - let decoded_data = SenderAddressResult::abi_decode( - &error_resp_data_bytes_bytes, - true, - )?; + let addr = decoded_data.sender; - let addr = decoded_data.sender; + println!("addr: {:?}", addr.clone()); - println!("addr: {:?}", addr.clone()); - - return Ok(addr); + return Ok(addr); + } else { + return Err(eyre::eyre!("No data in error response")); + }; } _ => { println!("error: {:?}", error); diff --git a/crates/yttrium/src/lib.rs b/crates/yttrium/src/lib.rs index 5f09591..32ea0ed 100644 --- a/crates/yttrium/src/lib.rs +++ b/crates/yttrium/src/lib.rs @@ -7,6 +7,7 @@ pub mod eip7702; pub mod entry_point; pub mod error; pub mod jsonrpc; +pub mod private_key_service; pub mod sign_service; pub mod signer; pub mod smart_accounts; diff --git a/crates/yttrium/src/private_key_service.rs b/crates/yttrium/src/private_key_service.rs new file mode 100644 index 0000000..db3ec85 --- /dev/null +++ b/crates/yttrium/src/private_key_service.rs @@ -0,0 +1,35 @@ +use crate::error::YttriumError; +use alloy::primitives::Address; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub type PrivateKeyFN = + dyn Fn() -> Result + Send + 'static; + +pub type BoxPrivateKeyFN = Box; + +pub struct PrivateKeyService { + private_key_fn: Arc>, + owner: Address, +} + +impl PrivateKeyService { + pub fn new(private_key_fn: BoxPrivateKeyFN, owner: Address) -> Self { + PrivateKeyService { + private_key_fn: Arc::new(Mutex::new(private_key_fn)), + owner, + } + } + + pub fn owner(&self) -> Address { + self.owner + } + + pub fn private_key(&self) -> Result { + let private_key_fn = self.private_key_fn.clone(); + let private_key_fn = private_key_fn + .try_lock() + .map_err(|e| YttriumError { message: e.to_string() })?; + (private_key_fn)() + } +} diff --git a/crates/yttrium/src/sign_service.rs b/crates/yttrium/src/sign_service.rs index 937a07b..f92b9e0 100644 --- a/crates/yttrium/src/sign_service.rs +++ b/crates/yttrium/src/sign_service.rs @@ -1,7 +1,10 @@ use crate::error::YttriumError; use alloy::{ primitives::Address, - signers::local::{coins_bip39::English, MnemonicBuilder}, + signers::{ + local::{coins_bip39::English, MnemonicBuilder, PrivateKeySigner}, + SignerSync, + }, }; use std::sync::Arc; use tokio::sync::Mutex; @@ -42,7 +45,7 @@ impl SignService { } } - pub async fn mock_with_mnemonic(mnemonic: String) -> Self { + pub fn new_with_mnemonic(mnemonic: String) -> Self { let phrase = mnemonic.clone(); let index: u32 = 0; @@ -53,18 +56,19 @@ impl SignService { .build() .unwrap(); - let alloy_signer = - alloy::signers::local::PrivateKeySigner::from(wallet.clone()); - - let signer = crate::signer::Signer::from(alloy_signer.clone()); + let alloy_signer = PrivateKeySigner::from(wallet.clone()); let owner = alloy_signer.address(); SignService { sign_fn: Arc::new(Mutex::new(Box::new(move |msg: String| { - let signature = signer.sign_message_string_sync(msg).unwrap(); - - Ok(signature) + let message_bytes = hex::decode(msg).unwrap(); + let signature = + alloy_signer.sign_message_sync(&message_bytes)?; + let sig_vec: Vec = signature.into(); + let sig_vec_hex = hex::encode(sig_vec.clone()); + println!("sig_vec_hex: {:?}", sig_vec_hex); + Ok(sig_vec_hex) }))), owner, } diff --git a/crates/yttrium/src/signer.rs b/crates/yttrium/src/signer.rs index 0c7d83c..c1ac481 100644 --- a/crates/yttrium/src/signer.rs +++ b/crates/yttrium/src/signer.rs @@ -147,6 +147,59 @@ where } } +pub fn sign_user_operation_v07_with_ecdsa_and_sign_service( + uo: &UserOperationV07, + ep: &Address, + chain_id: u64, + signer: PrivateKeySigner, + sign_service: &Arc>, +) -> eyre::Result { + let hash = uo.hash(&ep, chain_id)?; + + println!("hash: {:?}", hash.clone()); + + let message = hash.0; + + println!("message: {:?}", message.clone()); + + let message_bytes = message.to_vec(); + + println!("message_bytes: {:?}", message_bytes.clone()); + + let sign_service = Arc::clone(sign_service); + let sign_service = sign_service.try_lock()?; + + let message_hex = hex::encode(message_bytes.clone()); + + let signature_native = sign_service.sign(message_hex)?; + + println!("signature_native: {:?}", signature_native); + + let signature_native_bytes = hex::decode(signature_native.clone())?; + + { + let signature = signer.sign_message_sync(&message_bytes)?; + println!("signature: {:?}", signature); + let sig_vec: Vec = signature.into(); + let sig_vec_hex = hex::encode(sig_vec.clone()); + println!("sig_vec_hex: {:?}", sig_vec_hex); + + assert_eq!( + sig_vec, signature_native_bytes, + "sig_vec != signature_native_bytes" + ); + assert_eq!( + sig_vec_hex, signature_native, + "sig_vec_hex != signature_native" + ); + } + let sig_vec = signature_native_bytes; + + let mut user_operation = uo.clone(); + user_operation.signature = sig_vec.into(); + Ok(user_operation) +} + pub fn sign_user_operation_v07_with_ecdsa( uo: &UserOperationV07, ep: &Address, diff --git a/crates/yttrium/src/smart_accounts.rs b/crates/yttrium/src/smart_accounts.rs index 03862da..282ef6b 100644 --- a/crates/yttrium/src/smart_accounts.rs +++ b/crates/yttrium/src/smart_accounts.rs @@ -1,3 +1,4 @@ +pub mod deployed; pub mod nonce; pub mod safe; pub mod simple_account; diff --git a/crates/yttrium/src/smart_accounts/deployed.rs b/crates/yttrium/src/smart_accounts/deployed.rs new file mode 100644 index 0000000..cdbc5f9 --- /dev/null +++ b/crates/yttrium/src/smart_accounts/deployed.rs @@ -0,0 +1,88 @@ +use alloy::{ + contract::private::{Network, Provider, Transport}, + primitives::Address, +}; +use core::clone::Clone; + +pub async fn is_smart_account_deployed( + provider: &P, + sender_address: Address, +) -> eyre::Result +where + T: Transport + Clone, + P: Provider, + N: Network, +{ + let contract_code = provider.get_code_at(sender_address).await?; + + if contract_code.len() > 2 { + return Ok(true); + } + + Ok(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::{ + network::EthereumWallet, + providers::ProviderBuilder, + signers::local::{coins_bip39::English, MnemonicBuilder}, + }; + + const MNEMONIC_PHRASE: &str = + "test test test test test test test test test test test junk"; + + #[tokio::test] + async fn test_is_smart_account_deployed() -> eyre::Result<()> { + let config = crate::config::Config::local(); + 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 (owner_address, _local_signer, provider) = { + let phrase = MNEMONIC_PHRASE; + let index: u32 = 0; + let local_signer = MnemonicBuilder::::default() + .phrase(phrase) + .index(index)? + .build()?; + let ethereum_wallet = EthereumWallet::from(local_signer.clone()); + let rpc_url_string = config.endpoints.rpc.base_url; + let rpc_url: reqwest::Url = rpc_url_string.parse()?; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(ethereum_wallet.clone()) + .on_http(rpc_url); + let owner = ethereum_wallet.clone().default_signer(); + let owner_address = owner.address(); + eyre::Ok((owner_address, local_signer, provider)) + }?; + + use crate::smart_accounts::simple_account::factory::FactoryAddress; + let simple_account_factory_address = FactoryAddress::local_v07(); + use crate::entry_point::get_sender_address::get_sender_address_v07; + + use crate::smart_accounts::simple_account::create_account::SimpleAccountCreate; + + let factory_data_call = SimpleAccountCreate::new_u64(owner_address, 0); + let factory_data_value = factory_data_call.encode(); + + let sender_address = get_sender_address_v07( + &provider, + simple_account_factory_address.clone().into(), + factory_data_value.into(), + entry_point_address, + ) + .await?; + + let is_deployed = + is_smart_account_deployed(&provider, sender_address).await?; + + println!("is_deployed: {:?}", is_deployed); + + Ok(()) + } +} diff --git a/crates/yttrium/src/smart_accounts/nonce.rs b/crates/yttrium/src/smart_accounts/nonce.rs index d086468..8193f01 100644 --- a/crates/yttrium/src/smart_accounts/nonce.rs +++ b/crates/yttrium/src/smart_accounts/nonce.rs @@ -1,19 +1,25 @@ -use alloy::primitives::aliases::U192; +use crate::{ + entry_point::{EntryPoint, EntryPointAddress}, + smart_accounts::simple_account::SimpleAccountAddress, +}; +use alloy::{ + contract::private::{Network, Provider, Transport}, + primitives::aliases::U192, +}; +use core::clone::Clone; pub async fn get_nonce( provider: &P, - address: &crate::smart_accounts::simple_account::SimpleAccountAddress, - entry_point_address: &crate::entry_point::EntryPointAddress, + address: &SimpleAccountAddress, + entry_point_address: &EntryPointAddress, ) -> eyre::Result where - T: alloy::contract::private::Transport + ::core::clone::Clone, - P: alloy::contract::private::Provider, - N: alloy::contract::private::Network, + T: Transport + Clone, + P: Provider, + N: Network, { - let entry_point_instance = crate::entry_point::EntryPoint::new( - entry_point_address.to_address(), - provider, - ); + let entry_point_instance = + EntryPoint::new(entry_point_address.to_address(), provider); let key = U192::ZERO; let get_nonce_call = diff --git a/crates/yttrium/src/smart_accounts/simple_account.rs b/crates/yttrium/src/smart_accounts/simple_account.rs index 903c7c0..df97871 100644 --- a/crates/yttrium/src/smart_accounts/simple_account.rs +++ b/crates/yttrium/src/smart_accounts/simple_account.rs @@ -7,6 +7,7 @@ sol!( pub mod create_account; pub mod factory; +pub mod sender_address; pub struct SimpleAccountExecute(executeCall); diff --git a/crates/yttrium/src/smart_accounts/simple_account/factory.rs b/crates/yttrium/src/smart_accounts/simple_account/factory.rs index 66db655..d082538 100644 --- a/crates/yttrium/src/smart_accounts/simple_account/factory.rs +++ b/crates/yttrium/src/smart_accounts/simple_account/factory.rs @@ -1,6 +1,36 @@ use crate::chain::ChainId; use crate::entry_point::EntryPointVersion; -use alloy::{primitives::Address, sol}; +use alloy::{ + contract::private::{Network, Provider, Transport}, + primitives::Address, + sol, +}; +use std::marker::PhantomData; + +pub struct FactoryInstance { + address: Address, + provider: P, + _network_transport: PhantomData<(N, T)>, +} + +impl + Clone, N: Network> + FactoryInstance +{ + pub fn new(address: Address, provider: P) -> Self { + Self { address, provider, _network_transport: PhantomData } + } + + pub fn local_v07(provider: P) -> Self { + let address = FactoryAddress::local_v07().to_address(); + Self::new(address, provider) + } + + pub fn instance( + self, + ) -> SimpleAccountFactory::SimpleAccountFactoryInstance { + SimpleAccountFactory::new(self.address, self.provider) + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FactoryAddress(alloy::primitives::Address); diff --git a/crates/yttrium/src/smart_accounts/simple_account/sender_address.rs b/crates/yttrium/src/smart_accounts/simple_account/sender_address.rs new file mode 100644 index 0000000..7b48de8 --- /dev/null +++ b/crates/yttrium/src/smart_accounts/simple_account/sender_address.rs @@ -0,0 +1,83 @@ +use crate::{ + bundler::{ + client::BundlerClient, config::BundlerConfig, + pimlico::client::BundlerClient as PimlicoBundlerClient, + }, + chain::ChainId, + config::Config, + entry_point::{ + get_sender_address::get_sender_address_v07, EntryPointVersion, + }, + smart_accounts::simple_account::{ + create_account::SimpleAccountCreate, factory::FactoryAddress, + }, +}; +use alloy::{ + network::EthereumWallet, primitives::Address, providers::ProviderBuilder, + signers::local::PrivateKeySigner, +}; + +pub async fn get_sender_address_with_signer( + config: Config, + chain_id: u64, + signer: PrivateKeySigner, +) -> eyre::Result
{ + let bundler_base_url = config.clone().endpoints.bundler.base_url; + let paymaster_base_url = config.clone().endpoints.paymaster.base_url; + let rpc_base_url = config.clone().endpoints.rpc.base_url; + + let chain_id = ChainId::new_eip155(chain_id.clone()); + let chain = + crate::chain::Chain::new(chain_id.clone(), EntryPointVersion::V07, ""); + + let entry_point_config = chain.entry_point_config(); + + let entry_point_address = entry_point_config.address(); + + // Create a provider + + let alloy_signer = signer; + let ethereum_wallet = EthereumWallet::new(alloy_signer.clone()); + + let owner = ethereum_wallet.clone().default_signer(); + let owner_address = owner.address(); + + let rpc_url = rpc_base_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 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); + + Ok(sender_address) +} diff --git a/crates/yttrium/src/transaction.rs b/crates/yttrium/src/transaction.rs index 24fc246..8fa6a1d 100644 --- a/crates/yttrium/src/transaction.rs +++ b/crates/yttrium/src/transaction.rs @@ -1,10 +1,30 @@ +use alloy::primitives::{address, Address, Bytes, U256}; +use std::str::FromStr; + pub mod send; #[derive(Debug, Clone, PartialEq)] pub struct Transaction { - pub to: String, - pub value: String, - pub data: String, + pub to: Address, + pub value: U256, + pub data: Bytes, +} + +impl Transaction { + pub fn new(to: Address, value: U256, data: Bytes) -> Self { + Self { to, value, data } + } + + pub fn new_from_strings( + to: String, + value: String, + data: String, + ) -> eyre::Result { + let to = to.parse()?; + let value = value.parse()?; + let data = data.parse()?; + Ok(Self { to, value, data }) + } } impl std::fmt::Display for Transaction { @@ -20,9 +40,36 @@ impl std::fmt::Display for Transaction { impl Transaction { pub fn mock() -> Self { Self { - to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string(), - value: "0".to_string(), - data: "0x68656c6c6f".to_string(), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"), + value: U256::ZERO, + data: "0x68656c6c6f".parse().unwrap(), } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_from_strings() -> eyre::Result<()> { + let expected_transaction = Transaction::mock(); + + let transaction = Transaction::new_from_strings( + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string(), + "0".to_string(), + "0x68656c6c6f".to_string(), + )?; + + println!("transaction: {:?}", transaction); + + eyre::ensure!( + transaction == expected_transaction, + "transaction {} should be equal to expected transaction {}", + transaction, + expected_transaction + ); + + Ok(()) + } +} diff --git a/crates/yttrium/src/transaction/send.rs b/crates/yttrium/src/transaction/send.rs index f93202b..fac6cfa 100644 --- a/crates/yttrium/src/transaction/send.rs +++ b/crates/yttrium/src/transaction/send.rs @@ -1,13 +1,13 @@ +use crate::transaction::send::simple_account_test::send_transaction_with_signer; use crate::{ - sign_service::SignService, transaction::Transaction, - user_operation::UserOperationV07, + config::Config, transaction::Transaction, user_operation::UserOperationV07, }; +use alloy::signers::local::PrivateKeySigner; use core::fmt; -use std::sync::Arc; -use tokio::sync::Mutex; mod safe_test; -mod simple_account_test; +pub mod send_tests; +pub mod simple_account_test; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UserOperationEstimated(UserOperationV07); @@ -42,14 +42,54 @@ impl fmt::Display for SentUserOperationHash { } } -pub mod send_tests; +use crate::account_client::Signer; pub async fn send_transaction( - sign_service: Arc>, transaction: Transaction, + owner: String, + chain_id: u64, + config: Config, + signer: Signer, +) -> eyre::Result { + match signer { + Signer::PrivateKey(private_key_service) => { + let private_key_service = private_key_service.clone(); + let private_key_service = private_key_service.lock().await; + let private_key_signer_key = + private_key_service.private_key().unwrap(); + let private_key_signer: PrivateKeySigner = + private_key_signer_key.parse().unwrap(); + send_transaction_with_private_key_signer( + transaction, + owner, + chain_id, + config, + private_key_signer, + ) + .await + } + Signer::Native(sign_service) => { + todo!("Implement native signer support") + } + } +} + +pub async fn send_transaction_with_private_key_signer( + transaction: Transaction, + owner: String, + chain_id: u64, + config: Config, + private_key_signer: PrivateKeySigner, ) -> eyre::Result { - let _ = sign_service.try_lock()?; - todo!("Calling send_transaction with transaction: {transaction:?}") + let signer = private_key_signer; + + let user_operation_hash = + send_transaction_with_signer(transaction, config, chain_id, signer) + .await?; + + println!("user_operation_hash: {:?}", user_operation_hash); + + Ok(user_operation_hash) } #[cfg(test)] @@ -65,6 +105,7 @@ mod tests { }, }, entry_point::get_sender_address::get_sender_address_v07, + sign_service::SignService, signer::sign_user_operation_v07_with_ecdsa, smart_accounts::{ nonce::get_nonce, @@ -84,11 +125,13 @@ mod tests { }, }; use std::str::FromStr; + use std::sync::Arc; + use tokio::sync::Mutex; const MNEMONIC_PHRASE: &str = "test test test test test test test test test test test junk"; - async fn send_transaction( + async fn send_transaction_alt( sign_service: Arc>, transaction: Transaction, ) -> eyre::Result { @@ -109,7 +152,7 @@ mod tests { 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 chain_id = chain.id.eip155_chain_id(); let entry_point_address = entry_point_config.address(); @@ -186,11 +229,9 @@ mod tests { 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 to = transaction.to; + let value = transaction.value; + let data = transaction.data; let call_data = SimpleAccountExecute::new(to, value, data); let call_data_encoded = call_data.encode(); @@ -312,21 +353,19 @@ mod tests { #[tokio::test] #[ignore = "TODO: rewrite against local infrastructure"] - async fn test_send_transaction() -> eyre::Result<()> { + async fn test_send_transaction_alt() -> 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 = crate::sign_service::SignService::new_with_mnemonic( + mnemonic.clone(), + ); let sign_service_arc = Arc::new(Mutex::new(sign_service)); let transaction_hash = - send_transaction(sign_service_arc, transaction).await?; + send_transaction_alt(sign_service_arc, transaction).await?; println!("Transaction sent: {}", transaction_hash); diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index b5090fa..9c7e7fb 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -90,7 +90,7 @@ mod tests { 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 chain_id = chain.id.eip155_chain_id(); let entry_point_address = entry_point_config.address(); diff --git a/crates/yttrium/src/transaction/send/send_tests/test_send_pimlico_v07.rs b/crates/yttrium/src/transaction/send/send_tests/test_send_pimlico_v07.rs index 267f70e..ed5a7b4 100644 --- a/crates/yttrium/src/transaction/send/send_tests/test_send_pimlico_v07.rs +++ b/crates/yttrium/src/transaction/send/send_tests/test_send_pimlico_v07.rs @@ -19,6 +19,8 @@ mod tests { signers::local::{coins_bip39::English, MnemonicBuilder}, }; use std::str::FromStr; + use std::sync::Arc; + use tokio::sync::Mutex; const SIMPLE_ACCOUNT_FACTORY_ADDRESS: &str = "0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985"; @@ -48,11 +50,10 @@ mod tests { let phrase = MNEMONIC_PHRASE; let index: u32 = 0; - 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 chain_id = chain.id.eip155_chain_id(); let wallet = MnemonicBuilder::::default() .phrase(phrase) @@ -64,6 +65,12 @@ mod tests { let ethereum_wallet = EthereumWallet::from(wallet.clone()); + let mnemonic = phrase.to_string(); + + let sign_service = crate::sign_service::SignService::new_with_mnemonic( + mnemonic.clone(), + ); + let rpc_url = config.endpoints.rpc.base_url; // Create a provider with the wallet. @@ -190,7 +197,6 @@ mod tests { let paymaster_client = PaymasterClient::new(BundlerConfig::new(bundler_base_url.clone())); - let sponsor_user_op_result = paymaster_client .sponsor_user_operation_v07( &user_op.clone().into(), @@ -222,12 +228,14 @@ mod tests { // 9. Sign the UserOperation - let signed_user_op = crate::signer::sign_user_operation_v07_with_ecdsa( - &sponsored_user_op.clone(), - &entry_point_address.to_address(), - chain_id, - alloy_signer, - )?; + let signed_user_op = + crate::signer::sign_user_operation_v07_with_ecdsa_and_sign_service( + &sponsored_user_op.clone(), + &entry_point_address.to_address(), + chain_id, + alloy_signer, + &Arc::new(Mutex::new(sign_service)), + )?; println!("Generated signature: {:?}", signed_user_op.signature); diff --git a/crates/yttrium/src/transaction/send/simple_account_test.rs b/crates/yttrium/src/transaction/send/simple_account_test.rs index 2a595be..4f8ca12 100644 --- a/crates/yttrium/src/transaction/send/simple_account_test.rs +++ b/crates/yttrium/src/transaction/send/simple_account_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, + }, + }, + chain::ChainId, + config::Config, + entry_point::get_sender_address::get_sender_address_v07, + sign_service::SignService, + 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::{coins_bip39::English, MnemonicBuilder, PrivateKeySigner}, +}; use core::fmt; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::Mutex; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UserOperationEstimated(UserOperationV07); @@ -34,8 +66,220 @@ impl fmt::Display for SentUserOperationHash { } } +pub async fn send_transaction_with_signer( + transaction: Transaction, + config: Config, + chain_id: u64, + signer: PrivateKeySigner, +) -> eyre::Result { + let bundler_base_url = config.clone().endpoints.bundler.base_url; + let paymaster_base_url = config.clone().endpoints.paymaster.base_url; + let rpc_base_url = config.clone().endpoints.rpc.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())); + + use crate::entry_point::EntryPointVersion; + let chain_id = ChainId::new_eip155(chain_id.clone()); + let chain = + crate::chain::Chain::new(chain_id.clone(), EntryPointVersion::V07, ""); + let entry_point_config = chain.entry_point_config(); + + let entry_point_address = entry_point_config.address(); + + // Create a provider + + let alloy_signer = signer; + let ethereum_wallet = EthereumWallet::new(alloy_signer.clone()); + + let owner = ethereum_wallet.clone().default_signer(); + let owner_address = owner.address(); + + let rpc_url = rpc_base_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 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; + let value: alloy::primitives::Uint<256, 4> = transaction.value; + let data = transaction.data; + + 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); + + let is_deployed = + crate::smart_accounts::deployed::is_smart_account_deployed( + &provider, + sender_address, + ) + .await?; + + println!("is_deployed: {:?}", is_deployed); + + let factory: Option
= if !is_deployed { + Some(simple_account_factory_address.to_address()) + } else { + None + }; + + let factory_data: Option = + if !is_deployed { Some(factory_data_value.into()) } else { None }; + + // 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: factory, + factory_data: factory_data, + 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.eip155_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); + + // TODO convert to polling + use std::time::Duration; + tokio::time::sleep(Duration::from_secs(2)).await; + + 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) +} + #[cfg(test)] mod tests { + use super::*; use crate::{ bundler::{ client::BundlerClient, @@ -61,7 +305,7 @@ mod tests { network::EthereumWallet, primitives::{Address, Bytes, U256}, providers::ProviderBuilder, - signers::local::LocalSigner, + signers::{local::LocalSigner, SignerSync}, }; use std::str::FromStr; @@ -83,7 +327,7 @@ mod tests { 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 chain_id = chain.id.eip155_chain_id(); let entry_point_address = entry_point_config.address(); @@ -94,6 +338,9 @@ mod tests { let alloy_signer = LocalSigner::random(); let ethereum_wallet = EthereumWallet::new(alloy_signer.clone()); + let owner = ethereum_wallet.clone().default_signer(); + let owner_address = owner.address(); + let rpc_url: reqwest::Url = rpc_url.parse()?; let provider = ProviderBuilder::new() .with_recommended_fillers() @@ -105,9 +352,6 @@ mod tests { 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(); @@ -134,11 +378,9 @@ mod tests { 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 to = transaction.to; + let value = transaction.value; + let data = transaction.data; let call_data = SimpleAccountExecute::new(to, value, data); let call_data_encoded = call_data.encode(); diff --git a/platforms/swift/Sources/Yttrium/AccountClient.swift b/platforms/swift/Sources/Yttrium/AccountClient.swift index 568efee..e49630e 100644 --- a/platforms/swift/Sources/Yttrium/AccountClient.swift +++ b/platforms/swift/Sources/Yttrium/AccountClient.swift @@ -3,13 +3,7 @@ import YttriumCore public final class AccountClient: AccountClientProtocol { - public var onSign: OnSign? { - didSet { - if let onSign = onSign { - register(onSign: onSign) - } - } - } + public let ownerAddress: String public let chainId: Int @@ -17,43 +11,91 @@ public final class AccountClient: AccountClientProtocol { private let coreAccountClient: YttriumCore.FFIAccountClient - public init(ownerAddress: String, entryPoint: String, chainId: Int, onSign: OnSign?) { - let config: FFIAccountClientConfig = FFIAccountClientConfig( + public convenience init( + ownerAddress: String, + entryPoint: String, + chainId: Int, + config: Config + ) { + self.init( + ownerAddress: ownerAddress, + entryPoint: entryPoint, + chainId: chainId, + config: config, + signerType: .privateKey + ) + } + + init( + ownerAddress: String, + entryPoint: String, + chainId: Int, + config: Config, + signerType: SignerType + ) { + let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( + owner_address: ownerAddress.intoRustString(), + chain_id: UInt64(chainId), + config: config.ffi, + signer_type: signerType.toRustString() + ) + self.ownerAddress = ownerAddress + self.chainId = chainId + self.entryPoint = entryPoint + self.coreAccountClient = FFIAccountClient(ffiConfig) + } + + init( + ownerAddress: String, + entryPoint: String, + chainId: Int, + config: Config, + signer: Signer + ) { + let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( owner_address: ownerAddress.intoRustString(), - chain_id: Int64(chainId), - config: .init( - endpoints: .init( - rpc: .init( - api_key: "".intoRustString(), - base_url: "https://localhost:8545".intoRustString() // TODO - ), - bundler: .init( - api_key: "".intoRustString(), - base_url: "https://localhost:4337".intoRustString() // TODO - ), - paymaster: .init( - api_key: "".intoRustString(), - base_url: "https://localhost:4337".intoRustString() // TODO - ) - ) - ) + chain_id: UInt64(chainId), + config: config.ffi, + signer_type: signer.signerType.toRustString() ) + self.ownerAddress = ownerAddress self.chainId = chainId self.entryPoint = entryPoint - self.coreAccountClient = FFIAccountClient(config) + self.coreAccountClient = FFIAccountClient(ffiConfig) + + register(signer: signer) + } + + public func register(privateKey: String) { + let signerId: SignerId = .init( + signerType: .privateKey, + account: ownerAddress, + chainId: chainId + ) + let privateKeySigner = PrivateKeySigner( + id: signerId, + privateKey: privateKey + ) + register(signer: .privateKey(privateKeySigner)) } - private func register(onSign: @escaping OnSign) { - let signer: Signer = .init( - signerId: .init( - account: entryPoint, - chainId: chainId - ), + func register(onSign: @escaping OnSign) { + let signerId: SignerId = .init( + signerType: .native, + account: ownerAddress, + chainId: chainId + ) + let nativeSigner = NativeSigner( + id: signerId, onSign: { message in onSign(message) .mapError(YttriumCore.SignerError.from(error:)) } ) + register(signer: .native(nativeSigner)) + } + + private func register(signer: Signer) { Signers.shared.register(signer: signer) } @@ -72,6 +114,17 @@ public final class AccountClient: AccountClientProtocol { public func signMessage(_ message: String) throws -> String { fatalError("Not yet implemented") } + + func signMessageWithMnemonic( + message: String, + mnemonic: String + ) throws -> String { + try coreAccountClient.sign_message_with_mnemonic( + message.intoRustString(), + mnemonic.intoRustString() + ) + .toString() + } } extension Transaction { diff --git a/platforms/swift/Sources/Yttrium/AccountClient7702.swift b/platforms/swift/Sources/Yttrium/AccountClient7702.swift index f74dfb5..524b0c2 100644 --- a/platforms/swift/Sources/Yttrium/AccountClient7702.swift +++ b/platforms/swift/Sources/Yttrium/AccountClient7702.swift @@ -3,13 +3,7 @@ import YttriumCore public final class AccountClient7702 { - public var onSign: OnSign? { - didSet { - if let onSign = onSign { - register(onSign: onSign) - } - } - } + public let ownerAddress: String public let chainId: Int @@ -17,43 +11,91 @@ public final class AccountClient7702 { private let core7702AccountClient: YttriumCore.FFI7702AccountClient - public init(ownerAddress: String, entryPoint: String, chainId: Int, onSign: OnSign?) { - let config: FFIAccountClientConfig = FFIAccountClientConfig( + public convenience init( + ownerAddress: String, + entryPoint: String, + chainId: Int, + config: Config + ) { + self.init( + ownerAddress: ownerAddress, + entryPoint: entryPoint, + chainId: chainId, + config: config, + signerType: .privateKey + ) + } + + init( + ownerAddress: String, + entryPoint: String, + chainId: Int, + config: Config, + signerType: SignerType + ) { + let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( + owner_address: ownerAddress.intoRustString(), + chain_id: UInt64(chainId), + config: config.ffi, + signer_type: signerType.toRustString() + ) + self.ownerAddress = ownerAddress + self.chainId = chainId + self.entryPoint = entryPoint + self.core7702AccountClient = FFI7702AccountClient(ffiConfig) + } + + init( + ownerAddress: String, + entryPoint: String, + chainId: Int, + config: Config, + signer: Signer + ) { + let ffiConfig: FFIAccountClientConfig = FFIAccountClientConfig( owner_address: ownerAddress.intoRustString(), - chain_id: Int64(chainId), - config: .init( - endpoints: .init( - rpc: .init( - api_key: "".intoRustString(), - base_url: "https://localhost:8545".intoRustString() // TODO - ), - bundler: .init( - api_key: "".intoRustString(), - base_url: "https://localhost:4337".intoRustString() // TODO - ), - paymaster: .init( - api_key: "".intoRustString(), - base_url: "https://localhost:4337".intoRustString() // TODO - ) - ) - ) + chain_id: UInt64(chainId), + config: config.ffi, + signer_type: signer.signerType.toRustString() ) + self.ownerAddress = ownerAddress self.chainId = chainId self.entryPoint = entryPoint - self.core7702AccountClient = FFI7702AccountClient(config) + self.core7702AccountClient = FFI7702AccountClient(ffiConfig) + + register(signer: signer) } - private func register(onSign: @escaping OnSign) { - let signer: Signer = .init( - signerId: .init( - account: entryPoint, - chainId: chainId - ), + public func register(privateKey: String) { + let signerId: SignerId = .init( + signerType: .privateKey, + account: ownerAddress, + chainId: chainId + ) + let privateKeySigner = PrivateKeySigner( + id: signerId, + privateKey: privateKey + ) + register(signer: .privateKey(privateKeySigner)) + } + + func register(onSign: @escaping OnSign) { + let signerId: SignerId = .init( + signerType: .native, + account: ownerAddress, + chainId: chainId + ) + let nativeSigner = NativeSigner( + id: signerId, onSign: { message in onSign(message) .mapError(YttriumCore.SignerError.from(error:)) } ) + register(signer: .native(nativeSigner)) + } + + private func register(signer: Signer) { Signers.shared.register(signer: signer) } diff --git a/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift b/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift index 4ed838e..6723b92 100644 --- a/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift +++ b/platforms/swift/Sources/Yttrium/AccountClientProtocol.swift @@ -1,4 +1,5 @@ import Foundation +import YttriumCore public struct Transaction: Codable, Equatable { let to: String @@ -20,11 +21,11 @@ public typealias OnSign = (String) -> Result public protocol AccountClientProtocol { - var onSign: OnSign? { get set } - var chainId: Int { get } - init(ownerAddress: String, entryPoint: String, chainId: Int, onSign: OnSign?) + init(ownerAddress: String, entryPoint: String, chainId: Int, config: Config) + + func register(privateKey: String) func sendTransaction(_ transaction: Transaction) async throws -> String func sendBatchTransaction(_ batch: [Transaction]) async throws -> String diff --git a/platforms/swift/Sources/Yttrium/Config.swift b/platforms/swift/Sources/Yttrium/Config.swift new file mode 100644 index 0000000..bd416bd --- /dev/null +++ b/platforms/swift/Sources/Yttrium/Config.swift @@ -0,0 +1,114 @@ +import Foundation +import YttriumCore + +public struct Endpoint { + + public let baseURL: String + + public let apiKey: String? + + public init(baseURL: String, apiKey: String? = nil) { + self.baseURL = baseURL + self.apiKey = apiKey + } +} + +public struct Endpoints { + + public let rpc: Endpoint + + public let bundler: Endpoint + + public let paymaster: Endpoint + + public init(rpc: Endpoint, bundler: Endpoint, paymaster: Endpoint) { + self.rpc = rpc + self.bundler = bundler + self.paymaster = paymaster + } +} + +public struct Config { + + public let endpoints: Endpoints + + public init(endpoints: Endpoints) { + self.endpoints = endpoints + } +} + +extension Endpoint { + + public static func localRPC() -> Self { + Self(baseURL: "http://localhost:8545") + } + + public static func localBundler() -> Self { + Self(baseURL: "http://localhost:4337") + } + + public static func localPaymaster() -> Self { + Self(baseURL: "http://localhost:3000") + } + + public var ffi: FFIEndpoint { + FFIEndpoint( + api_key: (apiKey ?? "").intoRustString(), + base_url: baseURL.intoRustString() + ) + } +} + +extension Endpoints { + + public static func local() -> Self { + Endpoints( + rpc: .localRPC(), + bundler: .localBundler(), + paymaster: .localPaymaster() + ) + } + + + public var ffi: FFIEndpoints { + FFIEndpoints( + rpc: rpc.ffi, + bundler: bundler.ffi, + paymaster: paymaster.ffi + ) + } +} + +import SwiftDotenv + +extension Config { + + public static func local() -> Self { + Config(endpoints: .local()) + } + + static func pimlico() throws -> Self { + try Dotenv.configure() + + guard let rpcURL = Dotenv["PIMLICO_RPC_URL"]?.stringValue else { + fatalError("Missing PIMLICO_RPC_URL environment variable") + } + guard let bundlerURL = Dotenv["PIMLICO_BUNDLER_URL"]?.stringValue else { + fatalError("Missing PIMLICO_BUNDLER_URL environment variable") + } + let paymasterURL = bundlerURL + + return Self( + endpoints: .init( + rpc: .init(baseURL: rpcURL), + bundler: .init(baseURL: bundlerURL), + paymaster: .init(baseURL: paymasterURL) + ) + ) + } + + + public var ffi: FFIConfig { + FFIConfig(endpoints: endpoints.ffi) + } +} diff --git a/platforms/swift/Tests/YttriumTests/AccountClientTests.swift b/platforms/swift/Tests/YttriumTests/AccountClientTests.swift index 3c31949..012179e 100644 --- a/platforms/swift/Tests/YttriumTests/AccountClientTests.swift +++ b/platforms/swift/Tests/YttriumTests/AccountClientTests.swift @@ -4,23 +4,65 @@ import XCTest @testable import Yttrium final class AccountClientTests: XCTestCase { - func testGetAddress() async throws { - let accountAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - let ownerAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - let chainId = 0 + + static let mnemonic = "test test test test test test test test test test test junk" + + /// Using `mnemonic` derived at `m/44'/60'/0'/0/0` + static let privateKeyHex = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + + static let entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + + /// `Ethereum Sepolia` chain ID + static let chainId = 11155111 + + static let ownerAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + + static let simpleAccountAddress = "0x75BD33d92EEAC5Fe41446fcF5953050d691E7fc9" + + func testSendTransaction() async throws { + let config = Config.local() + let accountClient = AccountClient( - ownerAddress: ownerAddress, // TODO - entryPoint: accountAddress, // TODO - chainId: chainId, - onSign: { _ in - fatalError() - } + ownerAddress: Self.ownerAddress, + entryPoint: Self.entryPoint, + chainId: Self.chainId, + config: config, + signerType: .privateKey ) + accountClient.register(privateKey: Self.privateKeyHex) - let expectedAddress = "EXPECTED_ADDRESS" + let transaction = Transaction.mock() + + let user_operation_hash = try await accountClient.sendTransaction(transaction) + } + + func testGetAddress() async throws { + let config = Config.local() + + let accountClient = AccountClient( + ownerAddress: Self.ownerAddress, + entryPoint: Self.entryPoint, + chainId: Self.chainId, + config: config, + signerType: .privateKey + ) + accountClient.register(privateKey: Self.privateKeyHex) + + let expectedAddress = Self.simpleAccountAddress let address = try await accountClient.getAddress() XCTAssertEqual(address, expectedAddress) } } + +extension Transaction { + + public static func mock() -> Self { + Self( + to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + value: "0", + data: "0x68656c6c6f" + ) + } +} diff --git a/test/scripts/forked_state/docker-compose.yaml b/test/scripts/forked_state/docker-compose.yaml index d9f5aa3..2665b68 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" ] + entrypoint: [ "anvil", "--fork-url", "https://gateway.tenderly.co/public/sepolia", "--host", "0.0.0.0", "--block-time", "0.1", "--gas-price", "1", "--silent" ] platform: linux/amd64/v8 mock-paymaster: