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