1use std::{path::PathBuf, str::FromStr, time::Duration};
2use url::Url;
3
4use alloy_consensus::{SignableTransaction, Signed};
5use alloy_ens::NameOrAddress;
6use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder};
7use alloy_primitives::{Address, B256};
8use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
9use alloy_signer::{Signature, Signer};
10use clap::Parser;
11use eyre::{Result, eyre};
12use foundry_cli::{
13 opts::TransactionOpts,
14 utils::{LoadConfig, get_chain, maybe_print_resolved_lane, resolve_lane},
15};
16use foundry_common::{
17 FoundryTransactionBuilder,
18 fmt::{UIfmt, UIfmtReceiptExt},
19 provider::ProviderBuilder,
20 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
21};
22use foundry_config::Chain;
23use foundry_wallets::{TempoAccountsWallet, WalletSigner};
24use tempo_alloy::TempoNetwork;
25use tempo_primitives::transaction::FEE_PAYER_SIGNATURE_MARKER;
26
27use crate::{
28 cmd::{auth::confirm_auth_rpc_disclosure_during_build, tip20::iso4217_warning_message},
29 tempo,
30 tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
31};
32use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
33
34#[derive(Debug, Parser)]
36pub struct SendTxArgs {
37 #[arg(value_parser = NameOrAddress::from_str)]
41 to: Option<NameOrAddress>,
42
43 sig: Option<String>,
45
46 #[arg(allow_negative_numbers = true)]
48 args: Vec<String>,
49
50 #[arg(
52 long,
53 conflicts_with_all = &["sig", "args"]
54 )]
55 data: Option<String>,
56
57 #[command(flatten)]
58 send_tx: SendTxOpts,
59
60 #[command(subcommand)]
61 command: Option<SendTxSubcommands>,
62
63 #[arg(long, requires = "from")]
65 unlocked: bool,
66
67 #[arg(long)]
69 force: bool,
70
71 #[arg(long, value_name = "PERCENT", help_heading = "Transaction options")]
73 gas_estimate_multiplier: Option<u64>,
74
75 #[command(flatten)]
76 tx: TransactionOpts,
77
78 #[arg(
80 long,
81 value_name = "BLOB_DATA_PATH",
82 conflicts_with = "legacy",
83 requires = "blob",
84 help_heading = "Transaction options"
85 )]
86 path: Option<PathBuf>,
87}
88
89#[derive(Debug, Parser)]
90pub enum SendTxSubcommands {
91 #[command(name = "--create")]
93 Create {
94 code: String,
96
97 sig: Option<String>,
99
100 #[arg(allow_negative_numbers = true)]
102 args: Vec<String>,
103 },
104}
105
106impl SendTxArgs {
107 pub async fn run(self) -> Result<()> {
108 if self.tx.tempo.session_id()?.is_some() {
109 return self.run_generic::<TempoNetwork>(None, None).await;
110 }
111
112 let (is_tempo, signer, tempo_access_key) =
113 tempo::resolve_transaction_network_and_signer(&self.tx.tempo, &self.send_tx.eth)
114 .await?;
115
116 if is_tempo {
117 self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
118 } else {
119 self.run_generic::<Ethereum>(signer, None).await
120 }
121 }
122
123 pub async fn run_generic<N: Network>(
124 self,
125 pre_resolved_signer: Option<WalletSigner>,
126 mut access_key: Option<TempoAccountsWallet>,
127 ) -> Result<()>
128 where
129 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
130 N::UnsignedTx: SignableTransaction<Signature>,
131 N::TransactionRequest: FoundryTransactionBuilder<N>,
132 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
133 {
134 let Self {
135 to,
136 mut sig,
137 mut args,
138 data,
139 send_tx,
140 mut tx,
141 command,
142 unlocked,
143 force,
144 gas_estimate_multiplier,
145 path,
146 } = self;
147
148 let has_session = tx.tempo.session_id()?.is_some();
149 if has_session && unlocked {
150 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
151 }
152 if has_session && send_tx.browser.browser {
153 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --browser");
154 }
155
156 let print_sponsor_hash = tx.tempo.print_sponsor_hash;
157 let sponsor_url = tx.tempo.sponsor_url.clone();
158 let sponsor_fee_payer = tx.tempo.sponsor;
159 let expires_at = tx.tempo.resolve_expires();
160 let tempo_sponsor = if print_sponsor_hash || sponsor_url.is_some() {
161 None
162 } else {
163 tx.tempo.sponsor_config().await?
164 };
165
166 let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
167
168 if let Some(data) = data {
169 sig = Some(data);
170 }
171
172 let code = if let Some(SendTxSubcommands::Create {
173 code,
174 sig: constructor_sig,
175 args: constructor_args,
176 }) = command
177 {
178 if to.is_none() && !tx.auth.is_empty() {
181 return Err(eyre!(
182 "EIP-7702 transactions can't be CREATE transactions and require a destination address"
183 ));
184 }
185 if to.is_none() && blob_data.is_some() {
188 return Err(eyre!(
189 "EIP-4844 transactions can't be CREATE transactions and require a destination address"
190 ));
191 }
192
193 sig = constructor_sig;
194 args = constructor_args;
195 Some(code)
196 } else {
197 None
198 };
199
200 if let Some(ref to_addr) = to {
202 let is_factory = match to_addr {
203 NameOrAddress::Address(addr) => *addr == TIP20_FACTORY_ADDRESS,
204 NameOrAddress::Name(name) => {
205 Address::from_str(name).ok() == Some(TIP20_FACTORY_ADDRESS)
206 }
207 };
208
209 if !force
210 && is_factory
211 && let Some(ref sig_str) = sig
212 && sig_str.starts_with("createToken")
213 && let Some(currency) = args.get(2)
214 && !is_iso4217_currency(currency)
215 {
216 sh_warn!("{}", iso4217_warning_message(currency))?;
217 let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
218 if !matches!(response.trim(), "y" | "Y") {
219 sh_status!("Aborted.")?;
220 return Ok(());
221 }
222 }
223 }
224
225 let config = send_tx.eth.load_config()?;
226 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
227
228 let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
229
230 if let Some(interval) = send_tx.poll_interval {
231 provider.client().set_poll_interval(Duration::from_secs(interval))
232 }
233
234 if has_session || access_key.is_some() {
235 let chain = get_chain(config.chain, &provider).await?;
236 let (_, resolved_access_key) =
237 tempo::resolve_session_or_wallet_signer(&tx.tempo, &send_tx.eth.wallet, chain.id())
238 .await?;
239 access_key = resolved_access_key;
240 }
241
242 if let Some(ref ak) = access_key {
244 tx.tempo.key_id = Some(ak.key_id()?);
245 }
246
247 let builder = CastTxBuilder::new(&provider, tx, &config)
248 .await?
249 .with_gas_estimate_multiplier(gas_estimate_multiplier)
250 .with_to(to)
251 .await?
252 .with_code_sig_and_args(code, sig, args)
253 .await?
254 .with_blob_data(blob_data)?;
255
256 if print_sponsor_hash {
258 let chain = builder.chain();
259 let (mut tx, from) = if let Some(ref ak) = access_key {
260 if !confirm_auth_rpc_disclosure_during_build(&builder, ak.account(), force)? {
261 return Ok(());
262 }
263 let (tx, _, prepared) = builder.build_with_tempo_wallet(ak).await?;
264 (tx, prepared.account())
265 } else {
266 let signer = pre_resolved_signer.as_ref().ok_or_else(|| {
269 eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)")
270 })?;
271 let from = signer.address();
272 if !confirm_auth_rpc_disclosure_during_build(&builder, signer, force)? {
273 return Ok(());
274 }
275 let (tx, _) = builder.build(signer).await?;
276 (tx, from)
277 };
278 if let Some(fee_payer) = sponsor_fee_payer {
279 resolve_and_set_fee_token(
280 (!config.eth_rpc_curl).then_some(&provider),
281 Some(chain),
282 &mut tx,
283 Some(fee_payer),
284 )
285 .await?;
286 }
287 let hash = tx
288 .compute_sponsor_hash(from)
289 .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?;
290 sh_println!("{hash:?}")?;
291 return Ok(());
292 }
293
294 if let Some(ts) = expires_at {
295 sh_status!("Transaction expires at unix timestamp {ts}")?;
296 }
297
298 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
299
300 if let Some(ref url) = sponsor_url {
303 validate_sponsor_url(url)?;
304 if unlocked {
305 eyre::bail!("--sponsor-url cannot be combined with --unlocked");
306 }
307 if send_tx.browser.browser {
308 eyre::bail!("--sponsor-url cannot be combined with --browser");
309 }
310 }
311
312 let browser = send_tx.browser.run::<N>().await?;
314
315 if unlocked && browser.is_none() {
320 if let Some(config_chain) = config.chain {
322 let current_chain_id = provider.get_chain_id().await?;
323 let config_chain_id = config_chain.id();
324 if config_chain_id != current_chain_id {
327 sh_warn!("Switching to chain {}", config_chain)?;
328 provider
329 .raw_request::<_, ()>(
330 "wallet_switchEthereumChain".into(),
331 [serde_json::json!({
332 "chainId": format!("0x{:x}", config_chain_id),
333 })],
334 )
335 .await?;
336 }
337 }
338
339 let chain = builder.chain();
340 if !confirm_auth_rpc_disclosure_during_build(&builder, config.sender, force)? {
341 return Ok(());
342 }
343 let (mut tx_request, _) = builder.build(config.sender).await?;
344 maybe_print_resolved_lane(
345 resolved_lane.as_ref(),
346 tx_request.nonce().unwrap_or_default(),
347 )?;
348 if let Some(sponsor) = &tempo_sponsor {
349 sponsor
350 .resolve_and_set_fee_token(
351 (!config.eth_rpc_curl).then_some(&provider),
352 Some(chain),
353 &mut tx_request,
354 )
355 .await?;
356 sponsor.attach_and_print::<N>(&mut tx_request, config.sender).await?;
357 }
358
359 cast_send(
360 provider,
361 tx_request,
362 tempo_sponsor.is_none().then_some(chain),
363 None,
364 send_tx.cast_async,
365 send_tx.sync,
366 send_tx.confirmations,
367 timeout,
368 tempo_sponsor.is_none() && !config.eth_rpc_curl,
369 )
370 .await?;
371 } else if let Some(browser) = browser {
374 let chain = builder.chain();
375 if !confirm_auth_rpc_disclosure_during_build(&builder, browser.address(), force)? {
376 return Ok(());
377 }
378 let (mut tx_request, _) =
379 builder.with_browser_wallet().build(browser.address()).await?;
380 maybe_print_resolved_lane(
381 resolved_lane.as_ref(),
382 tx_request.nonce().unwrap_or_default(),
383 )?;
384
385 if let Some(sponsor) = &tempo_sponsor {
386 sponsor
387 .resolve_and_set_fee_token(
388 (!config.eth_rpc_curl).then_some(&provider),
389 Some(chain),
390 &mut tx_request,
391 )
392 .await?;
393 sponsor.attach_and_print::<N>(&mut tx_request, browser.address()).await?;
394 } else {
395 let fee_token = resolve_and_set_fee_token(
396 (!config.eth_rpc_curl).then_some(&provider),
397 Some(chain),
398 &mut tx_request,
399 Some(browser.address()),
400 )
401 .await?;
402 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
403 .await?;
404 }
405
406 if chain.id() != browser.chain_id() {
407 sh_warn!("Switching browser wallet to chain {}", chain)?;
408 browser.switch_chain(chain.id()).await?;
409 }
410
411 let tx_hash = browser.send_transaction_via_browser(tx_request).await?;
412
413 let cast = CastTxSender::new(&provider);
414 cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout)
415 .await?;
416 } else if let Some(ak) = access_key {
418 let chain = builder.chain();
419 if !confirm_auth_rpc_disclosure_during_build(&builder, ak.account(), force)? {
420 return Ok(());
421 }
422 let (mut tx_request, _, prepared) = builder.build_with_tempo_wallet(&ak).await?;
423 maybe_print_resolved_lane(
424 resolved_lane.as_ref(),
425 tx_request.nonce().unwrap_or_default(),
426 )?;
427 if let Some(sponsor) = &tempo_sponsor {
428 sponsor
429 .resolve_and_set_fee_token(
430 (!config.eth_rpc_curl).then_some(&provider),
431 Some(chain),
432 &mut tx_request,
433 )
434 .await?;
435 sponsor.attach_and_print::<N>(&mut tx_request, prepared.account()).await?;
436 }
437 if let Some(sponsor_url) = sponsor_url.as_deref() {
438 cast_send_with_tempo_wallet_via_sponsor(
439 &provider,
440 tx_request,
441 &prepared,
442 sponsor_url,
443 send_tx.cast_async,
444 send_tx.sync,
445 send_tx.confirmations,
446 timeout,
447 )
448 .await?;
449 } else {
450 cast_send_with_tempo_wallet(
451 &provider,
452 tx_request,
453 &prepared,
454 tempo_sponsor.is_none().then_some(chain),
455 None,
456 send_tx.cast_async,
457 send_tx.sync,
458 send_tx.confirmations,
459 timeout,
460 tempo_sponsor.is_none() && !config.eth_rpc_curl,
461 )
462 .await?;
463 }
464 } else if let Some(sponsor_url) = sponsor_url {
468 let signer = match pre_resolved_signer {
469 Some(s) => s,
470 None => send_tx.eth.wallet.signer().await?,
471 };
472 let from = signer.address();
473
474 tx::validate_from_address(send_tx.eth.wallet.from, from)?;
475
476 if !confirm_auth_rpc_disclosure_during_build(&builder, &signer, force)? {
477 return Ok(());
478 }
479 let (mut tx_request, _) = builder.build(&signer).await?;
480 maybe_print_resolved_lane(
481 resolved_lane.as_ref(),
482 tx_request.nonce().unwrap_or_default(),
483 )?;
484
485 tx_request.set_fee_payer_signature(FEE_PAYER_SIGNATURE_MARKER);
486
487 let wallet = EthereumWallet::from(signer);
488 let connector = tempo::sponsor_relay_connector(&provider, &sponsor_url)?;
489 let provider = AlloyProviderBuilder::<_, _, N>::default()
490 .wallet(wallet)
491 .connect_with(&connector)
492 .await?;
493
494 cast_send(
495 provider,
496 tx_request,
497 None,
498 None,
499 send_tx.cast_async,
500 send_tx.sync,
501 send_tx.confirmations,
502 timeout,
503 false,
504 )
505 .await?;
506 } else {
511 let signer = match pre_resolved_signer {
512 Some(s) => s,
513 None => send_tx.eth.wallet.signer().await?,
514 };
515 let from = signer.address();
516
517 tx::validate_from_address(send_tx.eth.wallet.from, from)?;
518
519 let chain = builder.chain();
520 if !confirm_auth_rpc_disclosure_during_build(&builder, &signer, force)? {
521 return Ok(());
522 }
523 let (mut tx_request, _) = builder.build(&signer).await?;
524 maybe_print_resolved_lane(
525 resolved_lane.as_ref(),
526 tx_request.nonce().unwrap_or_default(),
527 )?;
528
529 if let Some(sponsor) = &tempo_sponsor {
530 sponsor
531 .resolve_and_set_fee_token(
532 (!config.eth_rpc_curl).then_some(&provider),
533 Some(chain),
534 &mut tx_request,
535 )
536 .await?;
537 sponsor.attach_and_print::<N>(&mut tx_request, from).await?;
538 }
539
540 let wallet = EthereumWallet::from(signer);
541 let provider = AlloyProviderBuilder::<_, _, N>::default()
542 .wallet(wallet)
543 .connect_provider(&provider);
544
545 cast_send(
546 provider,
547 tx_request,
548 tempo_sponsor.is_none().then_some(chain),
549 None,
550 send_tx.cast_async,
551 send_tx.sync,
552 send_tx.confirmations,
553 timeout,
554 tempo_sponsor.is_none() && !config.eth_rpc_curl,
555 )
556 .await?;
557 }
558
559 Ok(())
560 }
561}
562
563#[allow(clippy::too_many_arguments)]
564pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
565 provider: P,
566 mut tx: N::TransactionRequest,
567 chain: Option<Chain>,
568 fee_payer: Option<Address>,
569 cast_async: bool,
570 sync: bool,
571 confs: u64,
572 timeout: u64,
573 resolve_unknown_fee_token_symbol: bool,
574) -> Result<B256>
575where
576 N::TransactionRequest: Default + FoundryTransactionBuilder<N>,
577 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
578{
579 let fee_token = resolve_and_set_fee_token(
580 resolve_unknown_fee_token_symbol.then_some(&provider),
581 chain,
582 &mut tx,
583 fee_payer,
584 )
585 .await?;
586 maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token).await?;
587 let cast = CastTxSender::new(provider);
588
589 if sync {
590 let (tx_hash, receipt) = cast.send_sync(tx).await?;
593 sh_println!("{receipt}")?;
594 Ok(tx_hash)
595 } else {
596 let pending_tx = cast.send(tx).await?;
597 let tx_hash = *pending_tx.inner().tx_hash();
598 cast.print_tx_result(tx_hash, cast_async, confs, timeout).await?;
599 Ok(tx_hash)
600 }
601}
602
603pub(crate) async fn cast_send_raw<N: Network, P: Provider<N>>(
605 provider: &P,
606 raw_tx: &[u8],
607 sync: bool,
608) -> Result<(B256, Option<String>)>
609where
610 N::TransactionRequest: FoundryTransactionBuilder<N>,
611 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
612{
613 if sync {
614 let (tx_hash, receipt) = CastTxSender::new(provider).send_raw_sync(raw_tx).await?;
615 Ok((tx_hash, Some(receipt)))
616 } else {
617 let tx_hash = *provider.send_raw_transaction(raw_tx).await?.tx_hash();
618 Ok((tx_hash, None))
619 }
620}
621
622#[allow(clippy::too_many_arguments)]
624pub(crate) async fn cast_send_with_tempo_wallet<N: Network, P: Provider<N>>(
625 provider: &P,
626 mut tx: N::TransactionRequest,
627 wallet: &TempoAccountsWallet,
628 chain: Option<Chain>,
629 fee_payer: Option<Address>,
630 cast_async: bool,
631 sync: bool,
632 confirmations: u64,
633 timeout: u64,
634 resolve_unknown_fee_token_symbol: bool,
635) -> Result<B256>
636where
637 N::TransactionRequest: Default + FoundryTransactionBuilder<N>,
638 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
639{
640 let fee_token = resolve_and_set_fee_token(
641 resolve_unknown_fee_token_symbol.then_some(provider),
642 chain,
643 &mut tx,
644 fee_payer,
645 )
646 .await?;
647 maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(provider), fee_token).await?;
648 let raw_tx = tx.sign_with_tempo_wallet(wallet).await?;
649 let cast = CastTxSender::new(provider);
650 let (tx_hash, receipt) = cast_send_raw(provider, &raw_tx, sync).await?;
651
652 if let Some(receipt) = receipt {
653 sh_println!("{receipt}")?;
654 } else {
655 cast.print_tx_result(tx_hash, cast_async, confirmations, timeout).await?;
656 }
657 Ok(tx_hash)
658}
659
660#[allow(clippy::too_many_arguments)]
663pub(crate) async fn cast_send_with_tempo_wallet_via_sponsor<N: Network, P: Provider<N>>(
664 provider: &P,
665 mut tx: N::TransactionRequest,
666 wallet: &TempoAccountsWallet,
667 sponsor_url: &str,
668 cast_async: bool,
669 sync: bool,
670 confirmations: u64,
671 timeout: u64,
672) -> Result<B256>
673where
674 N::TransactionRequest: Default + FoundryTransactionBuilder<N>,
675 N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
676{
677 tx.set_fee_payer_signature(FEE_PAYER_SIGNATURE_MARKER);
678 let connector = tempo::sponsor_relay_connector(provider, sponsor_url)?;
679 let sponsor_provider =
680 AlloyProviderBuilder::<_, _, N>::default().connect_with(&connector).await?;
681 cast_send_with_tempo_wallet(
682 &sponsor_provider,
683 tx,
684 wallet,
685 None,
686 None,
687 cast_async,
688 sync,
689 confirmations,
690 timeout,
691 false,
692 )
693 .await
694}
695
696pub(crate) fn validate_sponsor_url(raw: &str) -> Result<()> {
698 let url = Url::parse(raw)
699 .map_err(|e| eyre::eyre!("--sponsor-url is not a valid URL ({raw}): {e}"))?;
700
701 match url.scheme() {
702 "https" => Ok(()),
703 "http" => {
704 let host = url.host_str().unwrap_or("");
705 if host == "localhost" || host == "127.0.0.1" {
706 return Ok(());
707 }
708 eyre::bail!(
709 "--sponsor-url must use https:// for non-local endpoints (got {raw}). \
710 The sponsor relay is a trusted third party; use an encrypted channel."
711 );
712 }
713 _ => {
714 eyre::bail!(
715 "--sponsor-url must start with https:// (got {raw}). \
716 The sponsor relay is a trusted third party; use an encrypted channel."
717 );
718 }
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725 use alloy_json_rpc::{RequestPacket, ResponsePacket};
726 use alloy_provider::mock::Asserter;
727 use alloy_rpc_client::RpcClient;
728 use alloy_rpc_types::TransactionRequest;
729 use alloy_transport::{TransportError, TransportFut, mock::MockTransport};
730 use foundry_wallets::utils::create_local_signer;
731 use std::{
732 sync::{Arc, Mutex},
733 task::{Context, Poll},
734 };
735 use tempo_alloy::rpc::TempoTransactionRequest;
736 use tower::Service;
737
738 #[derive(Clone)]
739 struct RecordingTransport {
740 inner: MockTransport,
741 methods: Arc<Mutex<Vec<String>>>,
742 }
743
744 impl Service<RequestPacket> for RecordingTransport {
745 type Response = ResponsePacket;
746 type Error = TransportError;
747 type Future = TransportFut<'static>;
748
749 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
750 self.inner.poll_ready(cx)
751 }
752
753 fn call(&mut self, req: RequestPacket) -> Self::Future {
754 if let RequestPacket::Single(req) = &req {
755 self.methods.lock().unwrap().push(req.method().to_string());
756 }
757 self.inner.call(req)
758 }
759 }
760
761 #[test]
762 fn test_validate_sponsor_url() {
763 assert!(validate_sponsor_url("https://sponsor.tempo.xyz/tp_abc").is_ok());
765 assert!(validate_sponsor_url("http://localhost:8545").is_ok());
766 assert!(validate_sponsor_url("http://127.0.0.1:8545").is_ok());
767
768 assert!(validate_sponsor_url("http://sponsor.tempo.xyz").is_err());
770 assert!(validate_sponsor_url("not-a-url").is_err());
771 assert!(validate_sponsor_url("http://localhost.evil.com").is_err());
773 assert!(validate_sponsor_url("http://127.0.0.1.evil.com").is_err());
774 }
775
776 #[test]
777 fn parses_gas_estimate_multiplier() {
778 let args = SendTxArgs::parse_from([
779 "cast-send",
780 "0x0000000000000000000000000000000000000000",
781 "--gas-estimate-multiplier",
782 "125",
783 ]);
784 assert_eq!(args.gas_estimate_multiplier, Some(125));
785 }
786
787 #[tokio::test]
788 async fn tempo_wallet_sync_send_uses_sync_rpc_method() {
789 let asserter = Asserter::new();
790 let tx_hash = B256::repeat_byte(0x11);
791 asserter.push_success(&serde_json::json!({
792 "type": "0x76",
793 "status": "0x1",
794 "cumulativeGasUsed": "0x5208",
795 "logs": [],
796 "logsBloom": format!("0x{}", "00".repeat(256)),
797 "transactionHash": tx_hash,
798 "transactionIndex": "0x0",
799 "blockHash": B256::repeat_byte(0x22),
800 "blockNumber": "0x1",
801 "gasUsed": "0x5208",
802 "effectiveGasPrice": "0x1",
803 "from": Address::ZERO,
804 "to": Address::ZERO,
805 "contractAddress": null,
806 "feeToken": Address::repeat_byte(0x55),
807 "feePayer": Address::ZERO,
808 }));
809 let methods = Arc::new(Mutex::new(Vec::new()));
810 let transport =
811 RecordingTransport { inner: MockTransport::new(asserter), methods: methods.clone() };
812 let provider = AlloyProviderBuilder::new_with_network::<TempoNetwork>()
813 .connect_client(RpcClient::new(transport, true));
814 let root = Address::repeat_byte(0x33);
815 let access_key = create_local_signer(
816 "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
817 )
818 .unwrap();
819 let wallet =
820 TempoAccountsWallet::from_secp256k1(root, access_key, None).with_chain_id(4217);
821 let tx = TempoTransactionRequest {
822 inner: TransactionRequest {
823 to: Some(Address::repeat_byte(0x44).into()),
824 nonce: Some(0),
825 gas: Some(100_000),
826 max_fee_per_gas: Some(1),
827 max_priority_fee_per_gas: Some(1),
828 chain_id: Some(4217),
829 ..Default::default()
830 },
831 ..Default::default()
832 };
833
834 let actual_hash = cast_send_with_tempo_wallet(
835 &provider, tx, &wallet, None, None, false, true, 1, 1, false,
836 )
837 .await
838 .unwrap();
839
840 assert_eq!(actual_hash, tx_hash);
841 assert_eq!(methods.lock().unwrap().as_slice(), ["eth_sendRawTransactionSync"]);
842 }
843}