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