1use alloy_network::AnyNetwork;
9use alloy_primitives::{Address, B256, Bytes, U256, aliases::TxHash};
10use alloy_provider::{Identity, Provider, ProviderBuilder, RootProvider};
11use alloy_rpc_types::TransactionRequest;
12use alloy_signer_local::PrivateKeySigner;
13use alloy_sol_types::SolCall;
14use async_trait::async_trait;
15use reqwest::Url;
16use safe_rs::{
17 Call, CallBuilder, ChainConfig, Eoa, EoaBatchResult, Error as SafeRsError, ExecutionResult,
18 IMultiSend, ISafe, Operation, Safe, SafeTxParams, Wallet, WalletBuilder,
19 encoding::{compute_safe_transaction_hash, encode_multisend_data},
20};
21use thiserror::Error;
22
23type AnyHttpProvider = RootProvider<AnyNetwork>;
24type SafeWallet = Wallet<Safe<AnyHttpProvider>>;
25type EoaWallet = Wallet<Eoa<AnyHttpProvider>>;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PreparedTransaction {
31 pub to: Address,
33 pub data: Bytes,
35 pub value: Option<U256>,
37}
38
39pub fn call_to_tx<C: SolCall>(to: Address, call: C, value: Option<U256>) -> PreparedTransaction {
41 PreparedTransaction {
42 to,
43 data: Bytes::from(call.abi_encode()),
44 value,
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct SubmittedTx {
54 pub tx_hash: Bytes,
56 pub success: bool,
58 pub index: Option<usize>,
60}
61
62#[derive(Debug, Clone)]
69pub struct PreparedSafeExecution {
70 pub safe_address: Address,
72 pub chain_id: u64,
74 pub transactions: Vec<PreparedTransaction>,
76 pub safe_tx: SafeTxParams,
78 pub safe_tx_hash: B256,
80}
81
82impl PreparedSafeExecution {
83 pub fn is_batch(&self) -> bool {
85 self.transactions.len() > 1
86 }
87
88 pub fn to_exec_transaction(&self, signatures: Bytes) -> PreparedTransaction {
90 call_to_tx(
91 self.safe_address,
92 ISafe::execTransactionCall {
93 to: self.safe_tx.to,
94 value: self.safe_tx.value,
95 data: self.safe_tx.data.clone(),
96 operation: self.safe_tx.operation.as_u8(),
97 safeTxGas: self.safe_tx.safe_tx_gas,
98 baseGas: self.safe_tx.base_gas,
99 gasPrice: self.safe_tx.gas_price,
100 gasToken: self.safe_tx.gas_token,
101 refundReceiver: self.safe_tx.refund_receiver,
102 signatures,
103 },
104 None,
105 )
106 }
107}
108
109pub struct SafeExecutionBuilder {
115 safe_address: Address,
116 provider: AnyHttpProvider,
117}
118
119impl SafeExecutionBuilder {
120 pub fn connect(rpc_url: &str, safe_address: Address) -> Result<Self, RunnerError> {
122 let rpc_url = parse_rpc_url(rpc_url)?;
123 let provider = build_read_provider(rpc_url);
124 Ok(Self {
125 safe_address,
126 provider,
127 })
128 }
129
130 pub fn safe_address(&self) -> Address {
132 self.safe_address
133 }
134
135 pub async fn prepare_transactions(
137 &self,
138 txs: Vec<PreparedTransaction>,
139 ) -> Result<PreparedSafeExecution, RunnerError> {
140 let chain_id = self
141 .provider
142 .get_chain_id()
143 .await
144 .map_err(|err| RunnerError::Transport(err.to_string()))?;
145 let nonce = ISafe::new(self.safe_address, &self.provider)
146 .nonce()
147 .call()
148 .await
149 .map_err(|err| RunnerError::Transport(err.to_string()))?;
150 prepare_safe_execution(self.safe_address, chain_id, nonce, txs)
151 }
152}
153
154#[async_trait]
156pub trait BatchRun: Send {
157 fn add_transaction(&mut self, tx: PreparedTransaction);
159
160 async fn run(&mut self) -> Result<Vec<SubmittedTx>, RunnerError>;
162}
163
164pub struct BufferedBatchRun<'a, R: ContractRunner + ?Sized> {
166 runner: &'a R,
167 txs: Vec<PreparedTransaction>,
168}
169
170impl<'a, R: ContractRunner + ?Sized> BufferedBatchRun<'a, R> {
171 pub fn new(runner: &'a R) -> Self {
173 Self {
174 runner,
175 txs: Vec::new(),
176 }
177 }
178}
179
180#[async_trait]
181impl<R: ContractRunner + ?Sized> BatchRun for BufferedBatchRun<'_, R> {
182 fn add_transaction(&mut self, tx: PreparedTransaction) {
183 self.txs.push(tx);
184 }
185
186 async fn run(&mut self) -> Result<Vec<SubmittedTx>, RunnerError> {
187 self.runner
188 .send_transactions(std::mem::take(&mut self.txs))
189 .await
190 }
191}
192
193#[async_trait]
195pub trait ContractRunner: Send + Sync {
196 fn sender_address(&self) -> Address;
198
199 fn address(&self) -> Option<Address> {
201 Some(self.sender_address())
202 }
203
204 async fn estimate_gas(&self, _tx: PreparedTransaction) -> Result<u64, RunnerError> {
206 Err(RunnerError::Unsupported(
207 "gas estimation is not supported by this runner".to_string(),
208 ))
209 }
210
211 async fn call(&self, _tx: PreparedTransaction) -> Result<Bytes, RunnerError> {
213 Err(RunnerError::Unsupported(
214 "contract calls are not supported by this runner".to_string(),
215 ))
216 }
217
218 async fn resolve_name(&self, name: &str) -> Result<Option<Address>, RunnerError> {
223 Ok(name.parse().ok())
224 }
225
226 fn send_batch_transaction(&self) -> Box<dyn BatchRun + '_> {
228 Box::new(BufferedBatchRun::new(self))
229 }
230
231 async fn send_transactions(
233 &self,
234 txs: Vec<PreparedTransaction>,
235 ) -> Result<Vec<SubmittedTx>, RunnerError>;
236}
237
238#[derive(Debug, Error)]
240pub enum RunnerError {
241 #[error("runner refused to send transactions: {0}")]
242 Rejected(String),
243 #[error("runner transport error: {0}")]
244 Transport(String),
245 #[error("runner capability unsupported: {0}")]
246 Unsupported(String),
247}
248
249fn tx_hash_to_bytes(tx_hash: TxHash) -> Bytes {
250 Bytes::copy_from_slice(tx_hash.as_slice())
251}
252
253fn prepared_to_request(from: Option<Address>, tx: PreparedTransaction) -> TransactionRequest {
254 let mut request = TransactionRequest::default()
255 .to(tx.to)
256 .input(tx.data.into())
257 .with_input_and_data();
258
259 if let Some(from) = from {
260 request = request.from(from);
261 }
262
263 if let Some(value) = tx.value {
264 request = request.value(value);
265 }
266
267 request
268}
269
270fn submitted_from_execution_result(result: ExecutionResult) -> Vec<SubmittedTx> {
271 vec![SubmittedTx {
272 tx_hash: tx_hash_to_bytes(result.tx_hash),
273 success: result.success,
274 index: None,
275 }]
276}
277
278fn submitted_from_eoa_result(result: EoaBatchResult) -> Vec<SubmittedTx> {
279 result
280 .results
281 .into_iter()
282 .map(|tx| SubmittedTx {
283 tx_hash: tx_hash_to_bytes(tx.tx_hash),
284 success: tx.success,
285 index: Some(tx.index),
286 })
287 .collect()
288}
289
290fn map_safe_error(error: SafeRsError) -> RunnerError {
291 match error {
292 SafeRsError::Provider(_) | SafeRsError::Fetch { .. } | SafeRsError::UnsupportedChain(_) => {
293 RunnerError::Transport(error.to_string())
294 }
295 _ => RunnerError::Rejected(error.to_string()),
296 }
297}
298
299fn parse_rpc_url(rpc_url: &str) -> Result<Url, RunnerError> {
300 rpc_url
301 .parse()
302 .map_err(|err| RunnerError::Rejected(format!("invalid rpc url: {err}")))
303}
304
305fn parse_private_key(private_key: &str) -> Result<PrivateKeySigner, RunnerError> {
306 private_key
307 .parse()
308 .map_err(|err| RunnerError::Rejected(format!("invalid private key: {err}")))
309}
310
311fn build_read_provider(rpc_url: Url) -> AnyHttpProvider {
312 ProviderBuilder::<Identity, Identity, AnyNetwork>::default().connect_http(rpc_url)
313}
314
315fn prepared_to_safe_call(tx: &PreparedTransaction) -> Call {
316 Call::new(tx.to, tx.value.unwrap_or_default(), tx.data.clone())
317}
318
319fn build_safe_inner_tx(
320 txs: &[PreparedTransaction],
321 chain_config: &ChainConfig,
322) -> (Address, U256, Bytes, Operation) {
323 if txs.len() == 1 {
324 let tx = &txs[0];
325 (
326 tx.to,
327 tx.value.unwrap_or_default(),
328 tx.data.clone(),
329 Operation::Call,
330 )
331 } else {
332 let calls = txs.iter().map(prepared_to_safe_call).collect::<Vec<_>>();
333 let transactions = encode_multisend_data(&calls);
334 let calldata = Bytes::from(IMultiSend::multiSendCall { transactions }.abi_encode());
335 (
336 chain_config.addresses.multi_send,
337 U256::ZERO,
338 calldata,
339 Operation::DelegateCall,
340 )
341 }
342}
343
344fn prepare_safe_execution(
345 safe_address: Address,
346 chain_id: u64,
347 nonce: U256,
348 txs: Vec<PreparedTransaction>,
349) -> Result<PreparedSafeExecution, RunnerError> {
350 if txs.is_empty() {
351 return Err(RunnerError::Rejected(
352 "no transactions provided".to_string(),
353 ));
354 }
355
356 let chain_config = ChainConfig::new(chain_id);
357 let (to, value, data, operation) = build_safe_inner_tx(&txs, &chain_config);
358 let safe_tx = SafeTxParams {
359 to,
360 value,
361 data,
362 operation,
363 safe_tx_gas: U256::ZERO,
364 base_gas: U256::ZERO,
365 gas_price: U256::ZERO,
366 gas_token: Address::ZERO,
367 refund_receiver: Address::ZERO,
368 nonce,
369 };
370 let safe_tx_hash = compute_safe_transaction_hash(chain_id, safe_address, &safe_tx);
371
372 Ok(PreparedSafeExecution {
373 safe_address,
374 chain_id,
375 transactions: txs,
376 safe_tx,
377 safe_tx_hash,
378 })
379}
380
381fn attach_prepared_transactions<B>(builder: B, txs: Vec<PreparedTransaction>) -> B
382where
383 B: CallBuilder,
384{
385 txs.into_iter().fold(builder, |builder, tx| {
386 builder.add_raw(tx.to, tx.value.unwrap_or_default(), tx.data)
387 })
388}
389
390pub struct SafeContractRunner {
395 wallet: SafeWallet,
396 provider: AnyHttpProvider,
397}
398
399impl SafeContractRunner {
400 pub async fn connect(
403 rpc_url: &str,
404 private_key: &str,
405 safe_address: Address,
406 ) -> Result<Self, RunnerError> {
407 let rpc_url = parse_rpc_url(rpc_url)?;
408 let signer = parse_private_key(private_key)?;
409 let provider = build_read_provider(rpc_url);
410 let wallet = WalletBuilder::new(provider.clone(), signer)
411 .connect(safe_address)
412 .await
413 .map_err(map_safe_error)?;
414 wallet
415 .inner()
416 .verify_single_owner()
417 .await
418 .map_err(map_safe_error)?;
419 Ok(Self { wallet, provider })
420 }
421
422 pub async fn prepare_transactions(
425 &self,
426 txs: Vec<PreparedTransaction>,
427 ) -> Result<PreparedSafeExecution, RunnerError> {
428 let chain_id = self
429 .provider
430 .get_chain_id()
431 .await
432 .map_err(|err| RunnerError::Transport(err.to_string()))?;
433 let nonce = ISafe::new(self.wallet.address(), &self.provider)
434 .nonce()
435 .call()
436 .await
437 .map_err(|err| RunnerError::Transport(err.to_string()))?;
438 prepare_safe_execution(self.wallet.address(), chain_id, nonce, txs)
439 }
440}
441
442#[async_trait]
443impl ContractRunner for SafeContractRunner {
444 fn sender_address(&self) -> Address {
445 self.wallet.address()
446 }
447
448 async fn estimate_gas(&self, tx: PreparedTransaction) -> Result<u64, RunnerError> {
449 self.provider
450 .estimate_gas(prepared_to_request(self.address(), tx).into())
451 .await
452 .map_err(|err| RunnerError::Transport(err.to_string()))
453 }
454
455 async fn call(&self, tx: PreparedTransaction) -> Result<Bytes, RunnerError> {
456 self.provider
457 .call(prepared_to_request(self.address(), tx).into())
458 .await
459 .map_err(|err| RunnerError::Transport(err.to_string()))
460 }
461
462 async fn send_transactions(
463 &self,
464 txs: Vec<PreparedTransaction>,
465 ) -> Result<Vec<SubmittedTx>, RunnerError> {
466 if txs.is_empty() {
467 return Err(RunnerError::Rejected(
468 "no transactions provided".to_string(),
469 ));
470 }
471
472 let result = attach_prepared_transactions(self.wallet.batch(), txs)
473 .execute()
474 .await
475 .map_err(map_safe_error)?;
476 Ok(submitted_from_execution_result(result))
477 }
478}
479
480pub struct EoaContractRunner {
485 wallet: EoaWallet,
486 provider: AnyHttpProvider,
487}
488
489impl EoaContractRunner {
490 pub async fn connect(rpc_url: &str, private_key: &str) -> Result<Self, RunnerError> {
492 let rpc_url = parse_rpc_url(rpc_url)?;
493 let signer = parse_private_key(private_key)?;
494 let provider = build_read_provider(rpc_url.clone());
495 let wallet = WalletBuilder::new(provider.clone(), signer)
496 .connect_eoa(rpc_url)
497 .await
498 .map_err(map_safe_error)?;
499 Ok(Self { wallet, provider })
500 }
501}
502
503#[async_trait]
504impl ContractRunner for EoaContractRunner {
505 fn sender_address(&self) -> Address {
506 self.wallet.address()
507 }
508
509 async fn estimate_gas(&self, tx: PreparedTransaction) -> Result<u64, RunnerError> {
510 self.provider
511 .estimate_gas(prepared_to_request(self.address(), tx).into())
512 .await
513 .map_err(|err| RunnerError::Transport(err.to_string()))
514 }
515
516 async fn call(&self, tx: PreparedTransaction) -> Result<Bytes, RunnerError> {
517 self.provider
518 .call(prepared_to_request(self.address(), tx).into())
519 .await
520 .map_err(|err| RunnerError::Transport(err.to_string()))
521 }
522
523 async fn send_transactions(
524 &self,
525 txs: Vec<PreparedTransaction>,
526 ) -> Result<Vec<SubmittedTx>, RunnerError> {
527 if txs.is_empty() {
528 return Err(RunnerError::Rejected(
529 "no transactions provided".to_string(),
530 ));
531 }
532
533 let result = attach_prepared_transactions(self.wallet.batch(), txs)
534 .execute()
535 .await
536 .map_err(map_safe_error)?;
537 Ok(submitted_from_eoa_result(result))
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use alloy_node_bindings::Anvil;
545 use alloy_primitives::{TxKind, address};
546 use alloy_provider::Provider;
547 use safe_rs::{Call, EoaTxResult, SimulationResult};
548 use std::process::{Command, Stdio};
549 use std::sync::Mutex;
550
551 const ANVIL_FIRST_PRIVATE_KEY: &str =
552 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
553 const ANVIL_FIRST_ADDRESS: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
554 const ANVIL_SECOND_ADDRESS: Address = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
555
556 fn anvil_binary_available() -> bool {
557 Command::new("anvil")
558 .arg("--version")
559 .stdout(Stdio::null())
560 .stderr(Stdio::null())
561 .status()
562 .is_ok()
563 }
564
565 struct RecordingRunner {
566 sender: Address,
567 sent: Mutex<Vec<Vec<PreparedTransaction>>>,
568 }
569
570 impl Default for RecordingRunner {
571 fn default() -> Self {
572 Self {
573 sender: Address::repeat_byte(0x55),
574 sent: Mutex::new(Vec::new()),
575 }
576 }
577 }
578
579 #[async_trait]
580 impl ContractRunner for RecordingRunner {
581 fn sender_address(&self) -> Address {
582 self.sender
583 }
584
585 async fn send_transactions(
586 &self,
587 txs: Vec<PreparedTransaction>,
588 ) -> Result<Vec<SubmittedTx>, RunnerError> {
589 self.sent.lock().expect("record sent txs").push(txs);
590 Ok(vec![SubmittedTx {
591 tx_hash: Bytes::copy_from_slice(&[0x77; 32]),
592 success: true,
593 index: None,
594 }])
595 }
596 }
597
598 #[derive(Default)]
599 struct StubBuilder {
600 calls: Vec<Call>,
601 simulation_result: Option<SimulationResult>,
602 }
603
604 impl CallBuilder for StubBuilder {
605 fn calls_mut(&mut self) -> &mut Vec<Call> {
606 &mut self.calls
607 }
608
609 fn calls(&self) -> &Vec<Call> {
610 &self.calls
611 }
612
613 fn with_gas_limit(self, _gas_limit: u64) -> Self {
614 self
615 }
616
617 async fn simulate(self) -> safe_rs::Result<Self> {
618 Ok(self)
619 }
620
621 fn simulation_result(&self) -> Option<&SimulationResult> {
622 self.simulation_result.as_ref()
623 }
624
625 fn simulation_success(self) -> safe_rs::Result<Self> {
626 Ok(self)
627 }
628 }
629
630 #[test]
631 fn attach_prepared_transactions_preserves_order_and_default_value() {
632 let txs = vec![
633 PreparedTransaction {
634 to: Address::repeat_byte(0x11),
635 data: Bytes::from_static(&[0xaa, 0xbb]),
636 value: None,
637 },
638 PreparedTransaction {
639 to: Address::repeat_byte(0x22),
640 data: Bytes::from_static(&[0xcc]),
641 value: Some(U256::from(42u64)),
642 },
643 ];
644
645 let builder = attach_prepared_transactions(StubBuilder::default(), txs);
646 assert_eq!(builder.calls.len(), 2);
647 assert_eq!(builder.calls[0].to, Address::repeat_byte(0x11));
648 assert_eq!(builder.calls[0].value, U256::ZERO);
649 assert_eq!(builder.calls[0].data, Bytes::from_static(&[0xaa, 0xbb]));
650 assert_eq!(builder.calls[1].to, Address::repeat_byte(0x22));
651 assert_eq!(builder.calls[1].value, U256::from(42u64));
652 assert_eq!(builder.calls[1].data, Bytes::from_static(&[0xcc]));
653 }
654
655 #[test]
656 fn submitted_from_execution_result_returns_single_hash() {
657 let submitted = submitted_from_execution_result(ExecutionResult {
658 tx_hash: TxHash::repeat_byte(0x44),
659 success: true,
660 });
661
662 assert_eq!(submitted.len(), 1);
663 assert_eq!(submitted[0].tx_hash, Bytes::copy_from_slice(&[0x44; 32]));
664 assert!(submitted[0].success);
665 assert_eq!(submitted[0].index, None);
666 }
667
668 #[test]
669 fn submitted_from_eoa_result_preserves_order() {
670 let submitted = submitted_from_eoa_result(EoaBatchResult {
671 results: vec![
672 EoaTxResult {
673 tx_hash: TxHash::repeat_byte(0x01),
674 success: true,
675 index: 0,
676 },
677 EoaTxResult {
678 tx_hash: TxHash::repeat_byte(0x02),
679 success: true,
680 index: 1,
681 },
682 ],
683 success_count: 2,
684 failure_count: 0,
685 first_failure: None,
686 });
687
688 assert_eq!(submitted.len(), 2);
689 assert_eq!(submitted[0].tx_hash, Bytes::copy_from_slice(&[0x01; 32]));
690 assert_eq!(submitted[1].tx_hash, Bytes::copy_from_slice(&[0x02; 32]));
691 assert!(submitted[0].success);
692 assert!(submitted[1].success);
693 assert_eq!(submitted[0].index, Some(0));
694 assert_eq!(submitted[1].index, Some(1));
695 }
696
697 #[test]
698 fn prepare_safe_execution_uses_direct_call_for_single_transaction() {
699 let tx = PreparedTransaction {
700 to: Address::repeat_byte(0x11),
701 data: Bytes::from_static(&[0xaa, 0xbb]),
702 value: Some(U256::from(7u64)),
703 };
704
705 let prepared = prepare_safe_execution(
706 Address::repeat_byte(0x44),
707 100,
708 U256::from(9u64),
709 vec![tx.clone()],
710 )
711 .expect("prepare safe execution");
712
713 assert!(!prepared.is_batch());
714 assert_eq!(prepared.transactions, vec![tx.clone()]);
715 assert_eq!(prepared.safe_tx.to, tx.to);
716 assert_eq!(prepared.safe_tx.value, U256::from(7u64));
717 assert_eq!(prepared.safe_tx.data, tx.data);
718 assert_eq!(prepared.safe_tx.operation, Operation::Call);
719 assert_eq!(prepared.safe_tx.nonce, U256::from(9u64));
720 assert_eq!(
721 prepared.safe_tx_hash,
722 compute_safe_transaction_hash(100, Address::repeat_byte(0x44), &prepared.safe_tx)
723 );
724 }
725
726 #[test]
727 fn prepare_safe_execution_uses_multisend_for_batches() {
728 let txs = vec![
729 PreparedTransaction {
730 to: Address::repeat_byte(0x11),
731 data: Bytes::from_static(&[0xaa]),
732 value: None,
733 },
734 PreparedTransaction {
735 to: Address::repeat_byte(0x22),
736 data: Bytes::from_static(&[0xbb, 0xcc]),
737 value: Some(U256::from(5u64)),
738 },
739 ];
740 let calls = txs.iter().map(prepared_to_safe_call).collect::<Vec<_>>();
741 let expected_multisend = encode_multisend_data(&calls);
742 let expected_data = Bytes::from(
743 IMultiSend::multiSendCall {
744 transactions: expected_multisend,
745 }
746 .abi_encode(),
747 );
748
749 let prepared = prepare_safe_execution(
750 Address::repeat_byte(0x55),
751 100,
752 U256::from(3u64),
753 txs.clone(),
754 )
755 .expect("prepare safe execution");
756
757 assert!(prepared.is_batch());
758 assert_eq!(prepared.transactions, txs);
759 assert_eq!(
760 prepared.safe_tx.to,
761 ChainConfig::new(100).addresses.multi_send
762 );
763 assert_eq!(prepared.safe_tx.value, U256::ZERO);
764 assert_eq!(prepared.safe_tx.operation, Operation::DelegateCall);
765 assert_eq!(prepared.safe_tx.data, expected_data);
766 }
767
768 #[test]
769 fn prepared_safe_execution_builds_exec_transaction_call() {
770 let prepared = prepare_safe_execution(
771 Address::repeat_byte(0x66),
772 100,
773 U256::from(1u64),
774 vec![PreparedTransaction {
775 to: Address::repeat_byte(0x11),
776 data: Bytes::from_static(&[0xab, 0xcd]),
777 value: Some(U256::from(2u64)),
778 }],
779 )
780 .expect("prepare safe execution");
781
782 let tx = prepared.to_exec_transaction(Bytes::from_static(&[0x12, 0x34]));
783 let decoded =
784 ISafe::execTransactionCall::abi_decode(&tx.data).expect("decode execTransaction call");
785
786 assert_eq!(tx.to, prepared.safe_address);
787 assert_eq!(tx.value, None);
788 assert_eq!(decoded.to, prepared.safe_tx.to);
789 assert_eq!(decoded.value, prepared.safe_tx.value);
790 assert_eq!(decoded.data, prepared.safe_tx.data);
791 assert_eq!(decoded.operation, prepared.safe_tx.operation.as_u8());
792 assert_eq!(decoded.safeTxGas, prepared.safe_tx.safe_tx_gas);
793 assert_eq!(decoded.baseGas, prepared.safe_tx.base_gas);
794 assert_eq!(decoded.gasPrice, prepared.safe_tx.gas_price);
795 assert_eq!(decoded.gasToken, prepared.safe_tx.gas_token);
796 assert_eq!(decoded.refundReceiver, prepared.safe_tx.refund_receiver);
797 assert_eq!(decoded.signatures, Bytes::from_static(&[0x12, 0x34]));
798 }
799
800 #[test]
801 fn prepare_safe_execution_rejects_empty_batches() {
802 let result = prepare_safe_execution(Address::ZERO, 100, U256::ZERO, Vec::new());
803
804 assert!(matches!(result, Err(RunnerError::Rejected(_))));
805 }
806
807 #[test]
808 fn prepared_to_request_preserves_fields_and_duplicates_input_data() {
809 let tx = PreparedTransaction {
810 to: ANVIL_SECOND_ADDRESS,
811 data: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
812 value: Some(U256::from(321u64)),
813 };
814
815 let request = prepared_to_request(Some(ANVIL_FIRST_ADDRESS), tx.clone());
816
817 assert_eq!(request.from, Some(ANVIL_FIRST_ADDRESS));
818 assert_eq!(request.to, Some(TxKind::Call(ANVIL_SECOND_ADDRESS)));
819 assert_eq!(request.value, Some(U256::from(321u64)));
820 assert_eq!(request.input.input(), Some(&tx.data));
821 assert_eq!(request.input.data.as_ref(), Some(&tx.data));
822 }
823
824 #[test]
825 fn contract_runner_address_defaults_to_sender_address() {
826 let runner = RecordingRunner::default();
827
828 assert_eq!(runner.address(), Some(runner.sender_address()));
829 }
830
831 #[tokio::test]
832 async fn batch_runner_buffers_and_forwards_transactions() {
833 let runner = RecordingRunner::default();
834 let mut batch = runner.send_batch_transaction();
835
836 batch.add_transaction(PreparedTransaction {
837 to: Address::repeat_byte(0x11),
838 data: Bytes::from_static(&[0xaa]),
839 value: None,
840 });
841 batch.add_transaction(PreparedTransaction {
842 to: Address::repeat_byte(0x22),
843 data: Bytes::from_static(&[0xbb, 0xcc]),
844 value: Some(U256::from(9u64)),
845 });
846
847 let submitted = batch.run().await.expect("batch run succeeds");
848 let sent = runner.sent.lock().expect("inspect recorded batch");
849
850 assert_eq!(sent.len(), 1);
851 assert_eq!(sent[0].len(), 2);
852 assert_eq!(sent[0][0].to, Address::repeat_byte(0x11));
853 assert_eq!(sent[0][1].to, Address::repeat_byte(0x22));
854 assert_eq!(sent[0][1].value, Some(U256::from(9u64)));
855 assert_eq!(submitted[0].tx_hash, Bytes::copy_from_slice(&[0x77; 32]));
856 assert!(submitted[0].success);
857 }
858
859 #[tokio::test]
860 async fn default_resolve_name_parses_hex_address_strings() {
861 let runner = RecordingRunner::default();
862
863 assert_eq!(
864 runner
865 .resolve_name(&ANVIL_FIRST_ADDRESS.to_string())
866 .await
867 .expect("default resolver succeeds"),
868 Some(ANVIL_FIRST_ADDRESS)
869 );
870 assert_eq!(
871 runner
872 .resolve_name("alice.eth")
873 .await
874 .expect("default resolver succeeds"),
875 None
876 );
877 }
878
879 #[tokio::test]
880 async fn safe_runner_rejects_invalid_private_key() {
881 let result = SafeContractRunner::connect(
882 "https://rpc.example.invalid",
883 "not-a-private-key",
884 Address::ZERO,
885 )
886 .await;
887
888 assert!(matches!(result, Err(RunnerError::Rejected(_))));
889 }
890
891 #[tokio::test]
892 async fn eoa_runner_rejects_invalid_private_key() {
893 let result = EoaContractRunner::connect("https://rpc.example.invalid", "bad-key").await;
894
895 assert!(matches!(result, Err(RunnerError::Rejected(_))));
896 }
897
898 #[test]
899 fn safe_execution_builder_rejects_invalid_rpc_url() {
900 let result = SafeExecutionBuilder::connect("not-a-url", Address::ZERO);
901
902 assert!(matches!(result, Err(RunnerError::Rejected(_))));
903 }
904
905 #[tokio::test]
906 async fn eoa_runner_executes_value_transfer_on_anvil() {
907 if !anvil_binary_available() {
908 eprintln!("skipping anvil-backed test because `anvil` is not installed");
909 return;
910 }
911
912 let anvil = Anvil::new().spawn();
913 let provider = build_read_provider(anvil.endpoint_url());
914 let before = provider
915 .get_balance(ANVIL_SECOND_ADDRESS)
916 .await
917 .expect("balance before transfer");
918 let runner = EoaContractRunner::connect(&anvil.endpoint(), ANVIL_FIRST_PRIVATE_KEY)
919 .await
920 .expect("connect EOA runner");
921
922 assert_eq!(runner.sender_address(), ANVIL_FIRST_ADDRESS);
923
924 let submitted = runner
925 .send_transactions(vec![PreparedTransaction {
926 to: ANVIL_SECOND_ADDRESS,
927 data: Bytes::new(),
928 value: Some(U256::from(123u64)),
929 }])
930 .await
931 .expect("EOA transfer executes");
932
933 assert_eq!(submitted.len(), 1);
934
935 let after = provider
936 .get_balance(ANVIL_SECOND_ADDRESS)
937 .await
938 .expect("balance after transfer");
939 assert_eq!(after - before, U256::from(123u64));
940 }
941
942 #[tokio::test]
943 async fn eoa_runner_exposes_estimate_and_call_helpers() {
944 if !anvil_binary_available() {
945 eprintln!("skipping anvil-backed test because `anvil` is not installed");
946 return;
947 }
948
949 let anvil = Anvil::new().spawn();
950 let runner = EoaContractRunner::connect(&anvil.endpoint(), ANVIL_FIRST_PRIVATE_KEY)
951 .await
952 .expect("connect EOA runner");
953
954 let gas = runner
955 .estimate_gas(PreparedTransaction {
956 to: ANVIL_SECOND_ADDRESS,
957 data: Bytes::new(),
958 value: Some(U256::from(1u64)),
959 })
960 .await
961 .expect("estimate gas");
962 assert!(gas > 0);
963
964 let call_output = runner
965 .call(PreparedTransaction {
966 to: ANVIL_SECOND_ADDRESS,
967 data: Bytes::new(),
968 value: None,
969 })
970 .await
971 .expect("eth_call succeeds");
972 assert_eq!(call_output, Bytes::new());
973 }
974
975 #[tokio::test]
976 async fn safe_runner_rejects_non_safe_address_on_plain_anvil() {
977 if !anvil_binary_available() {
978 eprintln!("skipping anvil-backed test because `anvil` is not installed");
979 return;
980 }
981
982 let anvil = Anvil::new().spawn();
983 let result = SafeContractRunner::connect(
984 &anvil.endpoint(),
985 ANVIL_FIRST_PRIVATE_KEY,
986 ANVIL_FIRST_ADDRESS,
987 )
988 .await;
989
990 assert!(matches!(result, Err(RunnerError::Transport(_))));
991 }
992}