cast/
tx.rs

1use crate::traces::identifier::SignaturesIdentifier;
2use alloy_consensus::{SidecarBuilder, SignableTransaction, SimpleCoder};
3use alloy_dyn_abi::ErrorExt;
4use alloy_ens::NameOrAddress;
5use alloy_json_abi::Function;
6use alloy_network::{
7    AnyNetwork, AnyTypedTransaction, TransactionBuilder, TransactionBuilder4844,
8    TransactionBuilder7702,
9};
10use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256, hex};
11use alloy_provider::{PendingTransactionBuilder, Provider};
12use alloy_rpc_types::{AccessList, Authorization, TransactionInputKind, TransactionRequest};
13use alloy_serde::WithOtherFields;
14use alloy_signer::Signer;
15use alloy_transport::TransportError;
16use clap::Args;
17use eyre::{Result, WrapErr};
18use foundry_cli::{
19    opts::{CliAuthorizationList, EthereumOpts, TransactionOpts},
20    utils::{self, LoadConfig, get_provider_builder, parse_function_args},
21};
22use foundry_common::{
23    TransactionReceiptWithRevertReason, fmt::*, get_pretty_tx_receipt_attr,
24    provider::RetryProviderWithSigner, shell,
25};
26use foundry_config::{Chain, Config};
27use foundry_wallets::{WalletOpts, WalletSigner};
28use itertools::Itertools;
29use serde_json::value::RawValue;
30use std::{fmt::Write, str::FromStr, time::Duration};
31
32#[derive(Debug, Clone, Args)]
33pub struct SendTxOpts {
34    /// Only print the transaction hash and exit immediately.
35    #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
36    pub cast_async: bool,
37
38    /// Wait for transaction receipt synchronously instead of polling.
39    /// Note: uses `eth_sendTransactionSync` which may not be supported by all clients.
40    #[arg(long, conflicts_with = "async")]
41    pub sync: bool,
42
43    /// The number of confirmations until the receipt is fetched.
44    #[arg(long, default_value = "1")]
45    pub confirmations: u64,
46
47    /// Timeout for sending the transaction.
48    #[arg(long, env = "ETH_TIMEOUT")]
49    pub timeout: Option<u64>,
50
51    /// Polling interval for transaction receipts (in seconds).
52    #[arg(long, alias = "poll-interval", env = "ETH_POLL_INTERVAL")]
53    pub poll_interval: Option<u64>,
54
55    /// Ethereum options
56    #[command(flatten)]
57    pub eth: EthereumOpts,
58}
59
60/// Different sender kinds used by [`CastTxBuilder`].
61pub enum SenderKind<'a> {
62    /// An address without signer. Used for read-only calls and transactions sent through unlocked
63    /// accounts.
64    Address(Address),
65    /// A reference to a signer.
66    Signer(&'a WalletSigner),
67    /// An owned signer.
68    OwnedSigner(Box<WalletSigner>),
69}
70
71impl SenderKind<'_> {
72    /// Resolves the name to an Ethereum Address.
73    pub fn address(&self) -> Address {
74        match self {
75            Self::Address(addr) => *addr,
76            Self::Signer(signer) => signer.address(),
77            Self::OwnedSigner(signer) => signer.address(),
78        }
79    }
80
81    /// Resolves the sender from the wallet options.
82    ///
83    /// This function prefers the `from` field and may return a different address from the
84    /// configured signer
85    /// If from is specified, returns it
86    /// If from is not specified, but there is a signer configured, returns the signer's address
87    /// If from is not specified and there is no signer configured, returns zero address
88    pub async fn from_wallet_opts(opts: WalletOpts) -> Result<Self> {
89        if let Some(from) = opts.from {
90            Ok(from.into())
91        } else if let Ok(signer) = opts.signer().await {
92            Ok(Self::OwnedSigner(Box::new(signer)))
93        } else {
94            Ok(Address::ZERO.into())
95        }
96    }
97
98    /// Returns the signer if available.
99    pub fn as_signer(&self) -> Option<&WalletSigner> {
100        match self {
101            Self::Signer(signer) => Some(signer),
102            Self::OwnedSigner(signer) => Some(signer.as_ref()),
103            _ => None,
104        }
105    }
106}
107
108impl From<Address> for SenderKind<'_> {
109    fn from(addr: Address) -> Self {
110        Self::Address(addr)
111    }
112}
113
114impl<'a> From<&'a WalletSigner> for SenderKind<'a> {
115    fn from(signer: &'a WalletSigner) -> Self {
116        Self::Signer(signer)
117    }
118}
119
120impl From<WalletSigner> for SenderKind<'_> {
121    fn from(signer: WalletSigner) -> Self {
122        Self::OwnedSigner(Box::new(signer))
123    }
124}
125
126/// Prevents a misconfigured hwlib from sending a transaction that defies user-specified --from
127pub fn validate_from_address(
128    specified_from: Option<Address>,
129    signer_address: Address,
130) -> Result<()> {
131    if let Some(specified_from) = specified_from
132        && specified_from != signer_address
133    {
134        eyre::bail!(
135                "\
136The specified sender via CLI/env vars does not match the sender configured via
137the hardware wallet's HD Path.
138Please use the `--hd-path <PATH>` parameter to specify the BIP32 Path which
139corresponds to the sender, or let foundry automatically detect it by not specifying any sender address."
140            )
141    }
142    Ok(())
143}
144
145/// Initial state.
146#[derive(Debug)]
147pub struct InitState;
148
149/// State with known [TxKind].
150#[derive(Debug)]
151pub struct ToState {
152    to: Option<Address>,
153}
154
155/// State with known input for the transaction.
156#[derive(Debug)]
157pub struct InputState {
158    kind: TxKind,
159    input: Vec<u8>,
160    func: Option<Function>,
161}
162
163pub struct CastTxSender<P> {
164    provider: P,
165}
166
167impl<P: Provider<AnyNetwork>> CastTxSender<P> {
168    /// Creates a new Cast instance responsible for sending transactions.
169    pub fn new(provider: P) -> Self {
170        Self { provider }
171    }
172
173    /// Sends a transaction and waits for receipt synchronously
174    pub async fn send_sync(&self, tx: WithOtherFields<TransactionRequest>) -> Result<String> {
175        let mut receipt: TransactionReceiptWithRevertReason =
176            self.provider.send_transaction_sync(tx).await?.into();
177
178        // Allow to fail silently
179        let _ = receipt.update_revert_reason(&self.provider).await;
180
181        self.format_receipt(receipt, None)
182    }
183
184    /// Sends a transaction to the specified address
185    ///
186    /// # Example
187    ///
188    /// ```
189    /// use cast::tx::CastTxSender;
190    /// use alloy_primitives::{Address, U256, Bytes};
191    /// use alloy_serde::WithOtherFields;
192    /// use alloy_rpc_types::{TransactionRequest};
193    /// use alloy_provider::{RootProvider, ProviderBuilder, network::AnyNetwork};
194    /// use std::str::FromStr;
195    /// use alloy_sol_types::{sol, SolCall};    ///
196    ///
197    /// sol!(
198    ///     function greet(string greeting) public;
199    /// );
200    ///
201    /// # async fn foo() -> eyre::Result<()> {
202    /// let provider = ProviderBuilder::<_,_, AnyNetwork>::default().connect("http://localhost:8545").await?;;
203    /// let from = Address::from_str("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")?;
204    /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?;
205    /// let greeting = greetCall { greeting: "hello".to_string() }.abi_encode();
206    /// let bytes = Bytes::from_iter(greeting.iter());
207    /// let gas = U256::from_str("200000").unwrap();
208    /// let value = U256::from_str("1").unwrap();
209    /// let nonce = U256::from_str("1").unwrap();
210    /// let tx = TransactionRequest::default().to(to).input(bytes.into()).from(from);
211    /// let tx = WithOtherFields::new(tx);
212    /// let cast = CastTxSender::new(provider);
213    /// let data = cast.send(tx).await?;
214    /// println!("{:#?}", data);
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub async fn send(
219        &self,
220        tx: WithOtherFields<TransactionRequest>,
221    ) -> Result<PendingTransactionBuilder<AnyNetwork>> {
222        let res = self.provider.send_transaction(tx).await?;
223
224        Ok(res)
225    }
226
227    /// # Example
228    ///
229    /// ```
230    /// use alloy_provider::{ProviderBuilder, RootProvider, network::AnyNetwork};
231    /// use cast::tx::CastTxSender;
232    ///
233    /// async fn foo() -> eyre::Result<()> {
234    /// let provider =
235    ///     ProviderBuilder::<_, _, AnyNetwork>::default().connect("http://localhost:8545").await?;
236    /// let cast = CastTxSender::new(provider);
237    /// let tx_hash = "0xf8d1713ea15a81482958fb7ddf884baee8d3bcc478c5f2f604e008dc788ee4fc";
238    /// let receipt = cast.receipt(tx_hash.to_string(), None, 1, None, false).await?;
239    /// println!("{}", receipt);
240    /// # Ok(())
241    /// # }
242    /// ```
243    pub async fn receipt(
244        &self,
245        tx_hash: String,
246        field: Option<String>,
247        confs: u64,
248        timeout: Option<u64>,
249        cast_async: bool,
250    ) -> Result<String> {
251        let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
252
253        let mut receipt: TransactionReceiptWithRevertReason =
254            match self.provider.get_transaction_receipt(tx_hash).await? {
255                Some(r) => r,
256                None => {
257                    // if the async flag is provided, immediately exit if no tx is found, otherwise
258                    // try to poll for it
259                    if cast_async {
260                        eyre::bail!("tx not found: {:?}", tx_hash)
261                    } else {
262                        PendingTransactionBuilder::new(self.provider.root().clone(), tx_hash)
263                            .with_required_confirmations(confs)
264                            .with_timeout(timeout.map(Duration::from_secs))
265                            .get_receipt()
266                            .await?
267                    }
268                }
269            }
270            .into();
271
272        // Allow to fail silently
273        let _ = receipt.update_revert_reason(&self.provider).await;
274
275        self.format_receipt(receipt, field)
276    }
277
278    /// Helper method to format transaction receipts consistently
279    fn format_receipt(
280        &self,
281        receipt: TransactionReceiptWithRevertReason,
282        field: Option<String>,
283    ) -> Result<String> {
284        Ok(if let Some(ref field) = field {
285            get_pretty_tx_receipt_attr(&receipt, field)
286                .ok_or_else(|| eyre::eyre!("invalid receipt field: {}", field))?
287        } else if shell::is_json() {
288            // to_value first to sort json object keys
289            serde_json::to_value(&receipt)?.to_string()
290        } else {
291            receipt.pretty()
292        })
293    }
294}
295
296/// Builder type constructing [TransactionRequest] from cast send/mktx inputs.
297///
298/// It is implemented as a stateful builder with expected state transition of [InitState] ->
299/// [ToState] -> [InputState].
300#[derive(Debug)]
301pub struct CastTxBuilder<P, S> {
302    provider: P,
303    tx: WithOtherFields<TransactionRequest>,
304    /// Whether the transaction should be sent as a legacy transaction.
305    legacy: bool,
306    blob: bool,
307    auth: Vec<CliAuthorizationList>,
308    chain: Chain,
309    etherscan_api_key: Option<String>,
310    access_list: Option<Option<AccessList>>,
311    state: S,
312}
313
314impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InitState> {
315    /// Creates a new instance of [CastTxBuilder] filling transaction with fields present in
316    /// provided [TransactionOpts].
317    pub async fn new(provider: P, tx_opts: TransactionOpts, config: &Config) -> Result<Self> {
318        let mut tx = WithOtherFields::<TransactionRequest>::default();
319
320        let chain = utils::get_chain(config.chain, &provider).await?;
321        let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
322        // mark it as legacy if requested or the chain is legacy and no 7702 is provided.
323        let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty());
324
325        if let Some(gas_limit) = tx_opts.gas_limit {
326            tx.set_gas_limit(gas_limit.to());
327        }
328
329        if let Some(value) = tx_opts.value {
330            tx.set_value(value);
331        }
332
333        if let Some(gas_price) = tx_opts.gas_price {
334            if legacy {
335                tx.set_gas_price(gas_price.to());
336            } else {
337                tx.set_max_fee_per_gas(gas_price.to());
338            }
339        }
340
341        if !legacy && let Some(priority_fee) = tx_opts.priority_gas_price {
342            tx.set_max_priority_fee_per_gas(priority_fee.to());
343        }
344
345        if let Some(max_blob_fee) = tx_opts.blob_gas_price {
346            tx.set_max_fee_per_blob_gas(max_blob_fee.to())
347        }
348
349        if let Some(nonce) = tx_opts.nonce {
350            tx.set_nonce(nonce.to());
351        }
352
353        Ok(Self {
354            provider,
355            tx,
356            legacy,
357            blob: tx_opts.blob,
358            chain,
359            etherscan_api_key,
360            auth: tx_opts.auth,
361            access_list: tx_opts.access_list,
362            state: InitState,
363        })
364    }
365
366    /// Sets [TxKind] for this builder and changes state to [ToState].
367    pub async fn with_to(self, to: Option<NameOrAddress>) -> Result<CastTxBuilder<P, ToState>> {
368        let to = if let Some(to) = to { Some(to.resolve(&self.provider).await?) } else { None };
369        Ok(CastTxBuilder {
370            provider: self.provider,
371            tx: self.tx,
372            legacy: self.legacy,
373            blob: self.blob,
374            chain: self.chain,
375            etherscan_api_key: self.etherscan_api_key,
376            auth: self.auth,
377            access_list: self.access_list,
378            state: ToState { to },
379        })
380    }
381}
382
383impl<P: Provider<AnyNetwork>> CastTxBuilder<P, ToState> {
384    /// Accepts user-provided code, sig and args params and constructs calldata for the transaction.
385    /// If code is present, input will be set to code + encoded constructor arguments. If no code is
386    /// present, input is set to just provided arguments.
387    pub async fn with_code_sig_and_args(
388        self,
389        code: Option<String>,
390        sig: Option<String>,
391        args: Vec<String>,
392    ) -> Result<CastTxBuilder<P, InputState>> {
393        let (mut args, func) = if let Some(sig) = sig {
394            parse_function_args(
395                &sig,
396                args,
397                self.state.to,
398                self.chain,
399                &self.provider,
400                self.etherscan_api_key.as_deref(),
401            )
402            .await?
403        } else {
404            (Vec::new(), None)
405        };
406
407        let input = if let Some(code) = &code {
408            let mut code = hex::decode(code)?;
409            code.append(&mut args);
410            code
411        } else {
412            args
413        };
414
415        if self.state.to.is_none() && code.is_none() {
416            let has_value = self.tx.value.is_some_and(|v| !v.is_zero());
417            let has_auth = !self.auth.is_empty();
418            // We only allow user to omit the recipient address if transaction is an EIP-7702 tx
419            // without a value.
420            if !has_auth || has_value {
421                eyre::bail!("Must specify a recipient address or contract code to deploy");
422            }
423        }
424
425        Ok(CastTxBuilder {
426            provider: self.provider,
427            tx: self.tx,
428            legacy: self.legacy,
429            blob: self.blob,
430            chain: self.chain,
431            etherscan_api_key: self.etherscan_api_key,
432            auth: self.auth,
433            access_list: self.access_list,
434            state: InputState { kind: self.state.to.into(), input, func },
435        })
436    }
437}
438
439impl<P: Provider<AnyNetwork>> CastTxBuilder<P, InputState> {
440    /// Builds [TransactionRequest] and fills missing fields. Returns a transaction which is ready
441    /// to be broadcasted.
442    pub async fn build(
443        self,
444        sender: impl Into<SenderKind<'_>>,
445    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
446        self._build(sender, true, false).await
447    }
448
449    /// Builds [TransactionRequest] without filling missing fields. Used for read-only calls such as
450    /// eth_call, eth_estimateGas, etc
451    pub async fn build_raw(
452        self,
453        sender: impl Into<SenderKind<'_>>,
454    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
455        self._build(sender, false, false).await
456    }
457
458    /// Builds an unsigned RLP-encoded raw transaction.
459    ///
460    /// Returns the hex encoded string representation of the transaction.
461    pub async fn build_unsigned_raw(self, from: Address) -> Result<String> {
462        let (tx, _) = self._build(SenderKind::Address(from), true, true).await?;
463        let tx = tx.build_unsigned()?;
464        match tx {
465            AnyTypedTransaction::Ethereum(t) => Ok(hex::encode_prefixed(t.encoded_for_signing())),
466            _ => eyre::bail!("Cannot generate unsigned transaction for non-Ethereum transactions"),
467        }
468    }
469
470    async fn _build(
471        mut self,
472        sender: impl Into<SenderKind<'_>>,
473        fill: bool,
474        unsigned: bool,
475    ) -> Result<(WithOtherFields<TransactionRequest>, Option<Function>)> {
476        let sender = sender.into();
477        let from = sender.address();
478
479        self.tx.set_kind(self.state.kind);
480
481        // we set both fields to the same value because some nodes only accept the legacy `data` field: <https://github.com/foundry-rs/foundry/issues/7764#issuecomment-2210453249>
482        self.tx.set_input_kind(self.state.input.clone(), TransactionInputKind::Both);
483
484        self.tx.set_from(from);
485        self.tx.set_chain_id(self.chain.id());
486
487        let tx_nonce = if let Some(nonce) = self.tx.nonce {
488            nonce
489        } else {
490            let nonce = self.provider.get_transaction_count(from).await?;
491            if fill {
492                self.tx.nonce = Some(nonce);
493            }
494            nonce
495        };
496
497        if !unsigned {
498            self.resolve_auth(sender, tx_nonce).await?;
499        } else if !self.auth.is_empty() {
500            let mut signed_auths = Vec::with_capacity(self.auth.len());
501            for auth in std::mem::take(&mut self.auth) {
502                let CliAuthorizationList::Signed(signed_auth) = auth else {
503                    eyre::bail!(
504                        "SignedAuthorization needs to be provided for generating unsigned 7702 txs"
505                    )
506                };
507                signed_auths.push(signed_auth);
508            }
509
510            self.tx.set_authorization_list(signed_auths);
511        }
512
513        if let Some(access_list) = match self.access_list.take() {
514            None => None,
515            // --access-list provided with no value, call the provider to create it
516            Some(None) => Some(self.provider.create_access_list(&self.tx).await?.access_list),
517            // Access list provided as a string, attempt to parse it
518            Some(Some(access_list)) => Some(access_list),
519        } {
520            self.tx.set_access_list(access_list);
521        }
522
523        if !fill {
524            return Ok((self.tx, self.state.func));
525        }
526
527        if self.legacy && self.tx.gas_price.is_none() {
528            self.tx.gas_price = Some(self.provider.get_gas_price().await?);
529        }
530
531        if self.blob && self.tx.max_fee_per_blob_gas.is_none() {
532            self.tx.max_fee_per_blob_gas = Some(self.provider.get_blob_base_fee().await?)
533        }
534
535        if !self.legacy
536            && (self.tx.max_fee_per_gas.is_none() || self.tx.max_priority_fee_per_gas.is_none())
537        {
538            let estimate = self.provider.estimate_eip1559_fees().await?;
539
540            if self.tx.max_fee_per_gas.is_none() {
541                self.tx.max_fee_per_gas = Some(estimate.max_fee_per_gas);
542            }
543
544            if self.tx.max_priority_fee_per_gas.is_none() {
545                self.tx.max_priority_fee_per_gas = Some(estimate.max_priority_fee_per_gas);
546            }
547        }
548
549        if self.tx.gas.is_none() {
550            self.estimate_gas().await?;
551        }
552
553        Ok((self.tx, self.state.func))
554    }
555
556    /// Estimate tx gas from provider call. Tries to decode custom error if execution reverted.
557    async fn estimate_gas(&mut self) -> Result<()> {
558        match self.provider.estimate_gas(self.tx.clone()).await {
559            Ok(estimated) => {
560                self.tx.gas = Some(estimated);
561                Ok(())
562            }
563            Err(err) => {
564                if let TransportError::ErrorResp(payload) = &err {
565                    // If execution reverted with code 3 during provider gas estimation then try
566                    // to decode custom errors and append it to the error message.
567                    if payload.code == 3
568                        && let Some(data) = &payload.data
569                        && let Ok(Some(decoded_error)) = decode_execution_revert(data).await
570                    {
571                        eyre::bail!("Failed to estimate gas: {}: {}", err, decoded_error)
572                    }
573                }
574                eyre::bail!("Failed to estimate gas: {}", err)
575            }
576        }
577    }
578
579    /// Parses the passed --auth values and sets the authorization list on the transaction.
580    async fn resolve_auth(&mut self, sender: SenderKind<'_>, tx_nonce: u64) -> Result<()> {
581        if self.auth.is_empty() {
582            return Ok(());
583        }
584
585        let auths = std::mem::take(&mut self.auth);
586
587        // Validate that at most one address-based auth is provided (multiple addresses are
588        // almost always unintended).
589        let address_auth_count =
590            auths.iter().filter(|a| matches!(a, CliAuthorizationList::Address(_))).count();
591        if address_auth_count > 1 {
592            eyre::bail!(
593                "Multiple address-based authorizations provided. Only one address can be specified; \
594                use pre-signed authorizations (hex-encoded) for multiple authorizations."
595            );
596        }
597
598        let mut signed_auths = Vec::with_capacity(auths.len());
599
600        for auth in auths {
601            let signed_auth = match auth {
602                CliAuthorizationList::Address(address) => {
603                    let auth = Authorization {
604                        chain_id: U256::from(self.chain.id()),
605                        nonce: tx_nonce + 1,
606                        address,
607                    };
608
609                    let Some(signer) = sender.as_signer() else {
610                        eyre::bail!("No signer available to sign authorization");
611                    };
612                    let signature = signer.sign_hash(&auth.signature_hash()).await?;
613
614                    auth.into_signed(signature)
615                }
616                CliAuthorizationList::Signed(auth) => auth,
617            };
618            signed_auths.push(signed_auth);
619        }
620
621        self.tx.set_authorization_list(signed_auths);
622
623        Ok(())
624    }
625}
626
627impl<P, S> CastTxBuilder<P, S>
628where
629    P: Provider<AnyNetwork>,
630{
631    /// Populates the blob sidecar for the transaction if any blob data was provided.
632    pub fn with_blob_data(mut self, blob_data: Option<Vec<u8>>) -> Result<Self> {
633        let Some(blob_data) = blob_data else { return Ok(self) };
634
635        let mut coder = SidecarBuilder::<SimpleCoder>::default();
636        coder.ingest(&blob_data);
637        let sidecar = coder.build()?;
638
639        self.tx.set_blob_sidecar(sidecar);
640        self.tx.populate_blob_hashes();
641
642        Ok(self)
643    }
644}
645
646/// Helper function that tries to decode custom error name and inputs from error payload data.
647async fn decode_execution_revert(data: &RawValue) -> Result<Option<String>> {
648    let err_data = serde_json::from_str::<Bytes>(data.get())?;
649    let Some(selector) = err_data.get(..4) else { return Ok(None) };
650    if let Some(known_error) =
651        SignaturesIdentifier::new(false)?.identify_error(selector.try_into().unwrap()).await
652    {
653        let mut decoded_error = known_error.name.clone();
654        if !known_error.inputs.is_empty()
655            && let Ok(error) = known_error.decode_error(&err_data)
656        {
657            write!(decoded_error, "({})", format_tokens(&error.body).format(", "))?;
658        }
659        return Ok(Some(decoded_error));
660    }
661    Ok(None)
662}
663
664/// Creates a provider with wallet for signing transactions locally.
665pub(crate) async fn signing_provider(
666    tx_opts: &SendTxOpts,
667) -> eyre::Result<RetryProviderWithSigner> {
668    let config = tx_opts.eth.load_config()?;
669    let signer = tx_opts.eth.wallet.signer().await?;
670    let wallet = alloy_network::EthereumWallet::from(signer);
671    let provider = get_provider_builder(&config)?.build_with_wallet(wallet)?;
672    if let Some(interval) = tx_opts.poll_interval {
673        provider.client().set_poll_interval(Duration::from_secs(interval))
674    }
675    Ok(provider)
676}