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