From 4089a616ccac2467c0e22f739859e36c0185d24c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 19 Dec 2024 16:05:34 -0500 Subject: [PATCH] chore: more impl --- crates/kotlin-ffi/src/lib.rs | 2 +- .../bundler/models/user_operation_receipt.rs | 3 + .../yttrium/src/chain_abstraction/client.rs | 12 +- .../src/examples/eip7702_smart_sessions.rs | 27 +-- .../yttrium/src/execution/send/safe_test.rs | 1 + crates/yttrium/src/gas_abstraction/mod.rs | 200 ++++++++++++++---- crates/yttrium/src/gas_abstraction/tests.rs | 111 +++++++--- crates/yttrium/src/provider_pool.rs | 21 +- crates/yttrium/src/smart_accounts/safe.rs | 13 ++ crates/yttrium/src/uniffi_compat.rs | 13 +- 10 files changed, 293 insertions(+), 110 deletions(-) diff --git a/crates/kotlin-ffi/src/lib.rs b/crates/kotlin-ffi/src/lib.rs index a800a773..231f580b 100644 --- a/crates/kotlin-ffi/src/lib.rs +++ b/crates/kotlin-ffi/src/lib.rs @@ -196,7 +196,7 @@ impl ChainAbstractionClient { pub async fn erc20_token_balance( &self, - chain_id: String, + chain_id: &str, token: FFIAddress, owner: FFIAddress, ) -> Result { diff --git a/crates/yttrium/src/bundler/models/user_operation_receipt.rs b/crates/yttrium/src/bundler/models/user_operation_receipt.rs index 836b848e..444b540c 100644 --- a/crates/yttrium/src/bundler/models/user_operation_receipt.rs +++ b/crates/yttrium/src/bundler/models/user_operation_receipt.rs @@ -6,6 +6,7 @@ use { }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct TransactionReceipt { pub transaction_hash: TxHash, @@ -25,6 +26,7 @@ pub struct TransactionReceipt { // TODO replace with alloy's UserOperationReceipt #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct UserOperationReceipt { pub user_op_hash: B256, @@ -41,6 +43,7 @@ pub struct UserOperationReceipt { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[serde(rename_all = "camelCase")] pub struct TransactionLog { pub address: Address, diff --git a/crates/yttrium/src/chain_abstraction/client.rs b/crates/yttrium/src/chain_abstraction/client.rs index 76ac6a69..4e58148f 100644 --- a/crates/yttrium/src/chain_abstraction/client.rs +++ b/crates/yttrium/src/chain_abstraction/client.rs @@ -26,6 +26,7 @@ use { chain_abstraction::{ error::UiFieldsError, l1_data_fee::get_l1_data_fee, ui_fields, }, + config::Config, erc20::ERC20, provider_pool::ProviderPool, }, @@ -49,7 +50,7 @@ pub struct Client { impl Client { pub fn new(project_id: ProjectId) -> Self { - Self { provider_pool: ProviderPool::new(project_id) } + Self { provider_pool: ProviderPool::new(project_id, Config::local()) } } pub async fn prepare( @@ -155,7 +156,7 @@ impl Client { |chain_id| async move { let estimate = self .provider_pool - .get_provider(chain_id.clone()) + .get_provider(chain_id) .await .estimate_eip1559_fees(None) .await @@ -186,10 +187,7 @@ impl Client { ) .with_max_fee_per_gas(100000) .with_max_priority_fee_per_gas(1), - providers - .provider_pool - .get_provider(txn.chain_id.clone()) - .await, + providers.provider_pool.get_provider(&txn.chain_id).await, ) .await) } @@ -392,7 +390,7 @@ impl Client { pub async fn erc20_token_balance( &self, - chain_id: String, + chain_id: &str, token: Address, owner: Address, ) -> Result { diff --git a/crates/yttrium/src/examples/eip7702_smart_sessions.rs b/crates/yttrium/src/examples/eip7702_smart_sessions.rs index 0b137633..fb03b651 100644 --- a/crates/yttrium/src/examples/eip7702_smart_sessions.rs +++ b/crates/yttrium/src/examples/eip7702_smart_sessions.rs @@ -27,8 +27,9 @@ use { smart_accounts::{ nonce::get_nonce_with_key, safe::{ - get_call_data, Owners, SAFE_4337_MODULE_ADDRESS, - SAFE_ERC_7579_LAUNCHPAD_ADDRESS, SAFE_L2_SINGLETON_1_4_1, + get_call_data, AddSafe7579Contract, Owners, SetupContract, + SAFE_4337_MODULE_ADDRESS, SAFE_ERC_7579_LAUNCHPAD_ADDRESS, + SAFE_L2_SINGLETON_1_4_1, }, }, test_helpers::anvil_faucet, @@ -41,7 +42,6 @@ use { }, rpc::types::Authorization, signers::{local::LocalSigner, SignerSync}, - sol, sol_types::SolCall, }, alloy_provider::{Provider, ProviderBuilder, ReqwestProvider}, @@ -99,25 +99,6 @@ async fn test() { let sig = account.sign_hash_sync(&auth_7702.signature_hash()).unwrap(); let auth = auth_7702.into_signed(sig); - sol! { - #[allow(clippy::too_many_arguments)] - #[sol(rpc)] - contract SetupContract { - function setup(address[] calldata _owners,uint256 _threshold,address to,bytes calldata data,address fallbackHandler,address paymentToken,uint256 payment, address paymentReceiver) external; - } - - #[allow(clippy::too_many_arguments)] - #[sol(rpc)] - contract AddSafe7579Contract { - struct ModuleInit { - address module; - bytes initData; - } - - function addSafe7579(address safe7579, ModuleInit[] calldata validators, ModuleInit[] calldata executors, ModuleInit[] calldata fallbacks, ModuleInit[] calldata hooks, address[] calldata attesters, uint8 threshold) external; - } - }; - let faucet = anvil_faucet(rpc_url).await; let wallet = EthereumWallet::new(faucet); let wallet_provider = ProviderBuilder::new() @@ -148,7 +129,7 @@ async fn test() { RHINESTONE_ATTESTER_ADDRESS, MOCK_ATTESTER_ADDRESS, ], - threshold: owners.threshold, + threshold: 1, } .abi_encode() .into(), diff --git a/crates/yttrium/src/execution/send/safe_test.rs b/crates/yttrium/src/execution/send/safe_test.rs index e7f2a905..a005cbf9 100644 --- a/crates/yttrium/src/execution/send/safe_test.rs +++ b/crates/yttrium/src/execution/send/safe_test.rs @@ -154,6 +154,7 @@ pub async fn send_transactions( } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct PreparedSendTransaction { pub safe_op: SafeOp, pub domain: Eip712Domain, diff --git a/crates/yttrium/src/gas_abstraction/mod.rs b/crates/yttrium/src/gas_abstraction/mod.rs index fa1c399d..3ac7d862 100644 --- a/crates/yttrium/src/gas_abstraction/mod.rs +++ b/crates/yttrium/src/gas_abstraction/mod.rs @@ -3,10 +3,15 @@ use { bundler::{ client::BundlerClient, config::BundlerConfig, + models::user_operation_receipt::UserOperationReceipt, pimlico::{self, paymaster::client::PaymasterClient}, }, + chain_abstraction::api::InitialTransaction, config::Config, entry_point::ENTRYPOINT_ADDRESS_V07, + erc7579::addresses::{ + MOCK_ATTESTER_ADDRESS, RHINESTONE_ATTESTER_ADDRESS, + }, execution::send::safe_test::{ encode_send_transactions, DoSendTransactionParams, OwnerSignature, PreparedSendTransaction, @@ -14,24 +19,28 @@ use { provider_pool::ProviderPool, smart_accounts::{ nonce::get_nonce, - safe::{get_call_data, user_operation_to_safe_op}, + safe::{ + get_call_data, user_operation_to_safe_op, AddSafe7579Contract, + Owners, SetupContract, SAFE_4337_MODULE_ADDRESS, + SAFE_ERC_7579_LAUNCHPAD_ADDRESS, SAFE_L2_SINGLETON_1_4_1, + }, }, + test_helpers::anvil_faucet, user_operation::UserOperationV07, }, alloy::{ - primitives::{aliases::U48, Address, Bytes, U256, U64}, - rpc::types::TransactionRequest, - sol_types::SolStruct, + network::{EthereumWallet, TransactionBuilder7702}, + primitives::{ + aliases::U48, Address, Bytes, PrimitiveSignature, B256, U256, U64, + }, + rpc::types::Authorization, + sol_types::{SolCall, SolStruct}, }, - alloy_provider::Provider, + alloy_provider::{Provider, ProviderBuilder}, relay_rpc::domain::ProjectId, }; -// #[cfg(test)] -// mod test_helpers; - #[cfg(test)] -#[cfg(feature = "test_blockchain_api")] mod tests; // docs: https://github.com/reown-com/reown-docs/pull/201 @@ -48,23 +57,23 @@ impl Client { #[cfg_attr(feature = "uniffi", uniffi::constructor)] // TODO remove `config` param pub fn new(project_id: ProjectId, config: Config) -> Self { - Self { provider_pool: ProviderPool::new(project_id), config } + Self { + provider_pool: ProviderPool::new(project_id, config.clone()), + config, + } } // Or you can make chain-specific clients if you want to do it manually // async fn new(chain_id: Caip10, rpc_url: Url, bundler_url: Url, paymaster_url: Url) -> Self {} - // Prepare and send gas-abstracted transactions - async fn prepare_gas_abstraction( + async fn prepare( &self, - transaction: Transaction, + transaction: InitialTransaction, ) -> Result { - let Transaction { chain_id, from, to, value, input } = transaction; + let InitialTransaction { chain_id, from, to, value, input } = + transaction; - let provider = self - .provider_pool - .get_provider(format!("eip155:{chain_id}",)) - .await; + let provider = self.provider_pool.get_provider(&chain_id).await; let code = provider.get_code_at(from).await.unwrap(); // TODO check if the code is our contract, or something else @@ -72,13 +81,29 @@ impl Client { // If our contract, return UserOp hash to sign // If something else then return an error - // let mut hashes = Vec::with_capacity(2); - if code.is_empty() { - // return 7702 txn - } + let auth = if code.is_empty() { + let auth = Authorization { + chain_id: chain_id + .strip_prefix("eip155:") + .unwrap() + .parse() + .unwrap(), + address: SAFE_L2_SINGLETON_1_4_1, + // TODO should this be `pending` tag? https://github.com/wevm/viem/blob/a49c100a0b2878fbfd9f1c9b43c5cc25de241754/src/experimental/eip7702/actions/signAuthorization.ts#L149 + nonce: provider.get_transaction_count(from).await.unwrap(), + }; + + Some(PreparedGasAbstractionAuthorization { + hash: auth.signature_hash(), + auth, + }) + } else { + None + }; // Else assume it's our account for now + // TODO with_key if has modules let nonce = get_nonce(&provider, from.into(), &ENTRYPOINT_ADDRESS_V07.into()) .await @@ -158,13 +183,20 @@ impl Client { let (safe_op, domain) = user_operation_to_safe_op( &user_op, ENTRYPOINT_ADDRESS_V07, - chain_id, + U64::from( + chain_id + .strip_prefix("eip155:") + .unwrap() + .parse::() + .unwrap(), + ), valid_after, valid_until, ); let hash = safe_op.eip712_signing_hash(&domain); Ok(PreparedGasAbstraction { + auth, prepared_send_transaction: PreparedSendTransaction { safe_op, domain, @@ -178,20 +210,101 @@ impl Client { }) } - async fn execute_transaction( + // TODO error type + // TODO check if receipt is actually OK, return error result if not (with the receipt still) + async fn send( &self, - signatures: Vec, + // FIXME can't pass Authorization through FFI + auth_sig: Option, + signature: PrimitiveSignature, params: DoSendTransactionParams, - ) { + ) -> UserOperationReceipt { + let account = params.user_op.sender; + // TODO put this all inside the UserOperation once it's available + + if let Some(SignedAuthorization { auth, signature }) = auth_sig { + let chain_id = auth.chain_id; + let auth = auth.into_signed(signature); + + let faucet = + anvil_faucet(&self.config.endpoints.rpc.base_url).await; + let wallet = EthereumWallet::new(faucet); + let wallet_provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_provider( + self.provider_pool + .get_provider(&format!("eip155:{chain_id}")) + .await, + ); + let owners = Owners { threshold: 1, owners: vec![account.into()] }; + assert!(SetupContract::new( + account.into(), + wallet_provider.clone() + ) + .setup( + owners.owners, + U256::from(owners.threshold), + SAFE_ERC_7579_LAUNCHPAD_ADDRESS, + AddSafe7579Contract::addSafe7579Call { + safe7579: SAFE_4337_MODULE_ADDRESS, + validators: vec![ + // AddSafe7579Contract::ModuleInit { + // module: ownable_validator.address, + // initData: ownable_validator.init_data, + // }, + // AddSafe7579Contract::ModuleInit { + // module: smart_sessions.address, + // initData: smart_sessions.init_data, + // }, + ], + executors: vec![], + fallbacks: vec![], + hooks: vec![], + attesters: vec![ + RHINESTONE_ATTESTER_ADDRESS, + MOCK_ATTESTER_ADDRESS, + ], + threshold: 1, + } + .abi_encode() + .into(), + SAFE_4337_MODULE_ADDRESS, + Address::ZERO, + U256::ZERO, + Address::ZERO, + ) + .map(|mut t| { + t.set_authorization_list(vec![auth]); + t + }) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap() + .status()); + } + let bundler_client = BundlerClient::new(BundlerConfig::new( self.config.endpoints.bundler.base_url.clone(), )); - let user_op = - encode_send_transactions(signatures, params).await.unwrap(); - let _user_operation_hash = bundler_client + let user_op = encode_send_transactions( + vec![OwnerSignature { + owner: params.user_op.sender.into(), + signature, + }], + params, + ) + .await + .unwrap(); + let hash = bundler_client .send_user_operation(ENTRYPOINT_ADDRESS_V07.into(), user_op.clone()) .await .unwrap(); + + bundler_client.wait_for_user_operation_receipt(hash).await.unwrap() } // Signature creation @@ -204,27 +317,22 @@ impl Client { #[derive(Clone)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -pub struct Transaction { - pub chain_id: U64, - pub from: Address, - pub to: Address, - pub value: U256, - pub input: Bytes, +pub struct PreparedGasAbstraction { + pub auth: Option, + pub prepared_send_transaction: PreparedSendTransaction, } #[derive(Clone)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Object))] -pub struct PreparedGasAbstraction { - // hashes_to_sign: Vec, - // fees: GasAbstractionFees, // similar to RouteUiFields -> fee and other UI info for user display - prepared_send_transaction: PreparedSendTransaction, +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct PreparedGasAbstractionAuthorization { + pub auth: Authorization, + pub hash: B256, } #[derive(Clone)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Object))] -pub struct Params { - /// EOA signatures by the transaction's sender of `hashes_to_sign` - signatures: Vec>, - params: DoSendTransactionParams, - initial_transaction: TransactionRequest, +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct SignedAuthorization { + // FIXME cannot pass this through FFI like this + pub auth: Authorization, + pub signature: PrimitiveSignature, } diff --git a/crates/yttrium/src/gas_abstraction/tests.rs b/crates/yttrium/src/gas_abstraction/tests.rs index 65670d17..38069b7d 100644 --- a/crates/yttrium/src/gas_abstraction/tests.rs +++ b/crates/yttrium/src/gas_abstraction/tests.rs @@ -1,45 +1,104 @@ use { crate::{ + chain_abstraction::api::InitialTransaction, config::Config, - gas_abstraction::{Client as GasAbstractionClient, Transaction}, + gas_abstraction::{ + Client as GasAbstractionClient, SignedAuthorization, + }, }, alloy::{ - primitives::{Bytes, U256, U64}, - signers::local::LocalSigner, + network::Ethereum, + primitives::{Bytes, U256}, + signers::{local::LocalSigner, SignerSync}, }, + alloy_provider::{Provider, ReqwestProvider}, }; #[tokio::test] -async fn prepares() { +async fn happy_path() { + // TODO remove + let config = Config::local(); + let provider = ReqwestProvider::::new_http( + config.endpoints.rpc.base_url.parse().unwrap(), + ); + let chain_id = format!("eip155:{}", provider.get_chain_id().await.unwrap()); + + // You have a GasAbstractionClient + let project_id = std::env::var("REOWN_PROJECT_ID").unwrap().into(); + let client = GasAbstractionClient::new(project_id, config); + // You have an EOA let eoa = LocalSigner::random(); - // You have an incomming eth_sendTransaction - let txn = Transaction { - chain_id: U64::from(1), // TODO - from: eoa.address(), - to: LocalSigner::random().address(), - value: U256::ZERO, - input: Bytes::new(), - }; + { + // You have an incomming eth_sendTransaction + let txn = InitialTransaction { + chain_id: chain_id.clone(), + from: eoa.address(), + to: LocalSigner::random().address(), + value: U256::ZERO, + input: Bytes::new(), + }; + let result = client.prepare(txn).await.unwrap(); - // You have a GasAbstractionClient - let project_id = std::env::var("REOWN_PROJECT_ID").unwrap().into(); - let client = GasAbstractionClient::new(project_id, Config::local()); - let _result = client.prepare_gas_abstraction(txn).await; + assert!(result.auth.is_some()); + + // Sign the authorization + let auth_sig = result.auth.map(|auth| SignedAuthorization { + signature: eoa.sign_hash_sync(&auth.auth.signature_hash()).unwrap(), + auth: auth.auth, + }); + + // Ask the user for approval and sign the UserOperation + let signature = eoa + .sign_typed_data_sync( + &result.prepared_send_transaction.safe_op, + &result.prepared_send_transaction.domain, + ) + .unwrap(); + + // Send the UserOperation and get the receipt + let receipt = client + .send( + auth_sig, + signature, + result.prepared_send_transaction.do_send_transaction_params, + ) + .await; + println!("receipt: {:?}", receipt); + } - // let PreparedGasAbstraction { - // hashes_to_sign, - // fields - // } = client.prepare_gas_abstraction(txn).await?; + { + // You have an incomming eth_sendTransaction + let txn = InitialTransaction { + chain_id, + from: eoa.address(), + to: LocalSigner::random().address(), + value: U256::ZERO, + input: Bytes::new(), + }; + let result = client.prepare(txn).await.unwrap(); - // ask_user_for_approval(fields).await?; + assert!(result.auth.is_none()); - // let signatures = hashes_to_sign.iter().map(|hash| account.sign(hash)).collect(); + // Ask the user for approval and sign the UserOperation + let signature = eoa + .sign_typed_data_sync( + &result.prepared_send_transaction.safe_op, + &result.prepared_send_transaction.domain, + ) + .unwrap(); - // let receipt = client.execute_transaction(txn, Params { - // signatures - // }).await?; + // Send the UserOperation and get the receipt + let receipt = client + .send( + None, + signature, + result.prepared_send_transaction.do_send_transaction_params, + ) + .await; + println!("receipt: {:?}", receipt); + } - // display_success(receipt) + println!("success"); } diff --git a/crates/yttrium/src/provider_pool.rs b/crates/yttrium/src/provider_pool.rs index 3491c5aa..1fbb8bc6 100644 --- a/crates/yttrium/src/provider_pool.rs +++ b/crates/yttrium/src/provider_pool.rs @@ -1,5 +1,8 @@ use { - crate::blockchain_api::{BLOCKCHAIN_API_URL, PROXY_ENDPOINT_PATH}, + crate::{ + blockchain_api::{BLOCKCHAIN_API_URL, PROXY_ENDPOINT_PATH}, + config::Config, + }, alloy::{ network::Ethereum, rpc::client::RpcClient, transports::http::Http, }, @@ -17,21 +20,24 @@ pub struct ProviderPool { pub providers: Arc>>, pub blockchain_api_base_url: Url, pub project_id: ProjectId, + pub config: Config, } impl ProviderPool { - pub fn new(project_id: ProjectId) -> Self { + // TODO remove `config` param + pub fn new(project_id: ProjectId, config: Config) -> Self { Self { client: ReqwestClient::new(), providers: Arc::new(RwLock::new(HashMap::new())), blockchain_api_base_url: BLOCKCHAIN_API_URL.parse().unwrap(), project_id, + config, } } - pub async fn get_provider(&self, chain_id: String) -> ReqwestProvider { + pub async fn get_provider(&self, chain_id: &str) -> ReqwestProvider { let providers = self.providers.read().await; - if let Some(provider) = providers.get(&chain_id) { + if let Some(provider) = providers.get(chain_id) { provider.clone() } else { std::mem::drop(providers); @@ -40,13 +46,16 @@ impl ProviderPool { let mut url = self.blockchain_api_base_url.join(PROXY_ENDPOINT_PATH).unwrap(); url.query_pairs_mut() - .append_pair("chainId", &chain_id) + .append_pair("chainId", chain_id) .append_pair("projectId", self.project_id.as_ref()); let provider = ReqwestProvider::::new(RpcClient::new( Http::with_client(self.client.clone(), url), false, )); - self.providers.write().await.insert(chain_id, provider.clone()); + self.providers + .write() + .await + .insert(chain_id.to_owned(), provider.clone()); provider } } diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs index 53560b23..9e226cf0 100644 --- a/crates/yttrium/src/smart_accounts/safe.rs +++ b/crates/yttrium/src/smart_accounts/safe.rs @@ -216,6 +216,19 @@ sol! { } } +sol! { + #[allow(clippy::too_many_arguments)] + #[sol(rpc)] + contract AddSafe7579Contract { + struct ModuleInit { + address module; + bytes initData; + } + + function addSafe7579(address safe7579, ModuleInit[] calldata validators, ModuleInit[] calldata executors, ModuleInit[] calldata fallbacks, ModuleInit[] calldata hooks, address[] calldata attesters, uint8 threshold) external; + } +} + // permissionless -> getInitializerCode fn get_initializer_code(owners: Owners) -> Bytes { // let ownable_validator = get_ownable_validator(&owners, None); diff --git a/crates/yttrium/src/uniffi_compat.rs b/crates/yttrium/src/uniffi_compat.rs index 14f9e89b..e49da993 100644 --- a/crates/yttrium/src/uniffi_compat.rs +++ b/crates/yttrium/src/uniffi_compat.rs @@ -7,8 +7,9 @@ use { dyn_abi::Eip712Domain, primitives::{ aliases::U48, Address, Bytes, PrimitiveSignature, Uint, B256, U128, - U256, U64, + U256, U64, U8, }, + rpc::types::Authorization, }, relay_rpc::domain::ProjectId, }; @@ -39,12 +40,22 @@ uniffi::custom_type!(Eip712Domain, String, { lower: |_obj| "Does not support lowering Eip712Domain".to_owned(), }); +uniffi::custom_type!(Authorization, String, { + try_lift: |_val| unimplemented!("Does not support lifting Authorization"), + lower: |_obj| "Does not support lowering Authorization".to_owned(), +}); + fn uint_to_hex( obj: Uint, ) -> String { format!("0x{obj:x}") } +uniffi::custom_type!(U8, String, { + try_lift: |val| Ok(val.parse()?), + lower: |obj| uint_to_hex(obj), +}); + uniffi::custom_type!(U48, String, { try_lift: |val| Ok(val.parse()?), lower: |obj| uint_to_hex(obj),