diff --git a/Cargo.lock b/Cargo.lock index 337c5f061a59..422ebd05ae4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2981,10 +2981,12 @@ dependencies = [ "foundry-compilers", "foundry-config", "foundry-evm-core", + "foundry-wallets", "itertools 0.11.0", "jsonpath_lib", "k256", "p256", + "parking_lot", "revm", "serde_json", "thiserror", @@ -3183,7 +3185,6 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-rpc-types", - "alloy-signer", "alloy-sol-types", "const-hex", "eyre", @@ -3360,6 +3361,7 @@ dependencies = [ "rusoto_kms", "serde", "thiserror", + "tokio", "tracing", ] diff --git a/crates/cast/bin/cmd/send.rs b/crates/cast/bin/cmd/send.rs index b79bf1d7cd9b..b68ba5c7566d 100644 --- a/crates/cast/bin/cmd/send.rs +++ b/crates/cast/bin/cmd/send.rs @@ -1,7 +1,7 @@ use cast::{Cast, TxBuilder}; use clap::Parser; use ethers_core::types::NameOrAddress; -use ethers_middleware::MiddlewareBuilder; +use ethers_middleware::SignerMiddleware; use ethers_providers::Middleware; use ethers_signers::Signer; use eyre::Result; @@ -170,7 +170,7 @@ impl SendTxArgs { // enough information to sign and we must bail. } else { // Retrieve the signer, and bail if it can't be constructed. - let signer = eth.wallet.signer(chain.id()).await?; + let signer = eth.wallet.signer().await?; let from = signer.address(); // prevent misconfigured hwlib from sending a transaction that defies @@ -191,7 +191,7 @@ corresponds to the sender, or let foundry automatically detect it by not specify tx.nonce = Some(provider.get_transaction_count(from, None).await?.to_alloy()); } - let provider = provider.with_signer(signer); + let provider = SignerMiddleware::new_with_provider_chain(provider, signer).await?; cast_send( provider, diff --git a/crates/cast/bin/cmd/wallet/mod.rs b/crates/cast/bin/cmd/wallet/mod.rs index cbbd3e8d9fb9..5850c523a368 100644 --- a/crates/cast/bin/cmd/wallet/mod.rs +++ b/crates/cast/bin/cmd/wallet/mod.rs @@ -9,7 +9,7 @@ use ethers_signers::Signer; use eyre::{Context, Result}; use foundry_common::{fs, types::ToAlloy}; use foundry_config::Config; -use foundry_wallets::{RawWallet, Wallet}; +use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner}; use rand::thread_rng; use serde_json::json; use std::{path::Path, str::FromStr}; @@ -72,7 +72,7 @@ pub enum WalletSubcommands { private_key_override: Option, #[clap(flatten)] - wallet: Wallet, + wallet: WalletOpts, }, /// Sign a message or typed data. @@ -106,7 +106,7 @@ pub enum WalletSubcommands { no_hash: bool, #[clap(flatten)] - wallet: Wallet, + wallet: WalletOpts, }, /// Verify the signature of a message. @@ -133,7 +133,7 @@ pub enum WalletSubcommands { #[clap(long, short)] keystore_dir: Option, #[clap(flatten)] - raw_wallet_options: RawWallet, + raw_wallet_options: RawWalletOpts, }, /// List all the accounts in the keystore default directory #[clap(visible_alias = "ls")] @@ -241,18 +241,18 @@ impl WalletSubcommands { } WalletSubcommands::Address { wallet, private_key_override } => { let wallet = private_key_override - .map(|pk| Wallet { - raw: RawWallet { private_key: Some(pk), ..Default::default() }, + .map(|pk| WalletOpts { + raw: RawWalletOpts { private_key: Some(pk), ..Default::default() }, ..Default::default() }) .unwrap_or(wallet) - .signer(0) + .signer() .await?; let addr = wallet.address(); println!("{}", addr.to_alloy().to_checksum(None)); } WalletSubcommands::Sign { message, data, from_file, no_hash, wallet } => { - let wallet = wallet.signer(0).await?; + let wallet = wallet.signer().await?; let sig = if data { let typed_data: TypedData = if from_file { // data is a file name, read json from file @@ -297,16 +297,21 @@ impl WalletSubcommands { } // get wallet - let wallet: Wallet = raw_wallet_options.into(); - let wallet = wallet.try_resolve_local_wallet()?.ok_or_else(|| { - eyre::eyre!( - "\ + let wallet = raw_wallet_options + .signer()? + .and_then(|s| match s { + WalletSigner::Local(s) => Some(s), + _ => None, + }) + .ok_or_else(|| { + eyre::eyre!( + "\ Did you set a private key or mnemonic? Run `cast wallet import --help` and use the corresponding CLI flag to set your key via: --private-key, --mnemonic-path or --interactive." - ) - })?; + ) + })?; let private_key = wallet.signer().to_bytes(); let password = rpassword::prompt_password("Enter password: ")?; diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 4c2e69a36483..00cfaf7c39cf 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -17,6 +17,7 @@ foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true foundry-evm-core.workspace = true +foundry-wallets.workspace = true alloy-dyn-abi.workspace = true alloy-json-abi.workspace = true @@ -26,6 +27,7 @@ alloy-sol-types.workspace = true alloy-providers.workspace = true alloy-rpc-types.workspace = true alloy-signer = { workspace = true, features = ["mnemonic", "keystore"] } +parking_lot = "0.12" eyre.workspace = true diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 85b2dcaab61f..1fdb9d4abc80 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -1,5 +1,5 @@ use super::Result; -use crate::Vm::Rpc; +use crate::{script::ScriptWallets, Vm::Rpc}; use alloy_primitives::Address; use foundry_common::fs::normalize_path; use foundry_compilers::{utils::canonicalize, ProjectPathsConfig}; @@ -36,11 +36,13 @@ pub struct CheatsConfig { pub evm_opts: EvmOpts, /// Address labels from config pub labels: HashMap, + /// Script wallets + pub script_wallets: Option, } impl CheatsConfig { /// Extracts the necessary settings from the Config - pub fn new(config: &Config, evm_opts: EvmOpts) -> Self { + pub fn new(config: &Config, evm_opts: EvmOpts, script_wallets: Option) -> Self { let mut allowed_paths = vec![config.__root.0.clone()]; allowed_paths.extend(config.libs.clone()); allowed_paths.extend(config.allow_paths.clone()); @@ -58,6 +60,7 @@ impl CheatsConfig { allowed_paths, evm_opts, labels: config.labels.clone(), + script_wallets, } } @@ -172,6 +175,7 @@ impl Default for CheatsConfig { allowed_paths: vec![], evm_opts: Default::default(), labels: Default::default(), + script_wallets: None, } } } @@ -185,6 +189,7 @@ mod tests { CheatsConfig::new( &Config { __root: PathBuf::from(root).into(), fs_permissions, ..Default::default() }, Default::default(), + None, ) } diff --git a/crates/cheatcodes/src/error.rs b/crates/cheatcodes/src/error.rs index 19475982f3ed..66796026d567 100644 --- a/crates/cheatcodes/src/error.rs +++ b/crates/cheatcodes/src/error.rs @@ -5,6 +5,7 @@ use alloy_sol_types::SolError; use foundry_common::errors::FsPathError; use foundry_config::UnresolvedEnvVarError; use foundry_evm_core::backend::DatabaseError; +use foundry_wallets::error::WalletSignerError; use k256::ecdsa::signature::Error as SignatureError; use std::{borrow::Cow, fmt}; @@ -298,6 +299,7 @@ impl_from!( UnresolvedEnvVarError, WalletError, SignerError, + WalletSignerError, ); #[cfg(test)] diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 7341bb6b18db..50b5093ba692 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -7,7 +7,7 @@ use crate::{ prank::Prank, DealRecord, RecordAccess, }, - script::Broadcast, + script::{Broadcast, ScriptWallets}, test::expect::{ self, ExpectedCallData, ExpectedCallTracker, ExpectedCallType, ExpectedEmit, ExpectedRevert, ExpectedRevertKind, @@ -16,7 +16,6 @@ use crate::{ }; use alloy_primitives::{Address, Bytes, B256, U256, U64}; use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; -use alloy_signer::LocalWallet; use alloy_sol_types::{SolInterface, SolValue}; use foundry_common::{evm::Breakpoints, provider::alloy::RpcUrl}; use foundry_evm_core::{ @@ -127,7 +126,7 @@ pub struct Cheatcodes { pub labels: HashMap, /// Remembered private keys - pub script_wallets: Vec, + pub script_wallets: Option, /// Prank information pub prank: Option, @@ -218,7 +217,8 @@ impl Cheatcodes { #[inline] pub fn new(config: Arc) -> Self { let labels = config.labels.clone(); - Self { config, fs_commit: true, labels, ..Default::default() } + let script_wallets = config.script_wallets.clone(); + Self { config, fs_commit: true, labels, script_wallets, ..Default::default() } } fn apply_cheatcode( diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs index caaef1d8fee4..86cfb0e5d00b 100644 --- a/crates/cheatcodes/src/lib.rs +++ b/crates/cheatcodes/src/lib.rs @@ -36,6 +36,7 @@ mod string; mod test; mod utils; +pub use script::ScriptWallets; pub use test::expect::ExpectedCallTracker; /// Cheatcode implementation. diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 820d3ffd9a18..62a751837257 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -2,8 +2,11 @@ use crate::{Cheatcode, CheatsCtxt, DatabaseExt, Result, Vm::*}; use alloy_primitives::{Address, U256}; -use alloy_signer::Signer; +use alloy_signer::{LocalWallet, Signer}; use foundry_config::Config; +use foundry_wallets::{multi_wallet::MultiWallet, WalletSigner}; +use parking_lot::Mutex; +use std::sync::Arc; impl Cheatcode for broadcast_0Call { fn apply_full(&self, ccx: &mut CheatsCtxt) -> Result { @@ -72,6 +75,44 @@ pub struct Broadcast { pub single_call: bool, } +/// Contains context for wallet management. +#[derive(Debug)] +pub struct ScriptWalletsInner { + /// All signers in scope of the script. + pub multi_wallet: MultiWallet, + /// Optional signer provided as `--sender` flag. + pub provided_sender: Option
, +} + +/// Clonable wrapper around [ScriptWalletsInner]. +#[derive(Debug, Clone)] +pub struct ScriptWallets { + /// Inner data. + pub inner: Arc>, +} + +impl ScriptWallets { + #[allow(missing_docs)] + pub fn new(multi_wallet: MultiWallet, provided_sender: Option
) -> Self { + Self { inner: Arc::new(Mutex::new(ScriptWalletsInner { multi_wallet, provided_sender })) } + } + + /// Consumes [ScriptWallets] and returns [MultiWallet]. + /// + /// Panics if [ScriptWallets] is still in use. + pub fn into_multi_wallet(self) -> MultiWallet { + Arc::into_inner(self.inner) + .map(|m| m.into_inner().multi_wallet) + .unwrap_or_else(|| panic!("not all instances were dropped")) + } + + /// Locks inner Mutex and adds a signer to the [MultiWallet]. + pub fn add_signer(&self, private_key: impl AsRef<[u8]>) -> Result { + self.inner.lock().multi_wallet.add_signer(WalletSigner::from_private_key(private_key)?); + Ok(Default::default()) + } +} + /// Sets up broadcasting from a script using `new_origin` as the sender. fn broadcast( ccx: &mut CheatsCtxt, @@ -86,8 +127,25 @@ fn broadcast( correct_sender_nonce(ccx)?; + let mut new_origin = new_origin.cloned(); + + if new_origin.is_none() { + if let Some(script_wallets) = &ccx.state.script_wallets { + let mut script_wallets = script_wallets.inner.lock(); + if let Some(provided_sender) = script_wallets.provided_sender { + new_origin = Some(provided_sender); + } else { + let signers = script_wallets.multi_wallet.signers()?; + if signers.len() == 1 { + let address = signers.keys().next().unwrap(); + new_origin = Some(*address); + } + } + } + } + let broadcast = Broadcast { - new_origin: *new_origin.unwrap_or(&ccx.data.env.tx.caller), + new_origin: new_origin.unwrap_or(ccx.data.env.tx.caller), original_caller: ccx.caller, original_origin: ccx.data.env.tx.caller, depth: ccx.data.journaled_state.depth(), @@ -106,13 +164,15 @@ fn broadcast_key( private_key: &U256, single_call: bool, ) -> Result { - let mut wallet = super::utils::parse_wallet(private_key)?; - wallet.set_chain_id(Some(ccx.data.env.cfg.chain_id)); - let new_origin = &wallet.address(); + let key = super::utils::parse_private_key(private_key)?; + let new_origin = LocalWallet::from(key.clone()).address(); + + let result = broadcast(ccx, Some(&new_origin), single_call); - let result = broadcast(ccx, Some(new_origin), single_call); if result.is_ok() { - ccx.state.script_wallets.push(wallet); + if let Some(script_wallets) = &ccx.state.script_wallets { + script_wallets.add_signer(key.to_bytes())?; + } } result } diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs index 9cfeee276b59..525120d7de8f 100644 --- a/crates/cheatcodes/src/utils.rs +++ b/crates/cheatcodes/src/utils.rs @@ -87,9 +87,11 @@ impl Cheatcode for deriveKey_3Call { impl Cheatcode for rememberKeyCall { fn apply_full(&self, ccx: &mut CheatsCtxt) -> Result { let Self { privateKey } = self; - let wallet = parse_wallet(privateKey)?.with_chain_id(Some(ccx.data.env.cfg.chain_id)); - let address = wallet.address(); - ccx.state.script_wallets.push(wallet); + let key = parse_private_key(privateKey)?; + let address = LocalWallet::from(key.clone()).address(); + if let Some(script_wallets) = &ccx.state.script_wallets { + script_wallets.add_signer(key.to_bytes())?; + } Ok(address.abi_encode()) } } diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index 1a425d3f2e8f..78b30892a8d9 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -305,8 +305,12 @@ impl SessionSource { let executor = ExecutorBuilder::new() .inspectors(|stack| { stack.chisel_state(final_pc).trace(true).cheatcodes( - CheatsConfig::new(&self.config.foundry_config, self.config.evm_opts.clone()) - .into(), + CheatsConfig::new( + &self.config.foundry_config, + self.config.evm_opts.clone(), + None, + ) + .into(), ) }) .gas_limit(self.config.evm_opts.gas_limit()) diff --git a/crates/cli/src/opts/ethereum.rs b/crates/cli/src/opts/ethereum.rs index 9a1d8653bf47..62cebeaec73f 100644 --- a/crates/cli/src/opts/ethereum.rs +++ b/crates/cli/src/opts/ethereum.rs @@ -9,7 +9,7 @@ use foundry_config::{ }, impl_figment_convert_cast, Chain, Config, }; -use foundry_wallets::{Wallet, WalletSigner}; +use foundry_wallets::WalletOpts; use serde::Serialize; use std::borrow::Cow; @@ -150,17 +150,11 @@ pub struct EthereumOpts { pub etherscan: EtherscanOpts, #[clap(flatten)] - pub wallet: Wallet, + pub wallet: WalletOpts, } impl_figment_convert_cast!(EthereumOpts); -impl EthereumOpts { - pub async fn signer(&self) -> Result { - self.wallet.signer(self.etherscan.chain.unwrap_or_default().id()).await - } -} - // Make this args a `Figment` so that it can be merged into the `Config` impl figment::Provider for EthereumOpts { fn metadata(&self) -> Metadata { diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 9ea7dd0fa51e..33e491e113d7 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -25,7 +25,6 @@ alloy-json-abi.workspace = true alloy-primitives = { workspace = true, features = ["serde", "getrandom", "arbitrary", "rlp"] } alloy-sol-types.workspace = true alloy-rpc-types.workspace = true -alloy-signer.workspace = true hashbrown = { version = "0.14", features = ["serde"] } revm = { workspace = true, default-features = false, features = [ "std", diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 76d00b80b2b7..628bc502ebb6 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -12,7 +12,6 @@ use crate::inspectors::{ use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::Function; use alloy_primitives::{Address, Bytes, Log, U256}; -use alloy_signer::LocalWallet; use foundry_common::{abi::IntoFunction, evm::Breakpoints}; use foundry_evm_core::{ backend::{Backend, DatabaseError, DatabaseExt, DatabaseResult, FuzzBackendWrapper}, @@ -204,7 +203,6 @@ impl Executor { labels: res.labels, state_changeset: None, transactions: None, - script_wallets: res.script_wallets, }))) } } @@ -372,7 +370,6 @@ impl Executor { labels, traces, debug, - script_wallets, env, coverage, .. @@ -400,7 +397,6 @@ impl Executor { labels, state_changeset: None, transactions: None, - script_wallets }))); } } @@ -418,7 +414,6 @@ impl Executor { labels, state_changeset: None, transactions: None, - script_wallets, }))) } }; @@ -595,7 +590,6 @@ pub struct ExecutionErr { pub labels: HashMap, pub transactions: Option, pub state_changeset: Option, - pub script_wallets: Vec, } #[derive(Debug, thiserror::Error)] @@ -666,8 +660,6 @@ pub struct CallResult { /// This is only present if the changed state was not committed to the database (i.e. if you /// used `call` and `call_raw` not `call_committing` or `call_raw_committing`). pub state_changeset: Option, - /// The wallets added during the call using the `rememberKey` cheatcode - pub script_wallets: Vec, /// The `revm::Env` after the call pub env: Env, /// breakpoints @@ -711,8 +703,6 @@ pub struct RawCallResult { /// This is only present if the changed state was not committed to the database (i.e. if you /// used `call` and `call_raw` not `call_committing` or `call_raw_committing`). pub state_changeset: Option, - /// The wallets added during the call using the `rememberKey` cheatcode - pub script_wallets: Vec, /// The `revm::Env` after the call pub env: Env, /// The cheatcode states after execution @@ -740,7 +730,6 @@ impl Default for RawCallResult { debug: None, transactions: None, state_changeset: None, - script_wallets: Vec::new(), env: Default::default(), cheatcodes: Default::default(), out: None, @@ -782,16 +771,8 @@ fn convert_executed_result( _ => Bytes::new(), }; - let InspectorData { - logs, - labels, - traces, - coverage, - debug, - cheatcodes, - script_wallets, - chisel_state, - } = inspector.collect(); + let InspectorData { logs, labels, traces, coverage, debug, cheatcodes, chisel_state } = + inspector.collect(); let transactions = match cheatcodes.as_ref() { Some(cheats) if !cheats.broadcastable_transactions.is_empty() => { @@ -815,7 +796,6 @@ fn convert_executed_result( debug, transactions, state_changeset: Some(state_changeset), - script_wallets, env, cheatcodes, out, @@ -842,7 +822,6 @@ fn convert_call_result( debug, transactions, state_changeset, - script_wallets, env, .. } = call_result; @@ -875,7 +854,6 @@ fn convert_call_result( debug, transactions, state_changeset, - script_wallets, env, breakpoints, skipped: false, @@ -898,7 +876,6 @@ fn convert_call_result( labels, transactions, state_changeset, - script_wallets, }))) } } diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index 6ece0f2da4ff..a1fc13d70619 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -3,7 +3,6 @@ use super::{ StackSnapshotType, TracePrinter, TracingInspector, TracingInspectorConfig, }; use alloy_primitives::{Address, Bytes, Log, B256, U256}; -use alloy_signer::LocalWallet; use foundry_evm_core::{backend::DatabaseExt, debug::DebugArena}; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::CallTraceArena; @@ -189,7 +188,6 @@ pub struct InspectorData { pub debug: Option, pub coverage: Option, pub cheatcodes: Option, - pub script_wallets: Vec, pub chisel_state: Option<(Stack, Vec, InstructionResult)>, } @@ -316,12 +314,6 @@ impl InspectorStack { traces: self.tracer.map(|tracer| tracer.get_traces().clone()), debug: self.debugger.map(|debugger| debugger.arena), coverage: self.coverage.map(|coverage| coverage.maps), - #[allow(clippy::useless_asref)] // https://github.com/rust-lang/rust-clippy/issues/12135 - script_wallets: self - .cheatcodes - .as_ref() - .map(|cheatcodes| cheatcodes.script_wallets.clone()) - .unwrap_or_default(), cheatcodes: self.cheatcodes, chisel_state: self.chisel_state.and_then(|state| state.state), } diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 7b674138a3b9..9d2bef5e0edf 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -303,7 +303,7 @@ impl CoverageArgs { .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) - .with_cheats_config(CheatsConfig::new(&config, evm_opts.clone())) + .with_cheats_config(CheatsConfig::new(&config, evm_opts.clone(), None)) .with_test_options(TestOptions { fuzz: config.fuzz, invariant: config.invariant, diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 77deb356ac25..8c4752603bdc 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -11,7 +11,7 @@ use ethers_core::{ TransactionReceipt, TransactionRequest, }, }; -use ethers_middleware::MiddlewareBuilder; +use ethers_middleware::SignerMiddleware; use ethers_providers::Middleware; use eyre::{Context, Result}; use foundry_cli::{ @@ -145,8 +145,8 @@ impl CreateArgs { self.deploy(abi, bin, params, provider, chain_id).await } else { // Deploy with signer - let signer = self.eth.wallet.signer(chain_id).await?; - let provider = provider.with_signer(signer); + let signer = self.eth.wallet.signer().await?; + let provider = SignerMiddleware::new_with_provider_chain(provider, signer).await?; self.deploy(abi, bin, params, provider, chain_id).await } } diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 49717cc9724e..a7ab056332ad 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -6,7 +6,7 @@ use super::{ use alloy_primitives::{utils::format_units, Address, TxHash, U256}; use ethers_core::types::transaction::eip2718::TypedTransaction; use ethers_providers::{JsonRpcClient, Middleware, Provider}; -use ethers_signers::{LocalWallet, Signer}; +use ethers_signers::Signer; use eyre::{bail, Context, ContextCompat, Result}; use forge::{inspectors::cheatcodes::BroadcastableTransactions, traces::CallTraceDecoder}; use foundry_cli::{ @@ -38,7 +38,7 @@ impl ScriptArgs { &self, deployment_sequence: &mut ScriptSequence, fork_url: &str, - script_wallets: &[LocalWallet], + signers: &HashMap, ) -> Result<()> { let provider = Arc::new(try_get_http_provider(fork_url)?); let already_broadcasted = deployment_sequence.receipts.len(); @@ -64,12 +64,34 @@ impl ScriptArgs { ); (SendTransactionsKind::Unlocked(senders), chain.as_u64()) } else { - let local_wallets = self - .wallets - .find_all(provider.clone(), required_addresses, script_wallets) - .await?; - let chain = local_wallets.values().last().wrap_err("Error accessing local wallet when trying to send onchain transaction, did you set a private key, mnemonic or keystore?")?.chain_id(); - (SendTransactionsKind::Raw(local_wallets), chain) + let mut missing_addresses = Vec::new(); + + println!("\n###\nFinding wallets for all the necessary addresses..."); + for addr in &required_addresses { + if !signers.contains_key(addr) { + missing_addresses.push(addr); + } + } + + if !missing_addresses.is_empty() { + let mut error_msg = String::new(); + + // This is an actual used address + if required_addresses.contains(&Config::DEFAULT_SENDER) { + error_msg += "\nYou seem to be using Foundry's default sender. Be sure to set your own --sender.\n"; + } + + eyre::bail!( + "{}No associated wallet for addresses: {:?}. Unlocked wallets: {:?}", + error_msg, + missing_addresses, + signers.keys().collect::>() + ); + } + + let chain = provider.get_chainid().await?.as_u64(); + + (SendTransactionsKind::Raw(signers), chain) }; // We only wait for a transaction receipt before sending the next transaction, if there @@ -280,6 +302,7 @@ impl ScriptArgs { decoder: &CallTraceDecoder, mut script_config: ScriptConfig, verify: VerifyBundle, + signers: &HashMap, ) -> Result<()> { if let Some(txs) = result.transactions.take() { script_config.collect_rpcs(&txs); @@ -315,8 +338,8 @@ impl ScriptArgs { multi, libraries, &script_config.config, - result.script_wallets, verify, + signers, ) .await?; } @@ -325,8 +348,8 @@ impl ScriptArgs { deployments.first_mut().expect("missing deployment"), script_config, libraries, - result, verify, + signers, ) .await?; } @@ -347,8 +370,8 @@ impl ScriptArgs { deployment_sequence: &mut ScriptSequence, script_config: ScriptConfig, libraries: Libraries, - result: ScriptResult, verify: VerifyBundle, + signers: &HashMap, ) -> Result<()> { trace!(target: "script", "broadcasting single chain deployment"); @@ -360,7 +383,7 @@ impl ScriptArgs { deployment_sequence.add_libraries(libraries); - self.send_transactions(deployment_sequence, &rpc, &result.script_wallets).await?; + self.send_transactions(deployment_sequence, &rpc, signers).await?; if self.verify { return deployment_sequence.verify_contracts(&script_config.config, verify).await; @@ -639,14 +662,14 @@ enum SendTransactionKind<'a> { } /// Represents how to send _all_ transactions -enum SendTransactionsKind { +enum SendTransactionsKind<'a> { /// Send via `eth_sendTransaction` and rely on the `from` address being unlocked. Unlocked(HashSet
), /// Send a signed transaction via `eth_sendRawTransaction` - Raw(HashMap), + Raw(&'a HashMap), } -impl SendTransactionsKind { +impl SendTransactionsKind<'_> { /// Returns the [`SendTransactionKind`] for the given address /// /// Returns an error if no matching signer is found or the address is not unlocked diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 69032462d3e7..c1e0fefb0de3 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -17,8 +17,9 @@ use foundry_compilers::{ contracts::ArtifactContracts, }; use foundry_debugger::Debugger; -use foundry_evm::inspectors::cheatcodes::BroadcastableTransaction; -use std::sync::Arc; +use foundry_evm::inspectors::cheatcodes::{BroadcastableTransaction, ScriptWallets}; +use foundry_wallets::WalletSigner; +use std::{collections::HashMap, sync::Arc}; /// Helper alias type for the collection of data changed due to the new sender. type NewSenderChanges = (CallTraceDecoder, Libraries, ArtifactContracts); @@ -38,7 +39,9 @@ impl ScriptArgs { ..Default::default() }; - self.maybe_load_private_key(&mut script_config)?; + if let Some(sender) = self.maybe_load_private_key()? { + script_config.evm_opts.sender = sender; + } if let Some(ref fork_url) = script_config.evm_opts.fork_url { // when forking, override the sender's nonce to the onchain value @@ -72,13 +75,24 @@ impl ScriptArgs { // Execute once with default sender. let sender = script_config.evm_opts.sender; + let multi_wallet = self.wallets.get_multi_wallet().await?; + let script_wallets = ScriptWallets::new(multi_wallet, self.evm_opts.sender); + // We need to execute the script even if just resuming, in case we need to collect private // keys from the execution. - let mut result = - self.execute(&mut script_config, contract, sender, &predeploy_libraries).await?; + let mut result = self + .execute( + &mut script_config, + contract, + sender, + &predeploy_libraries, + script_wallets.clone(), + ) + .await?; if self.resume || (self.verify && !self.broadcast) { - return self.resume_deployment(script_config, linker, libraries, result, verify).await; + let signers = script_wallets.into_multi_wallet().into_signers()?; + return self.resume_deployment(script_config, linker, libraries, verify, &signers).await; } let known_contracts = flatten_contracts(&highlevel_known_contracts, true); @@ -95,7 +109,13 @@ impl ScriptArgs { } if let Some((new_traces, updated_libraries, updated_contracts)) = self - .maybe_prepare_libraries(&mut script_config, linker, predeploy_libraries, &mut result) + .maybe_prepare_libraries( + &mut script_config, + linker, + predeploy_libraries, + &mut result, + script_wallets.clone(), + ) .await? { decoder = new_traces; @@ -112,8 +132,17 @@ impl ScriptArgs { verify.known_contracts = flatten_contracts(&highlevel_known_contracts, false); self.check_contract_sizes(&result, &highlevel_known_contracts)?; - self.handle_broadcastable_transactions(result, libraries, &decoder, script_config, verify) - .await + let signers = script_wallets.into_multi_wallet().into_signers()?; + + self.handle_broadcastable_transactions( + result, + libraries, + &decoder, + script_config, + verify, + &signers, + ) + .await } // In case there are libraries to be deployed, it makes sure that these are added to the list of @@ -124,6 +153,7 @@ impl ScriptArgs { linker: Linker, predeploy_libraries: Vec, result: &mut ScriptResult, + script_wallets: ScriptWallets, ) -> Result> { if let Some(new_sender) = self.maybe_new_sender( &script_config.evm_opts, @@ -131,8 +161,9 @@ impl ScriptArgs { &predeploy_libraries, )? { // We have a new sender, so we need to relink all the predeployed libraries. - let (libraries, highlevel_known_contracts) = - self.rerun_with_new_deployer(script_config, new_sender, result, linker).await?; + let (libraries, highlevel_known_contracts) = self + .rerun_with_new_deployer(script_config, new_sender, result, linker, script_wallets) + .await?; // redo traces for the new addresses let new_traces = self.decode_traces( @@ -171,8 +202,8 @@ impl ScriptArgs { script_config: ScriptConfig, linker: Linker, libraries: Libraries, - result: ScriptResult, verify: VerifyBundle, + signers: &HashMap, ) -> Result<()> { if self.multi { return self @@ -184,16 +215,16 @@ impl ScriptArgs { )?, libraries, &script_config.config, - result.script_wallets, verify, + signers, ) .await; } self.resume_single_deployment( script_config, linker, - result, verify, + signers, ) .await .map_err(|err| { @@ -206,8 +237,8 @@ impl ScriptArgs { &mut self, script_config: ScriptConfig, linker: Linker, - result: ScriptResult, mut verify: VerifyBundle, + signers: &HashMap, ) -> Result<()> { trace!(target: "script", "resuming single deployment"); @@ -249,8 +280,7 @@ impl ScriptArgs { receipts::wait_for_pending(provider, &mut deployment_sequence).await?; if self.resume { - self.send_transactions(&mut deployment_sequence, fork_url, &result.script_wallets) - .await?; + self.send_transactions(&mut deployment_sequence, fork_url, signers).await?; } if self.verify { @@ -287,6 +317,7 @@ impl ScriptArgs { new_sender: Address, first_run_result: &mut ScriptResult, linker: Linker, + script_wallets: ScriptWallets, ) -> Result<(Libraries, ArtifactContracts)> { // if we had a new sender that requires relinking, we need to // get the nonce mainnet for accurate addresses for predeploy libs @@ -318,8 +349,9 @@ impl ScriptArgs { &script_config.evm_opts.fork_url, ); - let result = - self.execute(script_config, contract, new_sender, &predeploy_libraries).await?; + let result = self + .execute(script_config, contract, new_sender, &predeploy_libraries, script_wallets) + .await?; if let Some(new_txs) = &result.transactions { for new_tx in new_txs.iter() { @@ -338,15 +370,12 @@ impl ScriptArgs { /// In case the user has loaded *only* one private-key, we can assume that he's using it as the /// `--sender` - fn maybe_load_private_key(&mut self, script_config: &mut ScriptConfig) -> Result<()> { - if let Some(ref private_key) = self.wallets.private_key { - self.wallets.private_keys = Some(vec![private_key.clone()]); - } - if let Some(wallets) = self.wallets.private_keys()? { - if wallets.len() == 1 { - script_config.evm_opts.sender = wallets.first().unwrap().address().to_alloy() - } - } - Ok(()) + fn maybe_load_private_key(&mut self) -> Result> { + let maybe_sender = self + .wallets + .private_keys()? + .filter(|pks| pks.len() == 1) + .map(|pks| pks.first().unwrap().address().to_alloy()); + Ok(maybe_sender) } } diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index ac86c112a4ea..e0f8ed8ece1b 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -15,6 +15,7 @@ use forge::{ use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{get_contract_name, provider::ethers::RpcUrl, shell, ContractsByArtifact}; use foundry_compilers::artifacts::ContractBytecodeSome; +use foundry_evm::inspectors::cheatcodes::ScriptWallets; use futures::future::join_all; use parking_lot::RwLock; use std::{ @@ -31,6 +32,7 @@ impl ScriptArgs { contract: ContractBytecodeSome, sender: Address, predeploy_libraries: &[Bytes], + script_wallets: ScriptWallets, ) -> Result { trace!(target: "script", "start executing script"); @@ -42,7 +44,9 @@ impl ScriptArgs { ensure_clean_constructor(&abi)?; - let mut runner = self.prepare_runner(script_config, sender, SimulationStage::Local).await?; + let mut runner = self + .prepare_runner(script_config, sender, SimulationStage::Local, Some(script_wallets)) + .await?; let (address, mut result) = runner.setup( predeploy_libraries, bytecode, @@ -66,7 +70,6 @@ impl ScriptArgs { result.debug = script_result.debug; result.labeled_addresses.extend(script_result.labeled_addresses); result.returned = script_result.returned; - result.script_wallets.extend(script_result.script_wallets); result.breakpoints = script_result.breakpoints; match (&mut result.transactions, script_result.transactions) { @@ -252,7 +255,7 @@ impl ScriptArgs { let mut script_config = script_config.clone(); script_config.evm_opts.fork_url = Some(rpc.clone()); let runner = self - .prepare_runner(&mut script_config, sender, SimulationStage::OnChain) + .prepare_runner(&mut script_config, sender, SimulationStage::OnChain, None) .await?; Ok((rpc.clone(), runner)) }) @@ -267,6 +270,7 @@ impl ScriptArgs { script_config: &mut ScriptConfig, sender: Address, stage: SimulationStage, + script_wallets: Option, ) -> Result { trace!("preparing script runner"); let env = script_config.evm_opts.evm_env().await?; @@ -300,7 +304,12 @@ impl ScriptArgs { if let SimulationStage::Local = stage { builder = builder.inspectors(|stack| { stack.debug(self.debug).cheatcodes( - CheatsConfig::new(&script_config.config, script_config.evm_opts.clone()).into(), + CheatsConfig::new( + &script_config.config, + script_config.evm_opts.clone(), + script_wallets, + ) + .into(), ) }); } diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 17ff7e569238..5df893bb0ed5 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -6,7 +6,6 @@ use alloy_rpc_types::request::TransactionRequest; use clap::{Parser, ValueHint}; use dialoguer::Confirm; use ethers_providers::{Http, Middleware}; -use ethers_signers::LocalWallet; use eyre::{ContextCompat, Result, WrapErr}; use forge::{ backend::Backend, @@ -43,7 +42,7 @@ use foundry_evm::{ decode::RevertDecoder, inspectors::cheatcodes::{BroadcastableTransaction, BroadcastableTransactions}, }; -use foundry_wallets::MultiWallet; +use foundry_wallets::MultiWalletOpts; use futures::future; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -177,7 +176,7 @@ pub struct ScriptArgs { pub opts: BuildArgs, #[clap(flatten)] - pub wallets: MultiWallet, + pub wallets: MultiWalletOpts, #[clap(flatten)] pub evm_opts: EvmArgs, @@ -593,7 +592,6 @@ pub struct ScriptResult { pub transactions: Option, pub returned: Bytes, pub address: Option
, - pub script_wallets: Vec, pub breakpoints: Breakpoints, } diff --git a/crates/forge/bin/cmd/script/multi.rs b/crates/forge/bin/cmd/script/multi.rs index ab8cd64cbb01..874dd24ba636 100644 --- a/crates/forge/bin/cmd/script/multi.rs +++ b/crates/forge/bin/cmd/script/multi.rs @@ -4,15 +4,17 @@ use super::{ verify::VerifyBundle, ScriptArgs, }; -use ethers_signers::LocalWallet; +use alloy_primitives::Address; use eyre::{ContextCompat, Report, Result, WrapErr}; use foundry_cli::utils::now; use foundry_common::{fs, provider::ethers::get_http_provider}; use foundry_compilers::{artifacts::Libraries, ArtifactId}; use foundry_config::Config; +use foundry_wallets::WalletSigner; use futures::future::join_all; use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, io::{BufWriter, Write}, path::{Path, PathBuf}, sync::Arc, @@ -179,8 +181,8 @@ impl ScriptArgs { mut deployments: MultiChainSequence, libraries: Libraries, config: &Config, - script_wallets: Vec, verify: VerifyBundle, + signers: &HashMap, ) -> Result<()> { if !libraries.is_empty() { eyre::bail!("Libraries are currently not supported on multi deployment setups."); @@ -219,7 +221,7 @@ impl ScriptArgs { for sequence in deployments.deployments.iter_mut() { let rpc_url = sequence.rpc_url().unwrap().to_string(); - let result = match self.send_transactions(sequence, &rpc_url, &script_wallets).await { + let result = match self.send_transactions(sequence, &rpc_url, signers).await { Ok(_) if self.verify => sequence.verify_contracts(config, verify.clone()).await, Ok(_) => Ok(()), Err(err) => Err(err), diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/forge/bin/cmd/script/runner.rs index 838dcfdf34b7..96937bfdbc97 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/forge/bin/cmd/script/runner.rs @@ -7,7 +7,6 @@ use forge::{ revm::interpreter::{return_ok, InstructionResult}, traces::{TraceKind, Traces}, }; -use foundry_common::types::ToEthers; use foundry_config::Config; use yansi::Paint; @@ -93,17 +92,9 @@ impl ScriptRunner { traces.extend(constructor_traces.map(|traces| (TraceKind::Deployment, traces))); // Optionally call the `setUp` function - let (success, gas_used, labeled_addresses, transactions, debug, script_wallets) = if !setup - { + let (success, gas_used, labeled_addresses, transactions, debug) = if !setup { self.executor.backend.set_test_contract(address); - ( - true, - 0, - Default::default(), - None, - vec![constructor_debug].into_iter().collect(), - vec![], - ) + (true, 0, Default::default(), None, vec![constructor_debug].into_iter().collect()) } else { match self.executor.setup(Some(self.sender), address) { Ok(CallResult { @@ -114,7 +105,6 @@ impl ScriptRunner { debug, gas_used, transactions, - script_wallets, .. }) => { traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); @@ -128,7 +118,6 @@ impl ScriptRunner { labels, transactions, vec![constructor_debug, debug].into_iter().collect(), - script_wallets, ) } Err(EvmError::Execution(err)) => { @@ -140,7 +129,6 @@ impl ScriptRunner { debug, gas_used, transactions, - script_wallets, .. } = *err; traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); @@ -154,7 +142,6 @@ impl ScriptRunner { labels, transactions, vec![constructor_debug, debug].into_iter().collect(), - script_wallets, ) } Err(e) => return Err(e.into()), @@ -173,7 +160,6 @@ impl ScriptRunner { traces, debug, address: None, - script_wallets: script_wallets.to_ethers(), ..Default::default() }, )) @@ -279,17 +265,7 @@ impl ScriptRunner { res = self.executor.call_raw_committing(from, to, calldata, value)?; } - let RawCallResult { - result, - reverted, - logs, - traces, - labels, - debug, - transactions, - script_wallets, - .. - } = res; + let RawCallResult { result, reverted, logs, traces, labels, debug, transactions, .. } = res; let breakpoints = res.cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default(); Ok(ScriptResult { @@ -308,7 +284,6 @@ impl ScriptRunner { labeled_addresses: labels, transactions, address: None, - script_wallets: script_wallets.to_ethers(), breakpoints, }) } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 102f93e31ef1..4f0f9f29940a 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -195,7 +195,7 @@ impl TestArgs { .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) - .with_cheats_config(CheatsConfig::new(&config, evm_opts.clone())) + .with_cheats_config(CheatsConfig::new(&config, evm_opts.clone(), None)) .with_test_options(test_options.clone()) .build(project_root, output, env, evm_opts)?; diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index 22c01f67e155..04f58234fcf4 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -178,7 +178,7 @@ pub async fn runner_with_config(mut config: Config) -> MultiContractRunner { let output = COMPILED.clone(); base_runner() .with_test_options(test_opts()) - .with_cheats_config(CheatsConfig::new(&config, opts.clone())) + .with_cheats_config(CheatsConfig::new(&config, opts.clone(), None)) .sender(config.sender) .build(root, output, env, opts.clone()) .unwrap() diff --git a/crates/wallets/Cargo.toml b/crates/wallets/Cargo.toml index f531fd343ce6..501d0777f0fb 100644 --- a/crates/wallets/Cargo.toml +++ b/crates/wallets/Cargo.toml @@ -32,6 +32,9 @@ serde.workspace = true thiserror = "1" tracing.workspace = true +[dev-dependencies] +tokio = { version = "1", features = ["macros"] } + [features] default = ["rustls"] rustls = ["ethers-providers/rustls", "rusoto_core/rustls"] diff --git a/crates/wallets/src/error.rs b/crates/wallets/src/error.rs index 788c04db5911..6588f5e22cc1 100644 --- a/crates/wallets/src/error.rs +++ b/crates/wallets/src/error.rs @@ -19,6 +19,10 @@ pub enum WalletSignerError { Trezor(#[from] TrezorError), #[error(transparent)] Aws(#[from] AwsSignerError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + InvalidHex(#[from] FromHexError), #[error("{0} cannot sign raw hashes")] CannotSignRawHash(&'static str), } diff --git a/crates/wallets/src/lib.rs b/crates/wallets/src/lib.rs index 0e17c4ced3be..38ff5e7fa91a 100644 --- a/crates/wallets/src/lib.rs +++ b/crates/wallets/src/lib.rs @@ -4,8 +4,11 @@ extern crate tracing; pub mod error; pub mod multi_wallet; pub mod raw_wallet; +pub mod utils; pub mod wallet; +pub mod wallet_signer; -pub use multi_wallet::MultiWallet; -pub use raw_wallet::RawWallet; -pub use wallet::{Wallet, WalletSigner}; +pub use multi_wallet::MultiWalletOpts; +pub use raw_wallet::RawWalletOpts; +pub use wallet::WalletOpts; +pub use wallet_signer::{PendingSigner, WalletSigner}; diff --git a/crates/wallets/src/multi_wallet.rs b/crates/wallets/src/multi_wallet.rs index 4b7280731e5f..21692eb04794 100644 --- a/crates/wallets/src/multi_wallet.rs +++ b/crates/wallets/src/multi_wallet.rs @@ -1,63 +1,80 @@ -use crate::wallet::{WalletSigner, WalletTrait}; +use crate::{ + utils, + wallet_signer::{PendingSigner, WalletSigner}, +}; use alloy_primitives::Address; use clap::Parser; -use ethers_providers::Middleware; -use ethers_signers::{ - AwsSigner, HDPath as LedgerHDPath, Ledger, LocalWallet, Signer, Trezor, TrezorHDPath, -}; -use eyre::{Context, ContextCompat, Result}; -use foundry_common::{provider::ethers::RetryProvider, types::ToAlloy}; +use ethers_signers::Signer; +use eyre::Result; +use foundry_common::types::ToAlloy; use foundry_config::Config; -use itertools::izip; -use rusoto_core::{ - credential::ChainProvider as AwsChainProvider, region::Region as AwsRegion, - request::HttpClient as AwsHttpClient, Client as AwsClient, -}; -use rusoto_kms::KmsClient; use serde::Serialize; -use std::{ - collections::{HashMap, HashSet}, - iter::repeat, - sync::Arc, -}; +use std::{collections::HashMap, iter::repeat, path::PathBuf}; -macro_rules! get_wallets { - ($id:ident, [ $($wallets:expr),+ ], $call:expr) => { - $( - if let Some($id) = $wallets { - $call; - } - )+ - }; +/// Container for multiple wallets. +#[derive(Debug, Default)] +pub struct MultiWallet { + /// Vector of wallets that require an action to be unlocked. + /// Those are lazily unlocked on the first access of the signers. + pending_signers: Vec, + /// Contains unlocked signers. + signers: HashMap, +} + +impl MultiWallet { + pub fn new(pending_signers: Vec, signers: Vec) -> Self { + let signers = + signers.into_iter().map(|signer| (signer.address().to_alloy(), signer)).collect(); + Self { pending_signers, signers } + } + + fn maybe_unlock_pending(&mut self) -> Result<()> { + for pending in self.pending_signers.drain(..) { + let signer = pending.unlock()?; + self.signers.insert(signer.address().to_alloy(), signer); + } + Ok(()) + } + + pub fn signers(&mut self) -> Result<&HashMap> { + self.maybe_unlock_pending()?; + Ok(&self.signers) + } + + pub fn into_signers(mut self) -> Result> { + self.maybe_unlock_pending()?; + Ok(self.signers) + } + + pub fn add_signer(&mut self, signer: WalletSigner) { + self.signers.insert(signer.address().to_alloy(), signer); + } } /// A macro that initializes multiple wallets /// /// Should be used with a [`MultiWallet`] instance macro_rules! create_hw_wallets { - ($self:ident, $chain_id:ident ,$get_wallet:ident, $wallets:ident) => { - let mut $wallets = vec![]; + ($self:ident, $create_signer:expr, $signers:ident) => { + let mut $signers = vec![]; if let Some(hd_paths) = &$self.hd_paths { for path in hd_paths { - if let Some(hw) = $self.$get_wallet($chain_id, Some(path), None).await? { - $wallets.push(hw); - } + let hw = $create_signer(Some(path), 0).await?; + $signers.push(hw); } } if let Some(mnemonic_indexes) = &$self.mnemonic_indexes { for index in mnemonic_indexes { - if let Some(hw) = $self.$get_wallet($chain_id, None, Some(*index as usize)).await? { - $wallets.push(hw); - } + let hw = $create_signer(None, *index).await?; + $signers.push(hw); } } - if $wallets.is_empty() { - if let Some(hw) = $self.$get_wallet($chain_id, None, Some(0)).await? { - $wallets.push(hw); - } + if $signers.is_empty() { + let hw = $create_signer(None, 0).await?; + $signers.push(hw); } }; } @@ -72,7 +89,7 @@ macro_rules! create_hw_wallets { /// 7. AWS KMS #[derive(Clone, Debug, Default, Serialize, Parser)] #[clap(next_help_heading = "Wallet options", about = None, long_about = None)] -pub struct MultiWallet { +pub struct MultiWalletOpts { /// The sender accounts. #[clap( long, @@ -197,91 +214,72 @@ pub struct MultiWallet { pub aws: bool, } -impl WalletTrait for MultiWallet { - fn sender(&self) -> Option
{ - self.froms.as_ref()?.first().copied() - } -} +impl MultiWalletOpts { + /// Returns [MultiWallet] container configured with provided options. + pub async fn get_multi_wallet(&self) -> Result { + let mut pending = Vec::new(); + let mut signers: Vec = Vec::new(); -impl MultiWallet { - /// Given a list of addresses, it finds all the associated wallets if they exist. Throws an - /// error, if it can't find all. - pub async fn find_all( - &self, - provider: Arc, - mut addresses: HashSet
, - script_wallets: &[LocalWallet], - ) -> Result> { - println!("\n###\nFinding wallets for all the necessary addresses..."); - let chain = provider.get_chainid().await?.as_u64(); - - let mut local_wallets = HashMap::new(); - let mut unused_wallets = vec![]; - - get_wallets!( - wallets, - [ - self.trezors(chain).await?, - self.ledgers(chain).await?, - self.private_keys()?, - self.interactives()?, - self.mnemonics()?, - self.keystores()?, - self.aws_signers(chain).await?, - (!script_wallets.is_empty()).then(|| script_wallets.to_vec()) - ], - for wallet in wallets.into_iter() { - let address = wallet.address(); - if addresses.contains(&address.to_alloy()) { - addresses.remove(&address.to_alloy()); - - let signer = WalletSigner::from(wallet.with_chain_id(chain)); - local_wallets.insert(address.to_alloy(), signer); - - if addresses.is_empty() { - return Ok(local_wallets); - } - } else { - // Just to show on error. - unused_wallets.push(address.to_alloy()); - } - } - ); - - let mut error_msg = String::new(); - - // This is an actual used address - if addresses.contains(&Config::DEFAULT_SENDER) { - error_msg += "\nYou seem to be using Foundry's default sender. Be sure to set your own --sender.\n"; + if let Some(ledgers) = self.ledgers().await? { + signers.extend(ledgers); + } + if let Some(trezors) = self.trezors().await? { + signers.extend(trezors); + } + if let Some(aws_signers) = self.aws_signers().await? { + signers.extend(aws_signers); + } + if let Some((pending_keystores, unlocked)) = self.keystores()? { + pending.extend(pending_keystores); + signers.extend(unlocked); + } + if let Some(pks) = self.private_keys()? { + signers.extend(pks); + } + if let Some(mnemonics) = self.mnemonics()? { + signers.extend(mnemonics); + } + if self.interactives > 0 { + pending.extend(repeat(PendingSigner::Interactive).take(self.interactives as usize)); } - unused_wallets.extend(local_wallets.into_keys()); - eyre::bail!( - "{}No associated wallet for addresses: {:?}. Unlocked wallets: {:?}", - error_msg, - addresses, - unused_wallets - ) + Ok(MultiWallet::new(pending, signers)) } - pub fn interactives(&self) -> Result>> { - if self.interactives != 0 { - let mut wallets = vec![]; - for _ in 0..self.interactives { - wallets.push(self.get_from_interactive()?); + pub fn private_keys(&self) -> Result>> { + let mut pks = vec![]; + if let Some(private_key) = &self.private_key { + pks.push(private_key); + } + if let Some(private_keys) = &self.private_keys { + for pk in private_keys { + pks.push(pk); } - return Ok(Some(wallets)); } - Ok(None) + if !pks.is_empty() { + let wallets = pks + .into_iter() + .map(|pk| utils::create_private_key_signer(pk)) + .collect::>>()?; + Ok(Some(wallets)) + } else { + Ok(None) + } } - pub fn private_keys(&self) -> Result>> { - if let Some(private_keys) = &self.private_keys { - let mut wallets = vec![]; - for private_key in private_keys.iter() { - wallets.push(self.get_from_private_key(private_key.trim())?); - } - return Ok(Some(wallets)); + fn keystore_paths(&self) -> Result>> { + if let Some(keystore_paths) = &self.keystore_paths { + return Ok(Some(keystore_paths.iter().map(PathBuf::from).collect())); + } + if let Some(keystore_account_names) = &self.keystore_account_names { + let default_keystore_dir = Config::foundry_keystores_dir() + .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?; + return Ok(Some( + keystore_account_names + .iter() + .map(|keystore_name| default_keystore_dir.join(keystore_name)) + .collect(), + )); } Ok(None) } @@ -289,23 +287,10 @@ impl MultiWallet { /// Returns all wallets read from the provided keystores arguments /// /// Returns `Ok(None)` if no keystore provided. - pub fn keystores(&self) -> Result>> { - let default_keystore_dir = Config::foundry_keystores_dir() - .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?; - // If keystore paths are provided, use them, else, use default path + keystore account names - let keystore_paths = self.keystore_paths.clone().or_else(|| { - self.keystore_account_names.as_ref().map(|keystore_names| { - keystore_names - .iter() - .map(|keystore_name| { - default_keystore_dir.join(keystore_name).to_string_lossy().into_owned() - }) - .collect() - }) - }); - - if let Some(keystore_paths) = keystore_paths { - let mut wallets = Vec::with_capacity(keystore_paths.len()); + pub fn keystores(&self) -> Result, Vec)>> { + if let Some(keystore_paths) = self.keystore_paths()? { + let mut pending = Vec::new(); + let mut signers = Vec::new(); let mut passwords_iter = self.keystore_passwords.clone().unwrap_or_default().into_iter(); @@ -313,51 +298,49 @@ impl MultiWallet { let mut password_files_iter = self.keystore_password_files.clone().unwrap_or_default().into_iter(); - for path in keystore_paths { - let wallet = self.get_from_keystore(Some(&path), passwords_iter.next().as_ref(), password_files_iter.next().as_ref())?.wrap_err("Keystore paths do not have the same length as provided passwords or password files.")?; - wallets.push(wallet); + for path in &keystore_paths { + let (maybe_signer, maybe_pending) = utils::create_keystore_signer( + path, + passwords_iter.next().as_deref(), + password_files_iter.next().as_deref(), + )?; + if let Some(pending_signer) = maybe_pending { + pending.push(pending_signer); + } else if let Some(signer) = maybe_signer { + signers.push(signer); + } } - return Ok(Some(wallets)); + return Ok(Some((pending, signers))); } Ok(None) } - pub fn mnemonics(&self) -> Result>> { + pub fn mnemonics(&self) -> Result>> { if let Some(ref mnemonics) = self.mnemonics { let mut wallets = vec![]; - let hd_paths: Vec<_> = if let Some(ref hd_paths) = self.hd_paths { - hd_paths.iter().map(Some).collect() - } else { - repeat(None).take(mnemonics.len()).collect() - }; - let mnemonic_passphrases: Vec<_> = - if let Some(ref mnemonic_passphrases) = self.mnemonic_passphrases { - mnemonic_passphrases.iter().map(Some).collect() - } else { - repeat(None).take(mnemonics.len()).collect() - }; - let mnemonic_indexes: Vec<_> = if let Some(ref mnemonic_indexes) = self.mnemonic_indexes - { - mnemonic_indexes.to_vec() - } else { - repeat(0).take(mnemonics.len()).collect() - }; - for (mnemonic, mnemonic_passphrase, hd_path, mnemonic_index) in - izip!(mnemonics, mnemonic_passphrases, hd_paths, mnemonic_indexes) - { - wallets.push(self.get_from_mnemonic( + + let mut hd_paths_iter = self.hd_paths.clone().unwrap_or_default().into_iter(); + + let mut passphrases_iter = + self.mnemonic_passphrases.clone().unwrap_or_default().into_iter(); + + let mut indexes_iter = self.mnemonic_indexes.clone().unwrap_or_default().into_iter(); + + for mnemonic in mnemonics { + let wallet = utils::create_mnemonic_signer( mnemonic, - mnemonic_passphrase, - hd_path, - mnemonic_index, - )?) + passphrases_iter.next().as_deref(), + hd_paths_iter.next().as_deref(), + indexes_iter.next().unwrap_or(0), + )?; + wallets.push(wallet); } return Ok(Some(wallets)); } Ok(None) } - pub async fn ledgers(&self, chain_id: u64) -> Result>> { + pub async fn ledgers(&self) -> Result>> { if self.ledger { let mut args = self.clone(); @@ -368,34 +351,31 @@ impl MultiWallet { args.mnemonic_indexes = None; } - create_hw_wallets!(args, chain_id, get_from_ledger, wallets); + create_hw_wallets!(args, utils::create_ledger_signer, wallets); return Ok(Some(wallets)); } Ok(None) } - pub async fn trezors(&self, chain_id: u64) -> Result>> { + pub async fn trezors(&self) -> Result>> { if self.trezor { - create_hw_wallets!(self, chain_id, get_from_trezor, wallets); + create_hw_wallets!(self, utils::create_trezor_signer, wallets); return Ok(Some(wallets)); } Ok(None) } - pub async fn aws_signers(&self, chain_id: u64) -> Result>> { + pub async fn aws_signers(&self) -> Result>> { if self.aws { let mut wallets = vec![]; - let client = - AwsClient::new_with(AwsChainProvider::default(), AwsHttpClient::new().unwrap()); - - let kms = KmsClient::new_with_client(client, AwsRegion::default()); - - let env_key_ids = std::env::var("AWS_KMS_KEY_IDS"); - let key_ids = - if env_key_ids.is_ok() { env_key_ids? } else { std::env::var("AWS_KMS_KEY_ID")? }; - - for key in key_ids.split(',') { - let aws_signer = AwsSigner::new(kms.clone(), key, chain_id).await?; + let aws_keys = std::env::var("AWS_KMS_KEY_IDS") + .or(std::env::var("AWS_KMS_KEY_ID"))? + .split(',') + .map(|k| k.to_string()) + .collect::>(); + + for key in aws_keys { + let aws_signer = WalletSigner::from_aws(&key).await?; wallets.push(aws_signer) } @@ -403,50 +383,23 @@ impl MultiWallet { } Ok(None) } - - async fn get_from_trezor( - &self, - chain_id: u64, - hd_path: Option<&str>, - mnemonic_index: Option, - ) -> Result> { - let derivation = match &hd_path { - Some(hd_path) => TrezorHDPath::Other(hd_path.to_string()), - None => TrezorHDPath::TrezorLive(mnemonic_index.unwrap_or(0)), - }; - - Ok(Some(Trezor::new(derivation, chain_id, None).await?)) - } - - async fn get_from_ledger( - &self, - chain_id: u64, - hd_path: Option<&str>, - mnemonic_index: Option, - ) -> Result> { - let derivation = match hd_path { - Some(hd_path) => LedgerHDPath::Other(hd_path.to_string()), - None => LedgerHDPath::LedgerLive(mnemonic_index.unwrap_or(0)), - }; - - trace!(?chain_id, "Creating new ledger signer"); - Ok(Some(Ledger::new(derivation, chain_id).await.wrap_err("Ledger device not available.")?)) - } } #[cfg(test)] mod tests { + use ethers_signers::Signer; + use super::*; use std::path::Path; #[test] fn parse_keystore_args() { - let args: MultiWallet = - MultiWallet::parse_from(["foundry-cli", "--keystores", "my/keystore/path"]); + let args: MultiWalletOpts = + MultiWalletOpts::parse_from(["foundry-cli", "--keystores", "my/keystore/path"]); assert_eq!(args.keystore_paths, Some(vec!["my/keystore/path".to_string()])); std::env::set_var("ETH_KEYSTORE", "MY_KEYSTORE"); - let args: MultiWallet = MultiWallet::parse_from(["foundry-cli"]); + let args: MultiWalletOpts = MultiWalletOpts::parse_from(["foundry-cli"]); assert_eq!(args.keystore_paths, Some(vec!["MY_KEYSTORE".to_string()])); std::env::remove_var("ETH_KEYSTORE"); @@ -461,7 +414,7 @@ mod tests { let keystore_password_file = keystore.join("password-ec554").into_os_string(); - let args: MultiWallet = MultiWallet::parse_from([ + let args: MultiWalletOpts = MultiWalletOpts::parse_from([ "foundry-cli", "--keystores", keystore_file.to_str().unwrap(), @@ -473,10 +426,10 @@ mod tests { Some(vec![keystore_password_file.to_str().unwrap().to_string()]) ); - let wallets = args.keystores().unwrap().unwrap(); - assert_eq!(wallets.len(), 1); + let (_, unlocked) = args.keystores().unwrap().unwrap(); + assert_eq!(unlocked.len(), 1); assert_eq!( - wallets[0].address(), + unlocked[0].address(), "ec554aeafe75601aaab43bd4621a22284db566c2".parse().unwrap() ); } @@ -491,7 +444,7 @@ mod tests { ]; for test_case in wallet_options { - let args: MultiWallet = MultiWallet::parse_from([ + let args: MultiWalletOpts = MultiWalletOpts::parse_from([ "foundry-cli", &format!("--{}", test_case.0), test_case.1, diff --git a/crates/wallets/src/raw_wallet.rs b/crates/wallets/src/raw_wallet.rs index e33dc22876c5..ccb1d6388dc1 100644 --- a/crates/wallets/src/raw_wallet.rs +++ b/crates/wallets/src/raw_wallet.rs @@ -1,4 +1,6 @@ +use crate::{utils, PendingSigner, WalletSigner}; use clap::Parser; +use eyre::Result; use serde::Serialize; /// A wrapper for the raw data options for `Wallet`, extracted to also be used standalone. @@ -8,7 +10,7 @@ use serde::Serialize; /// 3. Mnemonic (via file path) #[derive(Clone, Debug, Default, Serialize, Parser)] #[clap(next_help_heading = "Wallet options - raw", about = None, long_about = None)] -pub struct RawWallet { +pub struct RawWalletOpts { /// Open an interactive prompt to enter your private key. #[clap(long, short)] pub interactive: bool, @@ -37,3 +39,24 @@ pub struct RawWallet { #[clap(long, conflicts_with = "hd_path", default_value_t = 0, value_name = "INDEX")] pub mnemonic_index: u32, } + +impl RawWalletOpts { + /// Returns signer configured by provided parameters. + pub fn signer(&self) -> Result> { + if self.interactive { + return Ok(Some(PendingSigner::Interactive.unlock()?)); + } + if let Some(private_key) = &self.private_key { + return Ok(Some(utils::create_private_key_signer(private_key)?)) + } + if let Some(mnemonic) = &self.mnemonic { + return Ok(Some(utils::create_mnemonic_signer( + mnemonic, + self.mnemonic_passphrase.as_deref(), + self.hd_path.as_deref(), + self.mnemonic_index, + )?)) + } + Ok(None) + } +} diff --git a/crates/wallets/src/utils.rs b/crates/wallets/src/utils.rs new file mode 100644 index 000000000000..a10903313034 --- /dev/null +++ b/crates/wallets/src/utils.rs @@ -0,0 +1,156 @@ +use crate::{error::PrivateKeyError, PendingSigner, WalletSigner}; +use ethers_signers::{HDPath as LedgerHDPath, LocalWallet, TrezorHDPath, WalletError}; +use eyre::{Context, Result}; +use foundry_config::Config; +use std::{ + fs, + path::{Path, PathBuf}, + str::FromStr, +}; + +/// Validates and sanitizes user inputs, returning configured [WalletSigner]. +pub fn create_private_key_signer(private_key: &str) -> Result { + let privk = private_key.trim().strip_prefix("0x").unwrap_or(private_key); + match LocalWallet::from_str(privk) { + Ok(pk) => Ok(WalletSigner::Local(pk)), + Err(err) => { + // helper closure to check if pk was meant to be an env var, this usually happens if + // `$` is missing + let ensure_not_env = |pk: &str| { + // check if pk was meant to be an env var + if !pk.starts_with("0x") && std::env::var(pk).is_ok() { + // SAFETY: at this point we know the user actually wanted to use an env var + // and most likely forgot the `$` anchor, so the + // `private_key` here is an unresolved env var + return Err(PrivateKeyError::ExistsAsEnvVar(pk.to_string())) + } + Ok(()) + }; + match err { + WalletError::HexError(err) => { + ensure_not_env(private_key)?; + return Err(PrivateKeyError::InvalidHex(err).into()); + } + WalletError::EcdsaError(_) => ensure_not_env(private_key)?, + _ => {} + }; + eyre::bail!("Failed to create wallet from private key: {err}") + } + } +} + +/// Creates [WalletSigner] instance from given mnemonic parameters. +/// +/// Mnemonic can be either a file path or a mnemonic phrase. +pub fn create_mnemonic_signer( + mnemonic: &str, + passphrase: Option<&str>, + hd_path: Option<&str>, + index: u32, +) -> Result { + let mnemonic = if Path::new(mnemonic).is_file() { + fs::read_to_string(mnemonic)?.replace('\n', "") + } else { + mnemonic.to_owned() + }; + + Ok(WalletSigner::from_mnemonic(&mnemonic, passphrase, hd_path, index)?) +} + +/// Creates [WalletSigner] instance from given Ledger parameters. +pub async fn create_ledger_signer( + hd_path: Option<&str>, + mnemonic_index: u32, +) -> Result { + let derivation = if let Some(hd_path) = hd_path { + LedgerHDPath::Other(hd_path.to_owned()) + } else { + LedgerHDPath::LedgerLive(mnemonic_index as usize) + }; + + WalletSigner::from_ledger_path(derivation).await.wrap_err_with(|| { + "\ +Could not connect to Ledger device. +Make sure it's connected and unlocked, with no other desktop wallet apps open." + }) +} + +/// Creates [WalletSigner] instance from given Trezor parameters. +pub async fn create_trezor_signer( + hd_path: Option<&str>, + mnemonic_index: u32, +) -> Result { + let derivation = if let Some(hd_path) = hd_path { + TrezorHDPath::Other(hd_path.to_owned()) + } else { + TrezorHDPath::TrezorLive(mnemonic_index as usize) + }; + + WalletSigner::from_trezor_path(derivation).await.wrap_err_with(|| { + "\ +Could not connect to Trezor device. +Make sure it's connected and unlocked, with no other conflicting desktop wallet apps open." + }) +} + +pub fn maybe_get_keystore_path( + maybe_path: Option<&str>, + maybe_name: Option<&str>, +) -> Result> { + let default_keystore_dir = Config::foundry_keystores_dir() + .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?; + Ok(maybe_path + .map(PathBuf::from) + .or_else(|| maybe_name.map(|name| default_keystore_dir.join(name)))) +} + +/// Creates keystore signer from given parameters. +/// +/// If correct password or password file is provided, the keystore is decrypted and a [WalletSigner] +/// is returned. +/// +/// Otherwise, a [PendingSigner] is returned, which can be used to unlock the keystore later, +/// prompting user for password. +pub fn create_keystore_signer( + path: &PathBuf, + maybe_password: Option<&str>, + maybe_password_file: Option<&str>, +) -> Result<(Option, Option)> { + if !path.exists() { + eyre::bail!("Keystore file `{path:?}` does not exist") + } + + if path.is_dir() { + eyre::bail!( + "Keystore path `{path:?}` is a directory. Please specify the keystore file directly." + ) + } + + let password = match (maybe_password, maybe_password_file) { + (Some(password), _) => Ok(Some(password.to_string())), + (_, Some(password_file)) => { + let password_file = Path::new(password_file); + if !password_file.is_file() { + Err(eyre::eyre!("Keystore password file `{password_file:?}` does not exist")) + } else { + Ok(Some( + fs::read_to_string(password_file) + .wrap_err_with(|| { + format!("Failed to read keystore password file at {password_file:?}") + })? + .trim_end() + .to_string(), + )) + } + } + (None, None) => Ok(None), + }?; + + if let Some(password) = password { + let wallet = LocalWallet::decrypt_keystore(path, password) + .wrap_err_with(|| format!("Failed to decrypt keystore {path:?}"))?; + Ok((Some(WalletSigner::Local(wallet)), None)) + } else { + Ok((None, Some(PendingSigner::Keystore(path.clone())))) + } +} diff --git a/crates/wallets/src/wallet.rs b/crates/wallets/src/wallet.rs index 640b7f99ce70..a15f805b93fc 100644 --- a/crates/wallets/src/wallet.rs +++ b/crates/wallets/src/wallet.rs @@ -1,31 +1,10 @@ -use crate::{ - error::{PrivateKeyError, WalletSignerError}, - raw_wallet::RawWallet, -}; -use alloy_primitives::{Address, B256}; -use async_trait::async_trait; +use crate::{raw_wallet::RawWalletOpts, utils, wallet_signer::WalletSigner}; +use alloy_primitives::Address; use clap::Parser; -use ethers_core::types::{ - transaction::{eip2718::TypedTransaction, eip712::Eip712}, - Signature, -}; -use ethers_signers::{ - coins_bip39::English, AwsSigner, HDPath as LedgerHDPath, Ledger, LocalWallet, MnemonicBuilder, - Signer, Trezor, TrezorHDPath, WalletError, -}; -use eyre::{bail, Result, WrapErr}; -use foundry_common::{fs, types::ToAlloy}; -use foundry_config::Config; -use rusoto_core::{ - credential::ChainProvider as AwsChainProvider, region::Region as AwsRegion, - request::HttpClient as AwsHttpClient, Client as AwsClient, -}; -use rusoto_kms::KmsClient; -use serde::{Deserialize, Serialize}; -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; +use ethers_signers::Signer; +use eyre::Result; +use foundry_common::types::ToAlloy; +use serde::Serialize; /// The wallet options can either be: /// 1. Raw (via private key / mnemonic file, see `RawWallet`) @@ -35,7 +14,7 @@ use std::{ /// 5. AWS KMS #[derive(Clone, Debug, Default, Serialize, Parser)] #[clap(next_help_heading = "Wallet options", about = None, long_about = None)] -pub struct Wallet { +pub struct WalletOpts { /// The sender account. #[clap( long, @@ -47,7 +26,7 @@ pub struct Wallet { pub from: Option
, #[clap(flatten)] - pub raw: RawWallet, + pub raw: RawWalletOpts, /// Use the keystore in the given folder or file. #[clap( @@ -104,118 +83,40 @@ pub struct Wallet { pub aws: bool, } -impl Wallet { - pub fn interactive(&self) -> Result> { - Ok(if self.raw.interactive { Some(self.get_from_interactive()?) } else { None }) - } - - pub fn private_key(&self) -> Result> { - Ok(if let Some(ref private_key) = self.raw.private_key { - Some(self.get_from_private_key(private_key)?) - } else { - None - }) - } - - pub fn keystore(&self) -> Result> { - let default_keystore_dir = Config::foundry_keystores_dir() - .ok_or_else(|| eyre::eyre!("Could not find the default keystore directory."))?; - // If keystore path is provided, use it, otherwise use default path + keystore account name - let keystore_path: Option = self.keystore_path.clone().or_else(|| { - self.keystore_account_name.as_ref().map(|keystore_name| { - default_keystore_dir.join(keystore_name).to_string_lossy().into_owned() - }) - }); - - self.get_from_keystore( - keystore_path.as_ref(), - self.keystore_password.as_ref(), - self.keystore_password_file.as_ref(), - ) - } - - pub fn mnemonic(&self) -> Result> { - Ok(if let Some(ref mnemonic) = self.raw.mnemonic { - Some(self.get_from_mnemonic( - mnemonic, - self.raw.mnemonic_passphrase.as_ref(), - self.raw.hd_path.as_ref(), - self.raw.mnemonic_index, - )?) - } else { - None - }) - } - - /// Returns the sender address of the signer or `from`. - pub async fn sender(&self) -> Address { - if let Ok(signer) = self.signer(0).await { - signer.address().to_alloy() - } else { - self.from.unwrap_or(Address::ZERO) - } - } - - /// Tries to resolve a local wallet from the provided options. - #[track_caller] - pub fn try_resolve_local_wallet(&self) -> Result> { - self.private_key() - .transpose() - .or_else(|| self.interactive().transpose()) - .or_else(|| self.mnemonic().transpose()) - .or_else(|| self.keystore().transpose()) - .transpose() - } - /// Returns a [Signer] corresponding to the provided private key, mnemonic or hardware signer. - #[instrument(skip(self), level = "trace")] - pub async fn signer(&self, chain_id: u64) -> Result { +impl WalletOpts { + pub async fn signer(&self) -> Result { trace!("start finding signer"); - if self.ledger { - let derivation = match self.raw.hd_path.as_ref() { - Some(hd_path) => LedgerHDPath::Other(hd_path.clone()), - None => LedgerHDPath::LedgerLive(self.raw.mnemonic_index as usize), - }; - let ledger = Ledger::new(derivation, chain_id).await.wrap_err_with(|| { - "\ -Could not connect to Ledger device. -Make sure it's connected and unlocked, with no other desktop wallet apps open." - })?; - - Ok(WalletSigner::Ledger(ledger)) + let signer = if self.ledger { + utils::create_ledger_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index) + .await? } else if self.trezor { - let derivation = match self.raw.hd_path.as_ref() { - Some(hd_path) => TrezorHDPath::Other(hd_path.clone()), - None => TrezorHDPath::TrezorLive(self.raw.mnemonic_index as usize), - }; - - // cached to ~/.ethers-rs/trezor/cache/trezor.session - let trezor = Trezor::new(derivation, chain_id, None).await.wrap_err_with(|| { - "\ -Could not connect to Trezor device. -Make sure it's connected and unlocked, with no other conflicting desktop wallet apps open." - })?; - - Ok(WalletSigner::Trezor(trezor)) + utils::create_trezor_signer(self.raw.hd_path.as_deref(), self.raw.mnemonic_index) + .await? } else if self.aws { - let client = - AwsClient::new_with(AwsChainProvider::default(), AwsHttpClient::new().unwrap()); - - let kms = KmsClient::new_with_client(client, AwsRegion::default()); - let key_id = std::env::var("AWS_KMS_KEY_ID")?; - - let aws_signer = AwsSigner::new(kms, key_id, chain_id).await?; - - Ok(WalletSigner::Aws(aws_signer)) + WalletSigner::from_aws(&key_id).await? + } else if let Some(raw_wallet) = self.raw.signer()? { + raw_wallet + } else if let Some(path) = utils::maybe_get_keystore_path( + self.keystore_path.as_deref(), + self.keystore_account_name.as_deref(), + )? { + let (maybe_signer, maybe_pending) = utils::create_keystore_signer( + &path, + self.keystore_password.as_deref(), + self.keystore_password_file.as_deref(), + )?; + if let Some(pending) = maybe_pending { + pending.unlock()? + } else if let Some(signer) = maybe_signer { + signer + } else { + unreachable!() + } } else { - trace!("finding local key"); - - let maybe_local = self.try_resolve_local_wallet()?; - - let local = maybe_local.ok_or_else(|| { - eyre::eyre!( - "\ + eyre::bail!( + "\ Error accessing local wallet. Did you set a private key, mnemonic or keystore? Run `cast send --help` or `forge create --help` and use the corresponding CLI flag to set your key via: @@ -223,282 +124,61 @@ flag to set your key via: Alternatively, if you're using a local node with unlocked accounts, use the --unlocked flag and either set the `ETH_FROM` environment variable to the address of the unlocked account you want to use, or provide the --from flag with the address directly." - ) - })?; - - Ok(WalletSigner::Local(local.with_chain_id(chain_id))) - } - } -} - -pub trait WalletTrait { - /// Returns the configured sender. - fn sender(&self) -> Option
; - - fn get_from_interactive(&self) -> Result { - let private_key = rpassword::prompt_password("Enter private key: ")?; - let private_key = private_key.strip_prefix("0x").unwrap_or(&private_key); - Ok(LocalWallet::from_str(private_key)?) - } - - #[track_caller] - fn get_from_private_key(&self, private_key: &str) -> Result { - let privk = private_key.trim().strip_prefix("0x").unwrap_or(private_key); - match LocalWallet::from_str(privk) { - Ok(pk) => Ok(pk), - Err(err) => { - // helper closure to check if pk was meant to be an env var, this usually happens if - // `$` is missing - let ensure_not_env = |pk: &str| { - // check if pk was meant to be an env var - if !pk.starts_with("0x") && std::env::var(pk).is_ok() { - // SAFETY: at this point we know the user actually wanted to use an env var - // and most likely forgot the `$` anchor, so the - // `private_key` here is an unresolved env var - return Err(PrivateKeyError::ExistsAsEnvVar(pk.to_string())) - } - Ok(()) - }; - match err { - WalletError::HexError(err) => { - ensure_not_env(private_key)?; - return Err(PrivateKeyError::InvalidHex(err).into()) - } - WalletError::EcdsaError(_) => { - ensure_not_env(private_key)?; - } - _ => {} - }; - bail!("Failed to create wallet from private key: {err}") - } - } - } - - fn get_from_mnemonic( - &self, - mnemonic: &String, - passphrase: Option<&String>, - derivation_path: Option<&String>, - index: u32, - ) -> Result { - let mnemonic = if Path::new(mnemonic).is_file() { - fs::read_to_string(mnemonic)?.replace('\n', "") - } else { - mnemonic.to_owned() - }; - let builder = MnemonicBuilder::::default().phrase(mnemonic.as_str()); - let builder = if let Some(passphrase) = passphrase { - builder.password(passphrase.as_str()) - } else { - builder - }; - let builder = if let Some(hd_path) = derivation_path { - builder.derivation_path(hd_path.as_str())? - } else { - builder.index(index)? + ) }; - Ok(builder.build()?) - } - - /// Ensures the path to the keystore exists. - /// - /// if the path is a directory, it bails and asks the user to specify the keystore file - /// directly. - fn find_keystore_file(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - if !path.exists() { - bail!("Keystore file `{path:?}` does not exist") - } - - if path.is_dir() { - bail!("Keystore path `{path:?}` is a directory. Please specify the keystore file directly.") - } - Ok(path.to_path_buf()) + Ok(signer) } - fn get_from_keystore( - &self, - keystore_path: Option<&String>, - keystore_password: Option<&String>, - keystore_password_file: Option<&String>, - ) -> Result> { - Ok(match (keystore_path, keystore_password, keystore_password_file) { - // Path and password provided - (Some(path), Some(password), _) => { - let path = self.find_keystore_file(path)?; - Some( - LocalWallet::decrypt_keystore(&path, password) - .wrap_err_with(|| format!("Failed to decrypt keystore {path:?}"))?, - ) - } - // Path and password file provided - (Some(path), _, Some(password_file)) => { - let path = self.find_keystore_file(path)?; - Some( - LocalWallet::decrypt_keystore(&path, self.password_from_file(password_file)?) - .wrap_err_with(|| format!("Failed to decrypt keystore {path:?} with password file {password_file:?}"))?, - ) - } - // Only Path provided -> interactive - (Some(path), None, None) => { - let path = self.find_keystore_file(path)?; - let password = rpassword::prompt_password("Enter keystore password:")?; - Some(LocalWallet::decrypt_keystore(path, password)?) - } - // Nothing provided - (None, _, _) => None, - }) - } - - /// Attempts to read the keystore password from the password file. - fn password_from_file(&self, password_file: impl AsRef) -> Result { - let password_file = password_file.as_ref(); - if !password_file.is_file() { - bail!("Keystore password file `{password_file:?}` does not exist") + /// Returns the sender address of the signer or `from`. + pub async fn sender(&self) -> Address { + if let Ok(signer) = self.signer().await { + signer.address().to_alloy() + } else { + self.from.unwrap_or(Address::ZERO) } - - Ok(fs::read_to_string(password_file)?.trim_end().to_string()) } } -impl From for Wallet { - fn from(options: RawWallet) -> Self { +impl From for WalletOpts { + fn from(options: RawWalletOpts) -> Self { Self { raw: options, ..Default::default() } } } -impl WalletTrait for Wallet { - fn sender(&self) -> Option
{ - self.from - } -} - -#[derive(Debug)] -pub enum WalletSigner { - Local(LocalWallet), - Ledger(Ledger), - Trezor(Trezor), - Aws(AwsSigner), -} - -impl From for WalletSigner { - fn from(wallet: LocalWallet) -> Self { - Self::Local(wallet) - } -} - -impl From for WalletSigner { - fn from(hw: Ledger) -> Self { - Self::Ledger(hw) - } -} - -impl From for WalletSigner { - fn from(hw: Trezor) -> Self { - Self::Trezor(hw) - } -} - -impl From for WalletSigner { - fn from(wallet: AwsSigner) -> Self { - Self::Aws(wallet) - } -} - -macro_rules! delegate { - ($s:ident, $inner:ident => $e:expr) => { - match $s { - Self::Local($inner) => $e, - Self::Ledger($inner) => $e, - Self::Trezor($inner) => $e, - Self::Aws($inner) => $e, - } - }; -} - -#[async_trait] -impl Signer for WalletSigner { - type Error = WalletSignerError; - - async fn sign_message>( - &self, - message: S, - ) -> Result { - delegate!(self, inner => inner.sign_message(message).await.map_err(Into::into)) - } - - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { - delegate!(self, inner => inner.sign_transaction(message).await.map_err(Into::into)) - } - - async fn sign_typed_data( - &self, - payload: &T, - ) -> Result { - delegate!(self, inner => inner.sign_typed_data(payload).await.map_err(Into::into)) - } - - fn address(&self) -> ethers_core::types::Address { - delegate!(self, inner => inner.address()) - } - - fn chain_id(&self) -> u64 { - delegate!(self, inner => inner.chain_id()) - } - - fn with_chain_id>(self, chain_id: T) -> Self { - match self { - Self::Local(inner) => Self::Local(inner.with_chain_id(chain_id)), - Self::Ledger(inner) => Self::Ledger(inner.with_chain_id(chain_id)), - Self::Trezor(inner) => Self::Trezor(inner.with_chain_id(chain_id)), - Self::Aws(inner) => Self::Aws(inner.with_chain_id(chain_id)), - } - } -} - -impl WalletSigner { - pub async fn sign_hash(&self, hash: &B256) -> Result { - match self { - // TODO: AWS can sign hashes but utilities aren't exposed in ethers-signers. - // TODO: Implement with alloy-signer. - Self::Aws(_aws) => Err(WalletSignerError::CannotSignRawHash("AWS")), - Self::Ledger(_) => Err(WalletSignerError::CannotSignRawHash("Ledger")), - Self::Local(wallet) => wallet.sign_hash(hash.0.into()).map_err(Into::into), - Self::Trezor(_) => Err(WalletSignerError::CannotSignRawHash("Trezor")), - } - } -} - -/// Excerpt of a keystore file. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KeystoreFile { - pub address: Address, -} - #[cfg(test)] mod tests { + use std::{path::Path, str::FromStr}; + use super::*; - #[test] - fn find_keystore() { + #[tokio::test] + async fn find_keystore() { let keystore = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore")); let keystore_file = keystore - .join("UTC--2022-10-30T06-51-20.130356000Z--560d246fcddc9ea98a8b032c9a2f474efb493c28"); - let wallet: Wallet = Wallet::parse_from([ + .join("UTC--2022-12-20T10-30-43.591916000Z--ec554aeafe75601aaab43bd4621a22284db566c2"); + let password_file = keystore.join("password-ec554"); + let wallet: WalletOpts = WalletOpts::parse_from([ "foundry-cli", "--from", "560d246fcddc9ea98a8b032c9a2f474efb493c28", + "--keystore", + keystore_file.to_str().unwrap(), + "--password-file", + password_file.to_str().unwrap(), ]); - let file = wallet.find_keystore_file(&keystore_file).unwrap(); - assert_eq!(file, keystore_file); + let signer = wallet.signer().await.unwrap(); + assert_eq!( + signer.address().to_alloy(), + Address::from_str("ec554aeafe75601aaab43bd4621a22284db566c2").unwrap() + ); } - #[test] - fn illformed_private_key_generates_user_friendly_error() { - let wallet = Wallet { - raw: RawWallet { + #[tokio::test] + async fn illformed_private_key_generates_user_friendly_error() { + let wallet = WalletOpts { + raw: RawWalletOpts { interactive: false, private_key: Some("123".to_string()), mnemonic: None, @@ -515,7 +195,7 @@ mod tests { trezor: false, aws: false, }; - match wallet.private_key() { + match wallet.signer().await { Ok(_) => { panic!("illformed private key shouldn't decode") } @@ -527,12 +207,4 @@ mod tests { } } } - - #[test] - fn gets_password_from_file() { - let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../cast/tests/fixtures/keystore/password"); - let wallet: Wallet = Wallet::parse_from(["foundry-cli"]); - let password = wallet.password_from_file(path).unwrap(); - assert_eq!(password, "this is keystore password") - } } diff --git a/crates/wallets/src/wallet_signer.rs b/crates/wallets/src/wallet_signer.rs new file mode 100644 index 000000000000..ed7b99a0c576 --- /dev/null +++ b/crates/wallets/src/wallet_signer.rs @@ -0,0 +1,190 @@ +use crate::error::WalletSignerError; +use alloy_primitives::B256; +use async_trait::async_trait; +use ethers_core::types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Signature, +}; +use ethers_signers::{ + coins_bip39::English, AwsSigner, HDPath as LedgerHDPath, Ledger, LocalWallet, MnemonicBuilder, + Signer, Trezor, TrezorHDPath, +}; +use rusoto_core::{ + credential::ChainProvider as AwsChainProvider, region::Region as AwsRegion, + request::HttpClient as AwsHttpClient, Client as AwsClient, +}; +use rusoto_kms::KmsClient; +use std::path::PathBuf; + +pub type Result = std::result::Result; + +/// Wrapper enum around different signers. +#[derive(Debug)] +pub enum WalletSigner { + /// Wrapper around local wallet. e.g. private key, mnemonic + Local(LocalWallet), + /// Wrapper around Ledger signer. + Ledger(Ledger), + /// Wrapper around Trezor signer. + Trezor(Trezor), + /// Wrapper around AWS KMS signer. + Aws(AwsSigner), +} + +impl WalletSigner { + pub async fn from_ledger_path(path: LedgerHDPath) -> Result { + let ledger = Ledger::new(path, 1).await?; + Ok(Self::Ledger(ledger)) + } + + pub async fn from_trezor_path(path: TrezorHDPath) -> Result { + // cached to ~/.ethers-rs/trezor/cache/trezor.session + let trezor = Trezor::new(path, 1, None).await?; + Ok(Self::Trezor(trezor)) + } + + pub async fn from_aws(key_id: &str) -> Result { + let client = + AwsClient::new_with(AwsChainProvider::default(), AwsHttpClient::new().unwrap()); + + let kms = KmsClient::new_with_client(client, AwsRegion::default()); + + Ok(Self::Aws(AwsSigner::new(kms, key_id, 1).await?)) + } + + pub fn from_private_key(private_key: impl AsRef<[u8]>) -> Result { + let wallet = LocalWallet::from_bytes(private_key.as_ref())?; + Ok(Self::Local(wallet)) + } + + pub fn from_mnemonic( + mnemonic: &str, + passphrase: Option<&str>, + derivation_path: Option<&str>, + index: u32, + ) -> Result { + let mut builder = MnemonicBuilder::::default().phrase(mnemonic); + + if let Some(passphrase) = passphrase { + builder = builder.password(passphrase) + } + + builder = if let Some(hd_path) = derivation_path { + builder.derivation_path(hd_path)? + } else { + builder.index(index)? + }; + + Ok(Self::Local(builder.build()?)) + } +} + +macro_rules! delegate { + ($s:ident, $inner:ident => $e:expr) => { + match $s { + Self::Local($inner) => $e, + Self::Ledger($inner) => $e, + Self::Trezor($inner) => $e, + Self::Aws($inner) => $e, + } + }; +} + +#[async_trait] +impl Signer for WalletSigner { + type Error = WalletSignerError; + + async fn sign_message>(&self, message: S) -> Result { + delegate!(self, inner => inner.sign_message(message).await.map_err(Into::into)) + } + + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + delegate!(self, inner => inner.sign_transaction(message).await.map_err(Into::into)) + } + + async fn sign_typed_data(&self, payload: &T) -> Result { + delegate!(self, inner => inner.sign_typed_data(payload).await.map_err(Into::into)) + } + + fn address(&self) -> ethers_core::types::Address { + delegate!(self, inner => inner.address()) + } + + fn chain_id(&self) -> u64 { + delegate!(self, inner => inner.chain_id()) + } + + fn with_chain_id>(self, chain_id: T) -> Self { + match self { + Self::Local(inner) => Self::Local(inner.with_chain_id(chain_id)), + Self::Ledger(inner) => Self::Ledger(inner.with_chain_id(chain_id)), + Self::Trezor(inner) => Self::Trezor(inner.with_chain_id(chain_id)), + Self::Aws(inner) => Self::Aws(inner.with_chain_id(chain_id)), + } + } +} + +#[async_trait] +impl Signer for &WalletSigner { + type Error = WalletSignerError; + + async fn sign_message>(&self, message: S) -> Result { + (*self).sign_message(message).await + } + + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + (*self).sign_transaction(message).await + } + + async fn sign_typed_data(&self, payload: &T) -> Result { + (*self).sign_typed_data(payload).await + } + + fn address(&self) -> ethers_core::types::Address { + (*self).address() + } + + fn chain_id(&self) -> u64 { + (*self).chain_id() + } + + fn with_chain_id>(self, chain_id: T) -> Self { + let _ = chain_id; + self + } +} + +impl WalletSigner { + pub async fn sign_hash(&self, hash: &B256) -> Result { + match self { + // TODO: AWS can sign hashes but utilities aren't exposed in ethers-signers. + // TODO: Implement with alloy-signer. + Self::Aws(_aws) => Err(WalletSignerError::CannotSignRawHash("AWS")), + Self::Ledger(_) => Err(WalletSignerError::CannotSignRawHash("Ledger")), + Self::Local(wallet) => wallet.sign_hash(hash.0.into()).map_err(Into::into), + Self::Trezor(_) => Err(WalletSignerError::CannotSignRawHash("Trezor")), + } + } +} + +/// Signers that require user action to be obtained. +#[derive(Debug, Clone)] +pub enum PendingSigner { + Keystore(PathBuf), + Interactive, +} + +impl PendingSigner { + pub fn unlock(self) -> Result { + match self { + Self::Keystore(path) => { + let password = rpassword::prompt_password("Enter keystore password:")?; + Ok(WalletSigner::Local(LocalWallet::decrypt_keystore(path, password)?)) + } + Self::Interactive => { + let private_key = rpassword::prompt_password("Enter private key:")?; + Ok(WalletSigner::from_private_key(hex::decode(private_key)?)?) + } + } + } +}