From 620541f1e092dddd9881a5d2c1523ddbace2d32a Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:28:01 +0200 Subject: [PATCH 1/2] feat(common): `FoundryTransactionBuilder::sign_with_access_key` method --- Cargo.lock | 4 +- crates/cast/src/cmd/batch_mktx.rs | 3 +- crates/cast/src/cmd/batch_send.rs | 3 +- crates/cast/src/cmd/erc20.rs | 3 +- crates/cast/src/cmd/send.rs | 2 +- crates/cast/src/tempo.rs | 2 - crates/common/Cargo.toml | 1 + crates/common/src/transactions/builder.rs | 90 ++++++++++++++++++++++- crates/script/Cargo.toml | 2 +- crates/script/src/broadcast.rs | 58 +++++---------- crates/wallets/Cargo.toml | 2 - crates/wallets/src/tempo.rs | 38 +--------- 12 files changed, 115 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97ac0ef414066..f65e395c4fa39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4690,7 +4690,6 @@ dependencies = [ "serde", "serde_json", "tempfile", - "tempo-alloy", "thiserror 2.0.18", "tokio", "tracing", @@ -4949,6 +4948,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-client", "alloy-rpc-types", + "alloy-signer", "alloy-sol-types", "alloy-transport", "alloy-transport-http", @@ -5493,7 +5493,6 @@ version = "1.6.0" dependencies = [ "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.0-rc.0", "alloy-network", "alloy-primitives", "alloy-rlp", @@ -5519,7 +5518,6 @@ dependencies = [ "rpassword", "serde", "serde_json", - "tempo-alloy", "tempo-primitives", "thiserror 2.0.18", "tokio", diff --git a/crates/cast/src/cmd/batch_mktx.rs b/crates/cast/src/cmd/batch_mktx.rs index f495fcf9220a8..a3d05d032676b 100644 --- a/crates/cast/src/cmd/batch_mktx.rs +++ b/crates/cast/src/cmd/batch_mktx.rs @@ -5,7 +5,6 @@ use crate::{ call_spec::CallSpec, - tempo::sign_with_access_key, tx::{self, CastTxBuilder}, }; use alloy_consensus::SignableTransaction; @@ -164,7 +163,7 @@ impl BatchMakeTxArgs { }; let signed_tx = if let Some(ref access_key) = tempo_access_key { - let raw_tx = sign_with_access_key(tx, &signer, access_key.wallet_address).await?; + let raw_tx = tx.sign_with_access_key(&signer, access_key.wallet_address).await?; alloy_primitives::hex::encode(raw_tx) } else { let envelope = tx.build(&EthereumWallet::new(signer)).await?; diff --git a/crates/cast/src/cmd/batch_send.rs b/crates/cast/src/cmd/batch_send.rs index 7655145973b7b..a683a768d0b3c 100644 --- a/crates/cast/src/cmd/batch_send.rs +++ b/crates/cast/src/cmd/batch_send.rs @@ -7,7 +7,6 @@ use crate::{ call_spec::CallSpec, cmd::send::cast_send, - tempo::sign_with_access_key, tx::{self, CastTxBuilder, CastTxSender, SendTxOpts}, }; use alloy_network::{EthereumWallet, TransactionBuilder}; @@ -160,7 +159,7 @@ impl BatchSendArgs { if let Some(ref access_key) = tempo_access_key { let raw_tx = - sign_with_access_key(tx_request, &signer, access_key.wallet_address).await?; + tx_request.sign_with_access_key(&signer, access_key.wallet_address).await?; let cast = CastTxSender::new(&provider); let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); diff --git a/crates/cast/src/cmd/erc20.rs b/crates/cast/src/cmd/erc20.rs index 166729b6ecbb6..aa327823c4525 100644 --- a/crates/cast/src/cmd/erc20.rs +++ b/crates/cast/src/cmd/erc20.rs @@ -594,8 +594,7 @@ async fn send_tempo_keychain>( tx.set_key_authorization(auth.clone()); } - let raw_tx = - foundry_wallets::tempo::sign_with_access_key(tx, signer, access_key.wallet_address).await?; + let raw_tx = tx.sign_with_access_key(signer, access_key.wallet_address).await?; let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index f6cdb2a3f2c98..c0d9c263f61c8 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -162,7 +162,7 @@ impl SendTxArgs { tx_request.set_key_authorization(auth); } - let raw_tx = crate::tempo::sign_with_access_key(tx_request, &signer, from).await?; + let raw_tx = tx_request.sign_with_access_key(&signer, from).await?; let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); diff --git a/crates/cast/src/tempo.rs b/crates/cast/src/tempo.rs index 3c64491aaea63..b5a7444b5d37d 100644 --- a/crates/cast/src/tempo.rs +++ b/crates/cast/src/tempo.rs @@ -2,8 +2,6 @@ use alloy_primitives::Address; use alloy_provider::Provider; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; -pub use foundry_wallets::tempo::sign_with_access_key; - /// Checks whether an access key is already provisioned on-chain. /// /// Queries the AccountKeychain precompile's `getKey` function. A key is considered diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 4d57de7dac31a..1486b8ef43fb2 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -30,6 +30,7 @@ alloy-primitives = { workspace = true, features = [ "rlp", ] } alloy-provider.workspace = true +alloy-signer.workspace = true alloy-pubsub.workspace = true alloy-rpc-client.workspace = true alloy-rpc-types = { workspace = true, features = ["eth", "engine"] } diff --git a/crates/common/src/transactions/builder.rs b/crates/common/src/transactions/builder.rs index 5f0fd7b98c2e4..3b09012211dbe 100644 --- a/crates/common/src/transactions/builder.rs +++ b/crates/common/src/transactions/builder.rs @@ -1,11 +1,17 @@ use alloy_consensus::{ BlobTransactionSidecar, BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant, }; -use alloy_eips::eip7702::SignedAuthorization; +use alloy_eips::{Encodable2718, eip7702::SignedAuthorization}; use alloy_network::{AnyNetwork, Ethereum, Network, TransactionBuilder}; use alloy_primitives::{Address, B256, Signature, U256}; -use tempo_alloy::TempoNetwork; -use tempo_primitives::transaction::SignedKeyAuthorization; +use alloy_provider::Provider; +use alloy_signer::Signer; +use eyre::Result; +use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; +use tempo_primitives::{ + TempoSignature, + transaction::{KeychainSignature, PrimitiveSignature, SignedKeyAuthorization}, +}; /// Composite transaction builder trait for Foundry transactions. /// @@ -233,6 +239,37 @@ pub trait FoundryTransactionBuilder: TransactionBuilder { /// Embeds a [`SignedKeyAuthorization`] in the transaction body, provisioning the access key /// on-chain as part of this transaction. fn set_key_authorization(&mut self, _key_authorization: SignedKeyAuthorization) {} + + /// Signs the transaction using an access key (keychain mode). + /// + /// Builds the transaction, computes the keychain signing hash, signs with the access + /// key, and returns the EIP-2718 encoded raw transaction bytes. + /// + /// The default implementation returns an error. Only `TempoNetwork` supports this. + fn sign_with_access_key( + self, + _signer: &(impl Signer + Sync), + _wallet_address: Address, + ) -> impl Future>> + Send { + async { eyre::bail!("access key signing is not supported for this network") } + } + + /// Signs the transaction using an access key, checking on-chain provisioning first. + /// + /// If `key_authorization` is provided and the key is not yet provisioned on-chain, + /// embeds the authorization in the transaction before signing. + /// + /// The default implementation returns an error. Only `TempoNetwork` supports this. + fn sign_with_access_key_provisioning( + self, + _provider: &impl Provider, + _signer: &(impl Signer + Sync), + _wallet_address: Address, + _key_address: Address, + _key_authorization: Option<&SignedKeyAuthorization>, + ) -> impl Future>> + Send { + async { eyre::bail!("access key signing is not supported for this network") } + } } impl FoundryTransactionBuilder for ::TransactionRequest { @@ -382,4 +419,51 @@ impl FoundryTransactionBuilder for ::Tran fn set_key_authorization(&mut self, key_authorization: SignedKeyAuthorization) { self.key_authorization = Some(key_authorization); } + + async fn sign_with_access_key( + self, + signer: &(impl Signer + Sync), + wallet_address: Address, + ) -> Result> { + let tempo_tx = self + .build_aa() + .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?; + + let sig_hash = tempo_tx.signature_hash(); + let signing_hash = KeychainSignature::signing_hash(sig_hash, wallet_address); + let raw_sig = signer.sign_hash(&signing_hash).await?; + + let keychain_sig = + KeychainSignature::new(wallet_address, PrimitiveSignature::Secp256k1(raw_sig)); + let aa_signed = tempo_tx.into_signed(TempoSignature::Keychain(keychain_sig)); + + let mut buf = Vec::new(); + aa_signed.encode_2718(&mut buf); + Ok(buf) + } + + fn sign_with_access_key_provisioning( + mut self, + provider: &impl Provider, + signer: &(impl Signer + Sync), + wallet_address: Address, + key_address: Address, + key_authorization: Option<&SignedKeyAuthorization>, + ) -> impl Future>> + Send { + let auth = key_authorization.cloned(); + let provisioning_fut = provider.get_keychain_key(wallet_address, key_address); + + async move { + if let Some(auth) = auth { + let is_provisioned = + provisioning_fut.await.map(|info| info.keyId != Address::ZERO).unwrap_or(false); + + if !is_provisioned { + self.set_key_authorization(auth); + } + } + + self.sign_with_access_key(signer, wallet_address).await + } + } } diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index d88e13ab58ed3..1d403ee05ebdc 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -55,7 +55,7 @@ alloy-primitives.workspace = true alloy-eips.workspace = true alloy-consensus.workspace = true thiserror.workspace = true -tempo-alloy.workspace = true + [dev-dependencies] tempfile.workspace = true diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 2139fa2ae6e23..4a971041922cf 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -1,5 +1,9 @@ use std::{cmp::Ordering, sync::Arc, time::Duration}; +use crate::{ + ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress, + sequence::ScriptSequenceKind, verify::BroadcastedState, +}; use alloy_chains::{Chain, NamedChain}; use alloy_consensus::{SignableTransaction, Signed}; use alloy_eips::{BlockId, eip2718::Encodable2718}; @@ -26,12 +30,6 @@ use foundry_wallets::{TempoAccessKeyConfig, WalletSigner, wallet_browser::signer use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use tempo_alloy::provider::TempoProviderExt; - -use crate::{ - ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress, - sequence::ScriptSequenceKind, verify::BroadcastedState, -}; pub async fn estimate_gas>( tx: &mut N::TransactionRequest, @@ -72,15 +70,14 @@ pub enum SendTransactionKind<'a, N: Network> { Raw(N::TransactionRequest, &'a EthereumWallet), Browser(N::TransactionRequest, &'a BrowserSigner), Signed(N::TxEnvelope), - AccessKey(N::TransactionRequest, &'a WalletSigner, &'a TempoAccessKeyConfig, String), + AccessKey(N::TransactionRequest, &'a WalletSigner, &'a TempoAccessKeyConfig), } impl<'a, N: Network> SendTransactionKind<'a, N> where N::TxEnvelope: From>, N::UnsignedTx: SignableTransaction, - N::TransactionRequest: - FoundryTransactionBuilder + Into, + N::TransactionRequest: FoundryTransactionBuilder, { /// Prepares the transaction for broadcasting by synchronizing nonce and estimating gas. /// @@ -99,7 +96,7 @@ where if let Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) - | Self::AccessKey(tx, _, _, _) = self + | Self::AccessKey(tx, _, _) = self { if sequential_broadcast { let from = tx.from().expect("no sender"); @@ -176,31 +173,18 @@ where // Sign and send the transaction via the browser wallet Ok(signer.send_transaction_via_browser(tx).await?) } - Self::AccessKey(mut tx, signer, access_key, rpc_url) => { + Self::AccessKey(tx, signer, access_key) => { debug!("sending transaction via tempo access key: {:?}", tx); - // Check if the key needs on-chain provisioning. - if let Some(auth) = &access_key.key_authorization { - let tempo_provider = foundry_common::provider::ProviderBuilder::< - tempo_alloy::TempoNetwork, - >::new(&rpc_url) - .build()?; - if !tempo_provider - .get_keychain_key(access_key.wallet_address, access_key.key_address) - .await - .map(|info| info.keyId != Address::ZERO) - .unwrap_or(false) - { - tx.set_key_authorization(auth.clone()); - } - } - - let raw_tx = foundry_wallets::tempo::sign_with_access_key( - tx, - signer, - access_key.wallet_address, - ) - .await?; + let raw_tx = tx + .sign_with_access_key_provisioning( + provider.as_ref(), + signer, + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.as_ref(), + ) + .await?; let pending = provider.send_raw_transaction(&raw_tx).await?; Ok(*pending.tx_hash()) @@ -253,7 +237,6 @@ impl SendTransactionsKind { &self, addr: &Address, tx: N::TransactionRequest, - rpc_url: &str, ) -> Result> { match self { Self::Unlocked(unlocked) => { @@ -264,7 +247,7 @@ impl SendTransactionsKind { } Self::Raw { eth_wallets, browser, access_keys } => { if let Some((signer, config)) = access_keys.get(addr) { - Ok(SendTransactionKind::AccessKey(tx, signer, config, rpc_url.to_string())) + Ok(SendTransactionKind::AccessKey(tx, signer, config)) } else if let Some(wallet) = eth_wallets.get(addr) { Ok(SendTransactionKind::Raw(tx, wallet)) } else if let Some(b) = browser @@ -338,8 +321,7 @@ where where N::TxEnvelope: From>, N::UnsignedTx: SignableTransaction, - N::TransactionRequest: - FoundryTransactionBuilder + Into, + N::TransactionRequest: FoundryTransactionBuilder, { let required_addresses = self .sequence @@ -503,7 +485,7 @@ where tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); } - send_kind.for_sender(&from, tx, sequence.rpc_url())? + send_kind.for_sender(&from, tx)? } }; diff --git a/crates/wallets/Cargo.toml b/crates/wallets/Cargo.toml index 381d8b2c600f7..0f30346c72039 100644 --- a/crates/wallets/Cargo.toml +++ b/crates/wallets/Cargo.toml @@ -44,8 +44,6 @@ alloy-signer-gcp = { workspace = true, features = ["eip712"], optional = true } alloy-signer-turnkey = { workspace = true, features = ["eip712"], optional = true } tempo-primitives.workspace = true -tempo-alloy.workspace = true -alloy-eips.workspace = true alloy-rlp.workspace = true async-trait.workspace = true diff --git a/crates/wallets/src/tempo.rs b/crates/wallets/src/tempo.rs index a86b568fdea2b..e0a05d1212eae 100644 --- a/crates/wallets/src/tempo.rs +++ b/crates/wallets/src/tempo.rs @@ -1,13 +1,8 @@ -use alloy_eips::Encodable2718; use alloy_primitives::{Address, hex}; use alloy_rlp::Decodable; -use alloy_signer::Signer; use eyre::Result; use std::path::PathBuf; -use tempo_alloy::rpc::TempoTransactionRequest; -use tempo_primitives::transaction::{ - KeychainSignature, PrimitiveSignature, SignedKeyAuthorization, TempoSignature, -}; +use tempo_primitives::transaction::SignedKeyAuthorization; use crate::{WalletSigner, utils}; @@ -163,34 +158,3 @@ pub fn lookup_signer(from: Address) -> Result { Ok(TempoLookup::NotFound) } - -/// Signs a Tempo transaction request using an access key (keychain V2 mode). -/// -/// Bypasses the standard `EthereumWallet` signing path and instead: -/// 1. Builds the `TempoTransaction` from the request -/// 2. Computes the V2 keychain signing hash -/// 3. Signs with the access key -/// 4. Wraps in a `KeychainSignature` and encodes to EIP-2718 wire format -pub async fn sign_with_access_key( - tx_request: impl Into, - signer: &impl Signer, - wallet_address: Address, -) -> Result> { - let tx_request: TempoTransactionRequest = tx_request.into(); - let tempo_tx = tx_request - .build_aa() - .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?; - - let sig_hash = tempo_tx.signature_hash(); - let signing_hash = KeychainSignature::signing_hash(sig_hash, wallet_address); - let raw_sig = signer.sign_hash(&signing_hash).await?; - - let keychain_sig = - KeychainSignature::new(wallet_address, PrimitiveSignature::Secp256k1(raw_sig)); - let aa_signed = tempo_tx.into_signed(TempoSignature::Keychain(keychain_sig)); - - let mut buf = Vec::new(); - aa_signed.encode_2718(&mut buf); - - Ok(buf) -} From 6c39f20065e99b116ea17a514e5eb195f28a0b90 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:08:35 +0200 Subject: [PATCH 2/2] refactor(cast): fold `run_tempo_keychain` into `run_generic` as access-key case Delete the standalone `run_tempo_keychain` method and handle Tempo access key signing as Case 3 inside `run_generic::`. - `run_generic` now accepts `access_key: Option` - `run()` routes both keychain and non-keychain Tempo flows through `run_generic::`, eliminating the early dispatch branch - `key_id` is injected into `tx.tempo` before builder construction so gas estimation sees the correct access key address - Keychain signing uses `sign_with_access_key_provisioning`, which encapsulates the provisioning check and `key_authorization` embedding that were previously done manually in `run_tempo_keychain` --- crates/cast/src/cmd/send.rs | 117 +++++++++++------------------------- 1 file changed, 36 insertions(+), 81 deletions(-) diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index c0d9c263f61c8..c0f9ea5c64161 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -87,93 +87,17 @@ impl SendTxArgs { // Resolve the signer early so we know if it's a Tempo access key. let (signer, tempo_access_key) = self.send_tx.eth.wallet.maybe_signer().await?; - if let Some(tempo_access_key) = tempo_access_key { - // Tempo keychain mode: always uses TempoNetwork. - self.run_tempo_keychain( - signer.expect("signer required for access key"), - tempo_access_key, - ) - .await - } else if self.tx.tempo.is_tempo() { - self.run_generic::(signer).await + if tempo_access_key.is_some() || self.tx.tempo.is_tempo() { + self.run_generic::(signer, tempo_access_key).await } else { - self.run_generic::(signer).await + self.run_generic::(signer, None).await } } - /// Handles Tempo access key (keychain mode) transactions. - /// - /// Bypasses `EthereumWallet` and manually constructs a `KeychainSignature`, - /// then sends the raw transaction. - async fn run_tempo_keychain( - self, - signer: WalletSigner, - access_key: TempoAccessKeyConfig, - ) -> Result<()> { - let Self { to, mut sig, mut args, data, send_tx, mut tx, command, unlocked: _, path } = - self; - - let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; - - if let Some(data) = data { - sig = Some(data); - } - - let code = if let Some(SendTxSubcommands::Create { - code, - sig: constructor_sig, - args: constructor_args, - }) = command - { - sig = constructor_sig; - args = constructor_args; - Some(code) - } else { - None - }; - - // Inject access key ID into TempoOpts so it's set before gas estimation. - tx.tempo.key_id = Some(access_key.key_address); - - let config = send_tx.eth.load_config()?; - let provider = ProviderBuilder::::from_config(&config)?.build()?; - - if let Some(interval) = send_tx.poll_interval { - provider.client().set_poll_interval(Duration::from_secs(interval)) - } - - let builder = CastTxBuilder::new(&provider, tx, &config) - .await? - .with_to(to) - .await? - .with_code_sig_and_args(code, sig, args) - .await? - .with_blob_data(blob_data)?; - - let from = access_key.wallet_address; - - // Build using wallet address for correct nonce/gas estimation. - let (mut tx_request, _) = builder.build(from).await?; - - // Only include key_authorization if the key is not yet provisioned on-chain. - if let Some(auth) = access_key.key_authorization - && !crate::tempo::is_key_provisioned(&provider, from, access_key.key_address).await - { - tx_request.set_key_authorization(auth); - } - - let raw_tx = tx_request.sign_with_access_key(&signer, from).await?; - - let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); - let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); - - let cast = CastTxSender::new(&provider); - cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await - } - pub async fn run_generic( self, pre_resolved_signer: Option, + access_key: Option, ) -> Result<()> where N::TxEnvelope: From>, @@ -181,7 +105,7 @@ impl SendTxArgs { N::TransactionRequest: FoundryTransactionBuilder, N::ReceiptResponse: UIfmt + UIfmtReceiptExt, { - let Self { to, mut sig, mut args, data, send_tx, tx, command, unlocked, path } = self; + let Self { to, mut sig, mut args, data, send_tx, mut tx, command, unlocked, path } = self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; @@ -226,6 +150,11 @@ impl SendTxArgs { provider.client().set_poll_interval(Duration::from_secs(interval)) } + // Inject access key ID into TempoOpts so it's set before gas estimation. + if let Some(ref ak) = access_key { + tx.tempo.key_id = Some(ak.key_address); + } + let builder = CastTxBuilder::new(&provider, tx, &config) .await? .with_to(to) @@ -294,6 +223,32 @@ impl SendTxArgs { let cast = CastTxSender::new(&provider); cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await // Case 3: + // Tempo access key (keychain) signing. Uses `sign_with_access_key_provisioning` which + // handles the provisioning check and embeds `key_authorization` when needed. + } else if let Some(ak) = access_key { + let signer = match pre_resolved_signer { + Some(s) => s, + None => send_tx.eth.wallet.signer().await?, + }; + let from = ak.wallet_address; + + let (tx_request, _) = builder.build(from).await?; + + let raw_tx = tx_request + .sign_with_access_key_provisioning( + &provider, + &signer, + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + + let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); + + let cast = CastTxSender::new(&provider); + cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await + // Case 4: // An option to use a local signer was provided. // If we cannot successfully instantiate a local signer, then we will assume we don't have // enough information to sign and we must bail.