Skip to main content

forge_script/
broadcast.rs

1use std::{cmp::Ordering, sync::Arc, time::Duration};
2
3use alloy_chains::{Chain, NamedChain};
4use alloy_consensus::TxEnvelope;
5use alloy_eips::{BlockId, eip2718::Encodable2718};
6use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder};
7use alloy_primitives::{
8    Address, TxHash,
9    map::{AddressHashMap, AddressHashSet},
10    utils::format_units,
11};
12use alloy_provider::{Provider, utils::Eip1559Estimation};
13use alloy_rpc_types::TransactionRequest;
14use alloy_serde::WithOtherFields;
15use eyre::{Context, Result, bail};
16use forge_verify::provider::VerificationProviderType;
17use foundry_cheatcodes::Wallets;
18use foundry_cli::utils::{has_batch_support, has_different_gas_calc};
19use foundry_common::{
20    TransactionMaybeSigned,
21    provider::{RetryProvider, get_http_provider, try_get_http_provider},
22    shell,
23};
24use foundry_config::Config;
25use foundry_wallets::{WalletSigner, wallet_browser::signer::BrowserSigner};
26use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered};
27use itertools::Itertools;
28
29use crate::{
30    ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress,
31    sequence::ScriptSequenceKind, verify::BroadcastedState,
32};
33
34pub async fn estimate_gas<P: Provider<AnyNetwork>>(
35    tx: &mut WithOtherFields<TransactionRequest>,
36    provider: &P,
37    estimate_multiplier: u64,
38) -> Result<()> {
39    // if already set, some RPC endpoints might simply return the gas value that is already
40    // set in the request and omit the estimate altogether, so we remove it here
41    tx.gas = None;
42
43    tx.set_gas_limit(
44        provider.estimate_gas(tx.clone()).await.wrap_err("Failed to estimate gas for tx")?
45            * estimate_multiplier
46            / 100,
47    );
48    Ok(())
49}
50
51pub async fn next_nonce(
52    caller: Address,
53    provider_url: &str,
54    block_number: Option<u64>,
55) -> eyre::Result<u64> {
56    let provider = try_get_http_provider(provider_url)
57        .wrap_err_with(|| format!("bad fork_url provider: {provider_url}"))?;
58
59    let block_id = block_number.map_or(BlockId::latest(), BlockId::number);
60    Ok(provider.get_transaction_count(caller).block_id(block_id).await?)
61}
62
63/// Represents how to send a single transaction.
64#[derive(Clone)]
65pub enum SendTransactionKind<'a> {
66    Unlocked(WithOtherFields<TransactionRequest>),
67    Raw(WithOtherFields<TransactionRequest>, &'a EthereumWallet),
68    Browser(WithOtherFields<TransactionRequest>, &'a BrowserSigner),
69    Signed(TxEnvelope),
70}
71
72impl<'a> SendTransactionKind<'a> {
73    /// Prepares the transaction for broadcasting by synchronizing nonce and estimating gas.
74    ///
75    /// This method performs two key operations:
76    /// 1. Nonce synchronization: Waits for the provider's nonce to catch up to the expected
77    ///    transaction nonce when doing sequential broadcast
78    /// 2. Gas estimation: Re-estimates gas right before broadcasting for chains that require it
79    pub async fn prepare(
80        &mut self,
81        provider: &RetryProvider,
82        sequential_broadcast: bool,
83        is_fixed_gas_limit: bool,
84        estimate_via_rpc: bool,
85        estimate_multiplier: u64,
86    ) -> Result<()> {
87        if let Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) = self {
88            if sequential_broadcast {
89                let from = tx.from.expect("no sender");
90
91                let tx_nonce = tx.nonce.expect("no nonce");
92                for attempt in 0..5 {
93                    let nonce = provider.get_transaction_count(from).await?;
94                    match nonce.cmp(&tx_nonce) {
95                        Ordering::Greater => {
96                            bail!(
97                                "EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider."
98                            )
99                        }
100                        Ordering::Less => {
101                            if attempt == 4 {
102                                bail!(
103                                    "After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce})."
104                                )
105                            }
106                            warn!(
107                                "Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."
108                            );
109                            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
110                        }
111                        Ordering::Equal => {
112                            // Nonces are equal, we can proceed.
113                            break;
114                        }
115                    }
116                }
117            }
118
119            // Chains which use `eth_estimateGas` are being sent sequentially and require their
120            // gas to be re-estimated right before broadcasting.
121            if !is_fixed_gas_limit && estimate_via_rpc {
122                estimate_gas(tx, provider, estimate_multiplier).await?;
123            }
124        }
125
126        Ok(())
127    }
128
129    /// Sends the transaction to the network.
130    ///
131    /// Depending on the transaction kind, this will either:
132    /// - Submit via `eth_sendTransaction` for unlocked accounts
133    /// - Sign and submit via `eth_sendRawTransaction` for raw transactions
134    /// - Submit pre-signed transaction via `eth_sendRawTransaction`
135    pub async fn send(self, provider: Arc<RetryProvider>) -> Result<TxHash> {
136        match self {
137            Self::Unlocked(tx) => {
138                debug!("sending transaction from unlocked account {:?}", tx);
139
140                // Submit the transaction
141                let pending = provider.send_transaction(tx).await?;
142                Ok(*pending.tx_hash())
143            }
144            Self::Raw(tx, signer) => {
145                debug!("sending transaction: {:?}", tx);
146                let signed = tx.build(signer).await?;
147
148                // Submit the raw transaction
149                let pending = provider.send_raw_transaction(signed.encoded_2718().as_ref()).await?;
150                Ok(*pending.tx_hash())
151            }
152            Self::Signed(tx) => {
153                debug!("sending transaction: {:?}", tx);
154                let pending = provider.send_raw_transaction(tx.encoded_2718().as_ref()).await?;
155                Ok(*pending.tx_hash())
156            }
157            Self::Browser(tx, signer) => {
158                debug!("sending transaction: {:?}", tx);
159
160                // Sign and send the transaction via the browser wallet
161                Ok(signer.send_transaction_via_browser(tx.into_inner()).await?)
162            }
163        }
164    }
165
166    /// Prepares and sends the transaction in one operation.
167    ///
168    /// This is a convenience method that combines [`prepare`](Self::prepare) and
169    /// [`send`](Self::send) into a single call.
170    pub async fn prepare_and_send(
171        mut self,
172        provider: Arc<RetryProvider>,
173        sequential_broadcast: bool,
174        is_fixed_gas_limit: bool,
175        estimate_via_rpc: bool,
176        estimate_multiplier: u64,
177    ) -> Result<TxHash> {
178        self.prepare(
179            &provider,
180            sequential_broadcast,
181            is_fixed_gas_limit,
182            estimate_via_rpc,
183            estimate_multiplier,
184        )
185        .await?;
186
187        self.send(provider).await
188    }
189}
190
191/// Convenience enum to represent either an Ethereum wallet or a browser signer
192pub enum EitherSigner {
193    Ethereum(EthereumWallet),
194    Browser(BrowserSigner),
195}
196
197impl From<EthereumWallet> for EitherSigner {
198    fn from(wallet: EthereumWallet) -> Self {
199        Self::Ethereum(wallet)
200    }
201}
202
203impl From<WalletSigner> for EitherSigner {
204    fn from(wallet: WalletSigner) -> Self {
205        match wallet {
206            WalletSigner::Browser(wallet) => Self::Browser(wallet),
207            // Convert any other signer to an Ethereum wallet
208            signer => EthereumWallet::new(signer).into(),
209        }
210    }
211}
212
213impl From<BrowserSigner> for EitherSigner {
214    fn from(wallet: BrowserSigner) -> Self {
215        Self::Browser(wallet)
216    }
217}
218
219/// Represents how to send _all_ transactions
220pub enum SendTransactionsKind {
221    /// Send via `eth_sendTransaction` and rely on the  `from` address being unlocked.
222    Unlocked(AddressHashSet),
223    /// Send a signed transaction via `eth_sendRawTransaction`
224    Raw(AddressHashMap<EitherSigner>),
225}
226
227impl SendTransactionsKind {
228    /// Returns the [`SendTransactionKind`] for the given address
229    ///
230    /// Returns an error if no matching signer is found or the address is not unlocked
231    pub fn for_sender(
232        &self,
233        addr: &Address,
234        tx: WithOtherFields<TransactionRequest>,
235    ) -> Result<SendTransactionKind<'_>> {
236        match self {
237            Self::Unlocked(unlocked) => {
238                if !unlocked.contains(addr) {
239                    bail!("Sender address {:?} is not unlocked", addr)
240                }
241                Ok(SendTransactionKind::Unlocked(tx))
242            }
243            Self::Raw(wallets) => {
244                if let Some(wallet) = wallets.get(addr) {
245                    match wallet {
246                        EitherSigner::Ethereum(wallet) => Ok(SendTransactionKind::Raw(tx, wallet)),
247                        EitherSigner::Browser(signer) => {
248                            Ok(SendTransactionKind::Browser(tx, signer))
249                        }
250                    }
251                } else {
252                    bail!("No matching signer for {:?} found", addr)
253                }
254            }
255        }
256    }
257}
258
259/// State after we have bundled all
260/// [`TransactionWithMetadata`](forge_script_sequence::TransactionWithMetadata) objects into a
261/// single [`ScriptSequenceKind`] object containing one or more script sequences.
262pub struct BundledState {
263    pub args: ScriptArgs,
264    pub script_config: ScriptConfig,
265    pub script_wallets: Wallets,
266    pub build_data: LinkedBuildData,
267    pub sequence: ScriptSequenceKind,
268}
269
270impl BundledState {
271    pub async fn wait_for_pending(mut self) -> Result<Self> {
272        let progress = ScriptProgress::default();
273        let progress_ref = &progress;
274        let futs = self
275            .sequence
276            .sequences_mut()
277            .iter_mut()
278            .enumerate()
279            .map(|(sequence_idx, sequence)| async move {
280                let rpc_url = sequence.rpc_url();
281                let provider = Arc::new(get_http_provider(rpc_url));
282                progress_ref
283                    .wait_for_pending(
284                        sequence_idx,
285                        sequence,
286                        &provider,
287                        self.script_config.config.transaction_timeout,
288                    )
289                    .await
290            })
291            .collect::<Vec<_>>();
292
293        let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::<Vec<_>>();
294
295        self.sequence.save(true, false)?;
296
297        if !errors.is_empty() {
298            return Err(eyre::eyre!("{}", errors.iter().format("\n")));
299        }
300
301        Ok(self)
302    }
303
304    /// Broadcasts transactions from all sequences.
305    pub async fn broadcast(mut self) -> Result<BroadcastedState> {
306        let required_addresses = self
307            .sequence
308            .sequences()
309            .iter()
310            .flat_map(|sequence| {
311                sequence
312                    .transactions()
313                    .filter(|tx| tx.is_unsigned())
314                    .map(|tx| tx.from().expect("missing from"))
315            })
316            .collect::<AddressHashSet>();
317
318        if required_addresses.contains(&Config::DEFAULT_SENDER) {
319            eyre::bail!(
320                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
321            );
322        }
323
324        let send_kind = if self.args.unlocked {
325            SendTransactionsKind::Unlocked(required_addresses.clone())
326        } else {
327            let signers = self.script_wallets.into_multi_wallet().into_signers()?;
328            let mut missing_addresses = Vec::new();
329
330            for addr in &required_addresses {
331                if !signers.contains_key(addr) {
332                    missing_addresses.push(addr);
333                }
334            }
335
336            if !missing_addresses.is_empty() {
337                eyre::bail!(
338                    "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}",
339                    missing_addresses,
340                    signers.keys().collect::<Vec<_>>()
341                );
342            }
343
344            let signers = signers.into_iter().map(|(addr, signer)| (addr, signer.into())).collect();
345
346            SendTransactionsKind::Raw(signers)
347        };
348
349        let progress = ScriptProgress::default();
350
351        for i in 0..self.sequence.sequences().len() {
352            let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
353
354            let provider = Arc::new(try_get_http_provider(sequence.rpc_url())?);
355            let already_broadcasted = sequence.receipts.len();
356
357            let seq_progress = progress.get_sequence_progress(i, sequence);
358
359            if already_broadcasted < sequence.transactions.len() {
360                let is_legacy = Chain::from(sequence.chain).is_legacy() || self.args.legacy;
361                // Make a one-time gas price estimation
362                let (gas_price, eip1559_fees) = match (
363                    is_legacy,
364                    self.args.with_gas_price,
365                    self.args.priority_gas_price,
366                ) {
367                    (true, Some(gas_price), _) => (Some(gas_price.to()), None),
368                    (true, None, _) => (Some(provider.get_gas_price().await?), None),
369                    (false, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => (
370                        None,
371                        Some(Eip1559Estimation {
372                            max_fee_per_gas: max_fee_per_gas.to(),
373                            max_priority_fee_per_gas: max_priority_fee_per_gas.to(),
374                        }),
375                    ),
376                    (false, _, _) => {
377                        let mut fees = provider.estimate_eip1559_fees().await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
378
379                        if let Some(gas_price) = self.args.with_gas_price {
380                            fees.max_fee_per_gas = gas_price.to();
381                        }
382
383                        if let Some(priority_gas_price) = self.args.priority_gas_price {
384                            fees.max_priority_fee_per_gas = priority_gas_price.to();
385                        }
386
387                        (None, Some(fees))
388                    }
389                };
390
391                // Iterate through transactions, matching the `from` field with the associated
392                // wallet. Then send the transaction. Panics if we find a unknown `from`
393                let transactions = sequence
394                    .transactions
395                    .iter()
396                    .skip(already_broadcasted)
397                    .map(|tx_with_metadata| {
398                        let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit;
399
400                        let kind = match tx_with_metadata.tx().clone() {
401                            TransactionMaybeSigned::Signed { tx, .. } => {
402                                SendTransactionKind::Signed(tx)
403                            }
404                            TransactionMaybeSigned::Unsigned(mut tx) => {
405                                let from = tx.from.expect("No sender for onchain transaction!");
406
407                                tx.set_chain_id(sequence.chain);
408
409                                // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in
410                                // alloy
411                                if tx.to.is_none() {
412                                    tx.set_create();
413                                }
414
415                                if let Some(gas_price) = gas_price {
416                                    tx.set_gas_price(gas_price);
417                                } else {
418                                    let eip1559_fees = eip1559_fees.expect("was set above");
419                                    tx.set_max_priority_fee_per_gas(
420                                        eip1559_fees.max_priority_fee_per_gas,
421                                    );
422                                    tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas);
423                                }
424
425                                send_kind.for_sender(&from, tx)?
426                            }
427                        };
428
429                        Ok((kind, is_fixed_gas_limit))
430                    })
431                    .collect::<Result<Vec<_>>>()?;
432
433                let estimate_via_rpc =
434                    has_different_gas_calc(sequence.chain) || self.args.skip_simulation;
435
436                // We only wait for a transaction receipt before sending the next transaction, if
437                // there is more than one signer. There would be no way of assuring
438                // their order otherwise.
439                // Or if the chain does not support batched transactions (eg. Arbitrum).
440                // Or if we need to invoke eth_estimateGas before sending transactions.
441                let sequential_broadcast = estimate_via_rpc
442                    || self.args.slow
443                    || required_addresses.len() != 1
444                    || !has_batch_support(sequence.chain);
445
446                // We send transactions and wait for receipts in batches.
447                let batch_size = if sequential_broadcast { 1 } else { self.args.batch_size };
448                let mut index = already_broadcasted;
449
450                for (batch_number, batch) in transactions.chunks(batch_size).enumerate() {
451                    seq_progress.inner.write().set_status(&format!(
452                        "Sending transactions [{} - {}]",
453                        batch_number * batch_size,
454                        batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1
455                    ));
456
457                    if !batch.is_empty() {
458                        let pending_transactions =
459                            batch.iter().map(|(kind, is_fixed_gas_limit)| {
460                                let provider = provider.clone();
461                                async move {
462                                    let res = kind
463                                        .clone()
464                                        .prepare_and_send(
465                                            provider,
466                                            sequential_broadcast,
467                                            *is_fixed_gas_limit,
468                                            estimate_via_rpc,
469                                            self.args.gas_estimate_multiplier,
470                                        )
471                                        .await;
472                                    (res, kind, 0, None)
473                                }
474                                .boxed()
475                            });
476
477                        let mut buffer = pending_transactions.collect::<FuturesUnordered<_>>();
478
479                        'send: while let Some((res, kind, attempt, original_res)) =
480                            buffer.next().await
481                        {
482                            if res.is_err() && attempt <= 3 {
483                                // Try to resubmit the transaction
484                                let provider = provider.clone();
485                                let progress = seq_progress.inner.clone();
486                                buffer.push(Box::pin(async move {
487                                    debug!(err=?res, ?attempt, "retrying transaction ");
488                                    let attempt = attempt + 1;
489                                    progress.write().set_status(&format!(
490                                        "retrying transaction {res:?} (attempt {attempt})"
491                                    ));
492                                    tokio::time::sleep(Duration::from_millis(1000 * attempt)).await;
493                                    let r = kind.clone().send(provider).await;
494                                    (r, kind, attempt, original_res.or(Some(res)))
495                                }));
496
497                                continue 'send;
498                            }
499
500                            // Preserve the original error if any
501                            let tx_hash = res.wrap_err_with(|| {
502                                if let Some(original_res) = original_res {
503                                    format!(
504                                        "Failed to send transaction after {attempt} attempts {original_res:?}"
505                                    )
506                                } else {
507                                    "Failed to send transaction".to_string()
508                                }
509                            })?;
510                            sequence.add_pending(index, tx_hash);
511
512                            // Checkpoint save
513                            self.sequence.save(true, false)?;
514                            sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
515
516                            seq_progress.inner.write().tx_sent(tx_hash);
517                            index += 1;
518                        }
519
520                        // Checkpoint save
521                        self.sequence.save(true, false)?;
522                        sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
523
524                        progress
525                            .wait_for_pending(
526                                i,
527                                sequence,
528                                &provider,
529                                self.script_config.config.transaction_timeout,
530                            )
531                            .await?
532                    }
533                    // Checkpoint save
534                    self.sequence.save(true, false)?;
535                    sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
536                }
537            }
538
539            let (total_gas, total_gas_price, total_paid) =
540                sequence.receipts.iter().fold((0, 0, 0), |acc, receipt| {
541                    let gas_used = receipt.gas_used;
542                    let gas_price = receipt.effective_gas_price as u64;
543                    (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price)
544                });
545            let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
546            let avg_gas_price = if sequence.receipts.is_empty() {
547                "N/A".to_string()
548            } else {
549                format_units(total_gas_price / sequence.receipts.len() as u64, 9)
550                    .unwrap_or_else(|_| "N/A".to_string())
551            };
552
553            let token_symbol = NamedChain::try_from(sequence.chain)
554                .unwrap_or_default()
555                .native_currency_symbol()
556                .unwrap_or("ETH");
557            seq_progress.inner.write().set_status(&format!(
558                "Total Paid: {} {} ({} gas * avg {} gwei)\n",
559                paid.trim_end_matches('0'),
560                token_symbol,
561                total_gas,
562                avg_gas_price.trim_end_matches('0').trim_end_matches('.')
563            ));
564            seq_progress.inner.write().finish();
565        }
566
567        if !shell::is_json() {
568            sh_println!("\n\n==========================")?;
569            sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?;
570        }
571
572        Ok(BroadcastedState {
573            args: self.args,
574            script_config: self.script_config,
575            build_data: self.build_data,
576            sequence: self.sequence,
577        })
578    }
579
580    pub fn verify_preflight_check(&self) -> Result<()> {
581        for sequence in self.sequence.sequences() {
582            if self.args.verifier.verifier == VerificationProviderType::Etherscan
583                && self
584                    .script_config
585                    .config
586                    .get_etherscan_api_key(Some(sequence.chain.into()))
587                    .is_none()
588            {
589                eyre::bail!("Missing etherscan key for chain {}", sequence.chain);
590            }
591        }
592
593        Ok(())
594    }
595}