,
pub call_data: Bytes,
pub call_gas_limit: U256,
diff --git a/crates/yttrium/src/entry_point.rs b/crates/yttrium/src/entry_point.rs
index fd7f56c6..54326c09 100644
--- a/crates/yttrium/src/entry_point.rs
+++ b/crates/yttrium/src/entry_point.rs
@@ -40,6 +40,20 @@ pub const ENTRYPOINT_ADDRESS_V07: &str =
pub const ENTRYPOINT_V06_TYPE: &str = "v0.6";
pub const ENTRYPOINT_V07_TYPE: &str = "v0.7";
+sol! (
+ struct PackedUserOperation {
+ address sender;
+ uint256 nonce;
+ bytes initCode;
+ bytes callData;
+ bytes32 accountGasLimits;
+ uint256 preVerificationGas;
+ bytes32 gasFees;
+ bytes paymasterAndData;
+ bytes signature;
+ }
+);
+
sol!(
#[allow(missing_docs)]
#[sol(rpc)]
diff --git a/crates/yttrium/src/entry_point/get_sender_address.rs b/crates/yttrium/src/entry_point/get_sender_address.rs
index a33e3c4c..14d38aa0 100644
--- a/crates/yttrium/src/entry_point/get_sender_address.rs
+++ b/crates/yttrium/src/entry_point/get_sender_address.rs
@@ -60,6 +60,10 @@ where
let call_builder = instance.getSenderAddress(init_code);
+ // Note: you may need to static call getSenderAddress() not call() as per
+ // the spec. Leaving as-is for now.
+ // let call = call_builder.call_raw().await;
+
let call: Result<
crate::entry_point::EntryPoint::getSenderAddressReturn,
ContractError,
diff --git a/crates/yttrium/src/smart_accounts/nonce.rs b/crates/yttrium/src/smart_accounts/nonce.rs
index f32ee1d8..d0864688 100644
--- a/crates/yttrium/src/smart_accounts/nonce.rs
+++ b/crates/yttrium/src/smart_accounts/nonce.rs
@@ -1,4 +1,4 @@
-use alloy::primitives::U256;
+use alloy::primitives::aliases::U192;
pub async fn get_nonce(
provider: &P,
@@ -14,7 +14,7 @@ where
entry_point_address.to_address(),
provider,
);
- let key = U256::ZERO;
+ let key = U192::ZERO;
let get_nonce_call =
entry_point_instance.getNonce(address.to_address(), key).call().await?;
diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs
index 16ee34ed..f4fe1f19 100644
--- a/crates/yttrium/src/smart_accounts/safe.rs
+++ b/crates/yttrium/src/smart_accounts/safe.rs
@@ -1,8 +1,9 @@
use alloy::{
dyn_abi::DynSolValue,
primitives::{address, keccak256, Address, Bytes, Uint, U256},
+ providers::ReqwestProvider,
sol,
- sol_types::SolCall,
+ sol_types::{SolCall, SolValue},
};
sol!(
@@ -158,3 +159,36 @@ pub fn factory_data(
saltNonce: U256::ZERO,
}
}
+
+pub async fn get_account_address(
+ provider: ReqwestProvider,
+ owners: Owners,
+) -> Address {
+ let creation_code =
+ SafeProxyFactory::new(SAFE_PROXY_FACTORY_ADDRESS, provider.clone())
+ .proxyCreationCode()
+ .call()
+ .await
+ .unwrap()
+ ._0;
+ let initializer = get_initializer_code(owners.clone());
+ let deployment_code = DynSolValue::Tuple(vec![
+ DynSolValue::Bytes(creation_code.to_vec()),
+ DynSolValue::FixedBytes(
+ SAFE_ERC_7579_LAUNCHPAD_ADDRESS.into_word(),
+ 32,
+ ),
+ ])
+ .abi_encode_packed();
+ let salt = keccak256(
+ DynSolValue::Tuple(vec![
+ DynSolValue::FixedBytes(
+ keccak256(initializer.abi_encode_packed()),
+ 32,
+ ),
+ DynSolValue::Uint(Uint::from(0), 256),
+ ])
+ .abi_encode_packed(),
+ );
+ SAFE_PROXY_FACTORY_ADDRESS.create2(salt, keccak256(deployment_code))
+}
diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs
index 4bfc9ae4..b5090fa0 100644
--- a/crates/yttrium/src/transaction/send/safe_test.rs
+++ b/crates/yttrium/src/transaction/send/safe_test.rs
@@ -45,33 +45,35 @@ mod tests {
paymaster::client::PaymasterClient,
},
},
- entry_point::get_sender_address::get_sender_address_v07,
+ config::Config,
smart_accounts::{
nonce::get_nonce,
safe::{
- factory_data, Execution, Owners, Safe7579, Safe7579Launchpad,
- SAFE_4337_MODULE_ADDRESS, SAFE_ERC_7579_LAUNCHPAD_ADDRESS,
- SAFE_PROXY_FACTORY_ADDRESS,
+ factory_data, get_account_address, Execution, Owners, Safe7579,
+ Safe7579Launchpad, SAFE_4337_MODULE_ADDRESS,
+ SAFE_ERC_7579_LAUNCHPAD_ADDRESS, SAFE_PROXY_FACTORY_ADDRESS,
SEPOLIA_SAFE_ERC_7579_SINGLETON_ADDRESS,
},
simple_account::{factory::FactoryAddress, SimpleAccountAddress},
},
- transaction::Transaction,
user_operation::UserOperationV07,
};
use alloy::{
dyn_abi::{DynSolValue, Eip712Domain},
- network::EthereumWallet,
- primitives::{Address, Bytes, FixedBytes, Uint, U128, U256},
- providers::ProviderBuilder,
- signers::{local::LocalSigner, SignerSync},
+ network::Ethereum,
+ primitives::{
+ aliases::U48, Address, Bytes, FixedBytes, Uint, U128, U256,
+ },
+ providers::{ext::AnvilApi, Provider, ReqwestProvider},
+ signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync},
sol,
sol_types::{SolCall, SolValue},
};
- use std::{ops::Not, str::FromStr, time::Duration};
+ use std::{ops::Not, str::FromStr};
async fn send_transaction(
- transaction: Transaction,
+ execution_calldata: Vec,
+ owner: LocalSigner,
) -> eyre::Result {
let config = crate::config::Config::local();
@@ -94,71 +96,26 @@ mod tests {
let rpc_url = config.endpoints.rpc.base_url;
- // Create a provider
-
- let alloy_signer = LocalSigner::random();
- let ethereum_wallet = EthereumWallet::new(alloy_signer.clone());
-
let rpc_url: reqwest::Url = rpc_url.parse()?;
- let provider = ProviderBuilder::new()
- .with_recommended_fillers()
- .wallet(ethereum_wallet.clone())
- .on_http(rpc_url);
+ let provider = ReqwestProvider::::new_http(rpc_url);
let safe_factory_address_primitives: Address =
SAFE_PROXY_FACTORY_ADDRESS;
let safe_factory_address =
FactoryAddress::new(safe_factory_address_primitives);
- let owner = ethereum_wallet.clone().default_signer();
- let owner_address = owner.address();
- let owners = Owners { owners: vec![owner_address], threshold: 1 };
+ let owners = Owners { owners: vec![owner.address()], threshold: 1 };
let factory_data_value = factory_data(owners.clone()).abi_encode();
- let factory_data_value_hex = hex::encode(factory_data_value.clone());
+ let sender_address =
+ get_account_address(provider.clone(), owners.clone()).await;
- 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,
- safe_factory_address.into(),
- factory_data_value.clone().into(),
- entry_point_address,
- )
- .await?;
-
- println!("Calculated sender address: {:?}", sender_address);
-
- let to: Address = transaction.to.parse()?;
- let value: alloy::primitives::Uint<256, 4> =
- transaction.value.parse()?;
- let data_hex = transaction.data.strip_prefix("0x").unwrap();
- let data: Bytes = Bytes::from_str(data_hex)?;
-
- // let execution_calldata =
- // vec![Execution { target: to, value, callData: data }];
- // let execution_calldata =
- // Execution { target: to, value, callData: data };
- let execution_calldata =
- [to.to_vec(), value.to_be_bytes_vec(), data.to_vec()]
- .concat()
- .into();
-
- // let call_type = if execution_calldata.len() > 1 {
- // CallType::BatchCall
- // } else {
- // CallType::Call
- // };
- let call_type = CallType::Call;
+ let call_type = if execution_calldata.len() > 1 {
+ CallType::BatchCall
+ } else {
+ CallType::Call
+ };
let revert_on_error = false;
let selector = [0u8; 4];
let context = [0u8; 22];
@@ -179,30 +136,23 @@ mod tests {
}
}
- let mut mode = Vec::with_capacity(32);
- mode.push(call_type.as_byte());
- mode.push(if revert_on_error { 0x01 } else { 0x00 });
- mode.extend_from_slice(&[0u8; 4]);
- mode.extend_from_slice(&selector);
- mode.extend_from_slice(&context);
- let mode = FixedBytes::from_slice(&mode);
-
- // let mode = DynSolValue::Tuple(vec![
- // DynSolValue::Uint(Uint::from(call_type.as_byte()), 8),
- // DynSolValue::Uint(Uint::from(revert_on_error as u8), 8),
- // DynSolValue::Bytes(selector.to_vec().into()),
- // DynSolValue::Bytes(context.to_vec().into()),
- // ]).abi_encode_packed().into();
+ let mode = DynSolValue::Tuple(vec![
+ DynSolValue::Uint(Uint::from(call_type.as_byte()), 8),
+ DynSolValue::Uint(Uint::from(revert_on_error as u8), 8),
+ DynSolValue::Bytes(vec![0u8; 4]),
+ DynSolValue::Bytes(selector.to_vec()),
+ DynSolValue::Bytes(context.to_vec()),
+ ])
+ .abi_encode_packed();
let call_data = Safe7579::executeCall {
- mode,
- // executionCalldata: execution_calldata.abi_encode().into(),
- executionCalldata: execution_calldata,
+ mode: FixedBytes::from_slice(&mode),
+ executionCalldata: execution_calldata.abi_encode_packed().into(),
}
.abi_encode()
.into();
- let deployed = false;
+ let deployed = provider.get_code_at(sender_address).await?.len() > 0;
// permissionless: signerToSafeSmartAccount -> encodeCallData
let call_data = if deployed {
call_data
@@ -232,15 +182,11 @@ mod tests {
.into()
};
- // 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),
@@ -282,8 +228,6 @@ mod tests {
)
.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();
@@ -301,12 +245,8 @@ mod tests {
op
};
- println!("Received paymaster sponsor result: {:?}", sponsored_user_op);
-
- // Sign the UserOperation
-
- let valid_after = 0;
- let valid_until = 0;
+ let valid_after = U48::from(0);
+ let valid_until = U48::from(0);
sol!(
struct SafeOp {
@@ -405,11 +345,11 @@ mod tests {
let verifying_contract = if erc7579_launchpad_address && !deployed {
sponsored_user_op.sender
} else {
- SAFE_ERC_7579_LAUNCHPAD_ADDRESS
+ SAFE_4337_MODULE_ADDRESS
};
// TODO loop per-owner
- let signature = alloy_signer.sign_typed_data_sync(
+ let signature = owner.sign_typed_data_sync(
&message,
&Eip712Domain {
chain_id: Some(Uint::from(chain_id)),
@@ -456,17 +396,74 @@ mod tests {
tx_hash
);
+ // Some extra calls to wait for/get the actual transaction. But these
+ // aren't required since eth_getUserOperationReceipt already waits
+ // let tx_hash = FixedBytes::from_slice(
+ // &hex::decode(tx_hash.strip_prefix("0x").unwrap()).unwrap(),
+ // );
+ // let pending_txn = provider
+ // .watch_pending_transaction(PendingTransactionConfig::new(tx_hash))
+ // .await?;
+ // pending_txn.await.unwrap();
+ // let transaction = provider.get_transaction_by_hash(tx_hash).await?;
+ // println!("Transaction included: {:?}", transaction);
+ // let transaction_receipt =
+ // provider.get_transaction_receipt(tx_hash).await?;
+ // println!("Transaction receipt: {:?}", transaction_receipt);
+
Ok(user_operation_hash)
}
#[tokio::test]
async fn test_send_transaction() -> eyre::Result<()> {
- let transaction = Transaction::mock();
+ let rpc_url = Config::local().endpoints.rpc.base_url;
+ let rpc_url: reqwest::Url = rpc_url.parse()?;
+ let provider = ReqwestProvider::::new_http(rpc_url);
+
+ let destination = LocalSigner::random();
+ let balance = provider.get_balance(destination.address()).await?;
+ assert_eq!(balance, Uint::from(0));
+
+ let owner = LocalSigner::random();
+ let sender_address = get_account_address(
+ provider.clone(),
+ Owners { owners: vec![owner.address()], threshold: 1 },
+ )
+ .await;
- let transaction_hash = send_transaction(transaction).await?;
+ provider.anvil_set_balance(sender_address, U256::from(100)).await?;
+ let transaction = vec![Execution {
+ target: destination.address(),
+ value: Uint::from(1),
+ callData: Bytes::new(),
+ }];
+
+ let transaction_hash =
+ send_transaction(transaction, owner.clone()).await?;
+
+ println!("Transaction sent: {}", transaction_hash);
+
+ let balance = provider.get_balance(destination.address()).await?;
+ assert_eq!(balance, Uint::from(1));
+
+ provider.anvil_set_balance(sender_address, U256::from(100)).await?;
+ let transaction = vec![Execution {
+ target: destination.address(),
+ value: Uint::from(1),
+ callData: Bytes::new(),
+ }];
+
+ let transaction_hash = send_transaction(transaction, owner).await?;
println!("Transaction sent: {}", transaction_hash);
+ let balance = provider.get_balance(destination.address()).await?;
+ assert_eq!(balance, Uint::from(2));
+
Ok(())
}
+
+ // TODO test/fix: if invalid call data (e.g. sending balance that you don't
+ // have), the account will still be deployed but the transfer won't happen.
+ // Why can't we detect this?
}
diff --git a/crates/yttrium/src/transaction/send/simple_account_test.rs b/crates/yttrium/src/transaction/send/simple_account_test.rs
index bbaa7e74..2a595be3 100644
--- a/crates/yttrium/src/transaction/send/simple_account_test.rs
+++ b/crates/yttrium/src/transaction/send/simple_account_test.rs
@@ -63,7 +63,7 @@ mod tests {
providers::ProviderBuilder,
signers::local::LocalSigner,
};
- use std::{str::FromStr, time::Duration};
+ use std::str::FromStr;
async fn send_transaction(
transaction: Transaction,