1pub mod pending_nonce_manager;
41pub mod types;
42
43use alloy_network::{Ethereum as AlloyEthereum, EthereumWallet, NetworkWallet, TransactionBuilder};
44use alloy_primitives::{Address, B256, Bytes, U256};
45use alloy_provider::fillers::{
46 BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller,
47};
48use alloy_provider::{
49 Identity, PendingTransactionError, Provider, ProviderBuilder, RootProvider, WalletProvider,
50};
51use alloy_rpc_client::RpcClient;
52use alloy_rpc_types_eth::{BlockId, TransactionReceipt, TransactionRequest};
53use alloy_signer::Signer;
54use alloy_signer_local::PrivateKeySigner;
55use alloy_transport::TransportError;
56use alloy_transport::layers::{FallbackLayer, ThrottleLayer};
57use alloy_transport_http::Http;
58use serde::{Deserialize, Serialize};
59use std::fmt::{Display, Formatter};
60use std::num::NonZeroUsize;
61use std::ops::Mul;
62use std::sync::Arc;
63use std::sync::atomic::{AtomicUsize, Ordering};
64use tower::ServiceBuilder;
65use tracing::Instrument;
66
67use crate::chain::{ChainId, ChainProviderOps, DeployedTokenAmount, FromConfig};
68use crate::config::{Eip155ChainConfig, RpcConfig};
69use crate::util::money_amount::{MoneyAmount, MoneyAmountParseError};
70
71pub use pending_nonce_manager::*;
72pub use types::*;
73
74pub type InnerFiller = JoinFill<
76 GasFiller,
77 JoinFill<BlobGasFiller, JoinFill<NonceFiller<PendingNonceManager>, ChainIdFiller>>,
78>;
79
80pub type InnerProvider = FillProvider<
85 JoinFill<JoinFill<Identity, InnerFiller>, WalletFiller<EthereumWallet>>,
86 RootProvider,
87>;
88
89pub const EIP155_NAMESPACE: &str = "eip155";
91
92#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
108pub struct Eip155ChainReference(u64);
109
110impl Eip155ChainReference {
111 pub fn as_chain_id(&self) -> ChainId {
113 ChainId::new(EIP155_NAMESPACE, self.0.to_string())
114 }
115}
116
117impl From<Eip155ChainReference> for ChainId {
118 fn from(value: Eip155ChainReference) -> Self {
119 ChainId::new(EIP155_NAMESPACE, value.0.to_string())
120 }
121}
122
123impl From<&Eip155ChainReference> for ChainId {
124 fn from(value: &Eip155ChainReference) -> Self {
125 ChainId::new(EIP155_NAMESPACE, value.0.to_string())
126 }
127}
128
129impl TryFrom<ChainId> for Eip155ChainReference {
130 type Error = Eip155ChainReferenceFormatError;
131
132 fn try_from(value: ChainId) -> Result<Self, Self::Error> {
133 if value.namespace != EIP155_NAMESPACE {
134 return Err(Eip155ChainReferenceFormatError::InvalidNamespace(
135 value.namespace,
136 ));
137 }
138 let chain_id: u64 = value.reference.parse().map_err(|_| {
139 Eip155ChainReferenceFormatError::InvalidReference(value.reference.clone())
140 })?;
141 Ok(Eip155ChainReference(chain_id))
142 }
143}
144
145impl TryFrom<&ChainId> for Eip155ChainReference {
146 type Error = Eip155ChainReferenceFormatError;
147
148 fn try_from(value: &ChainId) -> Result<Self, Self::Error> {
149 if value.namespace != EIP155_NAMESPACE {
150 return Err(Eip155ChainReferenceFormatError::InvalidNamespace(
151 value.namespace.clone(),
152 ));
153 }
154 let chain_id: u64 = value.reference.parse().map_err(|_| {
155 Eip155ChainReferenceFormatError::InvalidReference(value.reference.clone())
156 })?;
157 Ok(Eip155ChainReference(chain_id))
158 }
159}
160
161#[derive(Debug, thiserror::Error)]
163pub enum Eip155ChainReferenceFormatError {
164 #[error("Invalid namespace {0}, expected eip155")]
166 InvalidNamespace(String),
167 #[error("Invalid eip155 chain reference {0}")]
169 InvalidReference(String),
170}
171
172impl Eip155ChainReference {
173 pub fn new(chain_id: u64) -> Self {
175 Self(chain_id)
176 }
177
178 pub fn inner(&self) -> u64 {
180 self.0
181 }
182}
183
184impl Display for Eip155ChainReference {
185 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186 write!(f, "{}", self.0)
187 }
188}
189
190#[derive(Debug, Clone, Eq, PartialEq, Hash)]
210#[allow(dead_code)] pub struct Eip155TokenDeployment {
212 pub chain_reference: Eip155ChainReference,
214 pub address: Address,
216 pub decimals: u8,
218 pub eip712: Option<TokenDeploymentEip712>,
220}
221
222#[allow(dead_code)] impl Eip155TokenDeployment {
224 pub fn amount<V: Into<TokenAmount>>(
228 &self,
229 v: V,
230 ) -> DeployedTokenAmount<U256, Eip155TokenDeployment> {
231 DeployedTokenAmount {
232 amount: v.into().0,
233 token: self.clone(),
234 }
235 }
236
237 pub fn parse<V>(
260 &self,
261 v: V,
262 ) -> Result<DeployedTokenAmount<U256, Eip155TokenDeployment>, MoneyAmountParseError>
263 where
264 V: TryInto<MoneyAmount>,
265 MoneyAmountParseError: From<<V as TryInto<MoneyAmount>>::Error>,
266 {
267 let money_amount = v.try_into()?;
268 let scale = money_amount.scale();
269 let token_scale = self.decimals as u32;
270 if scale > token_scale {
271 return Err(MoneyAmountParseError::WrongPrecision {
272 money: scale,
273 token: token_scale,
274 });
275 }
276 let scale_diff = token_scale - scale;
277 let multiplier = U256::from(10).pow(U256::from(scale_diff));
278 let digits = money_amount.mantissa();
279 let value = U256::from(digits).mul(multiplier);
280 Ok(DeployedTokenAmount {
281 amount: value,
282 token: self.clone(),
283 })
284 }
285}
286
287#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
292#[allow(dead_code)] pub struct TokenDeploymentEip712 {
294 pub name: String,
296 pub version: String,
298}
299
300#[derive(Debug)]
320pub struct Eip155ChainProvider {
321 chain: Eip155ChainReference,
322 eip1559: bool,
323 flashblocks: bool,
324 receipt_timeout_secs: u64,
325 inner: InnerProvider,
326 signer_addresses: Arc<Vec<Address>>,
328 signer_cursor: Arc<AtomicUsize>,
330 nonce_manager: PendingNonceManager,
332}
333
334impl Eip155ChainProvider {
335 pub fn rpc_client(chain_id: ChainId, rpc: &[RpcConfig]) -> RpcClient {
336 let transports = rpc
337 .iter()
338 .filter_map(|provider_config| {
339 let scheme = provider_config.http.scheme();
340 let is_http = scheme == "http" || scheme == "https";
341 if !is_http {
342 return None;
343 }
344 let rpc_url = provider_config.http.clone();
345 tracing::info!(chain=%chain_id, rpc_url=%rpc_url, rate_limit=?provider_config.rate_limit, "Using HTTP transport");
346 let rate_limit = provider_config.rate_limit.unwrap_or(u32::MAX);
347 let service = ServiceBuilder::new()
348 .layer(ThrottleLayer::new(rate_limit))
349 .service(Http::new(rpc_url));
350 Some(service)
351 })
352 .collect::<Vec<_>>();
353 let fallback = ServiceBuilder::new()
354 .layer(
355 FallbackLayer::default().with_active_transport_count(
356 NonZeroUsize::new(transports.len())
357 .expect("Non-zero amount of stateless transports"),
358 ),
359 )
360 .service(transports);
361 RpcClient::new(fallback, false)
362 }
363
364 fn next_signer_address(&self) -> Address {
366 debug_assert!(!self.signer_addresses.is_empty());
367 if self.signer_addresses.len() == 1 {
368 self.signer_addresses[0]
369 } else {
370 let next =
371 self.signer_cursor.fetch_add(1, Ordering::Relaxed) % self.signer_addresses.len();
372 self.signer_addresses[next]
373 }
374 }
375}
376
377#[async_trait::async_trait]
388impl FromConfig<Eip155ChainConfig> for Eip155ChainProvider {
389 async fn from_config(config: &Eip155ChainConfig) -> Result<Self, Box<dyn std::error::Error>> {
390 let signers = config
392 .signers()
393 .iter()
394 .map(|s| B256::from_slice(s.inner().as_bytes()))
395 .map(|b| {
396 PrivateKeySigner::from_bytes(&b)
397 .map(|s| s.with_chain_id(Some(config.chain_reference().inner())))
398 })
399 .collect::<Result<Vec<_>, _>>()?;
400 if signers.is_empty() {
401 return Err("at least one signer should be provided".into());
402 }
403 let wallet = {
404 let mut iter = signers.into_iter();
405 let first_signer = iter
406 .next()
407 .expect("iterator contains at least one element by construction");
408 let mut wallet = EthereumWallet::from(first_signer);
409 for signer in iter {
410 wallet.register_signer(signer);
411 }
412 wallet
413 };
414 let signer_addresses =
415 NetworkWallet::<AlloyEthereum>::signer_addresses(&wallet).collect::<Vec<_>>();
416 let signer_addresses = Arc::new(signer_addresses);
417 let signer_cursor = Arc::new(AtomicUsize::new(0));
418
419 let client = Self::rpc_client(config.chain_id(), config.rpc());
421
422 let nonce_manager = PendingNonceManager::default();
425 let filler = JoinFill::new(
428 GasFiller,
429 JoinFill::new(
430 BlobGasFiller::default(),
431 JoinFill::new(
432 NonceFiller::new(nonce_manager.clone()),
433 ChainIdFiller::default(),
434 ),
435 ),
436 );
437 let inner: InnerProvider = ProviderBuilder::default()
438 .filler(filler)
439 .wallet(wallet)
440 .connect_client(client);
441
442 tracing::info!(chain=%config.chain_id(), signers=?signer_addresses, "Using EVM provider");
443
444 Ok(Self {
445 chain: config.chain_reference(),
446 eip1559: config.eip1559(),
447 flashblocks: config.flashblocks(),
448 receipt_timeout_secs: config.receipt_timeout_secs(),
449 inner,
450 signer_addresses,
451 signer_cursor,
452 nonce_manager,
453 })
454 }
455}
456
457impl Eip155MetaTransactionProvider for Eip155ChainProvider {
458 type Error = MetaTransactionSendError;
459 type Inner = InnerProvider;
460
461 fn inner(&self) -> &Self::Inner {
462 &self.inner
463 }
464
465 fn chain(&self) -> &Eip155ChainReference {
466 &self.chain
467 }
468
469 async fn send_transaction(
507 &self,
508 tx: MetaTransaction,
509 ) -> Result<TransactionReceipt, Self::Error> {
510 let from_address = self.next_signer_address();
511 let mut txr = TransactionRequest::default()
512 .with_to(tx.to)
513 .with_from(from_address)
514 .with_input(tx.calldata);
515
516 if !self.eip1559 {
517 let provider = &self.inner;
518 let gas: u128 = provider
519 .get_gas_price()
520 .instrument(tracing::info_span!("get_gas_price"))
521 .await?;
522 txr.set_gas_price(gas);
523 }
524
525 if txr.gas.is_none() {
527 let block_id = if self.flashblocks {
528 BlockId::latest()
529 } else {
530 BlockId::pending()
531 };
532 let gas_limit = self.inner.estimate_gas(txr.clone()).block(block_id).await?;
533 txr.set_gas_limit(gas_limit)
534 }
535
536 let pending_tx = match self.inner.send_transaction(txr).await {
538 Ok(pending) => pending,
539 Err(e) => {
540 self.nonce_manager.reset_nonce(from_address).await;
542 return Err(MetaTransactionSendError::Transport(e));
543 }
544 };
545
546 let timeout = std::time::Duration::from_secs(self.receipt_timeout_secs);
549
550 let watcher = pending_tx
551 .with_required_confirmations(tx.confirmations)
552 .with_timeout(Some(timeout));
553
554 match watcher.get_receipt().await {
555 Ok(receipt) => Ok(receipt),
556 Err(e) => {
557 self.nonce_manager.reset_nonce(from_address).await;
559 Err(MetaTransactionSendError::PendingTransaction(e))
560 }
561 }
562 }
563}
564
565#[derive(Debug, thiserror::Error)]
566pub enum MetaTransactionSendError {
567 #[error(transparent)]
568 Transport(#[from] TransportError),
569 #[error(transparent)]
570 PendingTransaction(#[from] PendingTransactionError),
571 #[allow(dead_code)] #[error("{0}")]
573 Custom(String),
574}
575
576impl ChainProviderOps for Eip155ChainProvider {
577 fn signer_addresses(&self) -> Vec<String> {
578 self.inner
579 .signer_addresses()
580 .map(|a| a.to_string())
581 .collect()
582 }
583
584 fn chain_id(&self) -> ChainId {
585 self.chain.into()
586 }
587}
588
589pub struct MetaTransaction {
591 pub to: Address,
593 pub calldata: Bytes,
595 pub confirmations: u64,
597}
598
599pub trait Eip155MetaTransactionProvider {
601 type Error;
603 type Inner: Provider;
605
606 fn inner(&self) -> &Self::Inner;
608 fn chain(&self) -> &Eip155ChainReference;
610
611 fn send_transaction(
613 &self,
614 tx: MetaTransaction,
615 ) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send;
616}
617
618impl<T: Eip155MetaTransactionProvider> Eip155MetaTransactionProvider for Arc<T> {
619 type Error = T::Error;
620 type Inner = T::Inner;
621
622 fn inner(&self) -> &Self::Inner {
623 (**self).inner()
624 }
625
626 fn chain(&self) -> &Eip155ChainReference {
627 (**self).chain()
628 }
629
630 fn send_transaction(
631 &self,
632 tx: MetaTransaction,
633 ) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send {
634 (**self).send_transaction(tx)
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use crate::chain::solana::{
642 Address as SolanaAddress, SolanaChainReference, SolanaTokenDeployment,
643 };
644 use crate::networks::KnownNetworkSolana;
645 use std::str::FromStr;
646
647 fn create_test_deployment(decimals: u8) -> Eip155TokenDeployment {
648 let chain_ref = Eip155ChainReference::new(1); Eip155TokenDeployment {
650 chain_reference: chain_ref,
651 address: alloy_primitives::Address::ZERO,
652 decimals,
653 eip712: None,
654 }
655 }
656
657 #[test]
658 fn test_parse_whole_number() {
659 let deployment = create_test_deployment(6); let result = deployment.parse("100");
661 assert!(result.is_ok());
662 assert_eq!(result.unwrap().amount, U256::from(100_000_000u64)); }
664
665 #[test]
666 fn test_parse_with_decimals() {
667 let deployment = create_test_deployment(6);
668 let result = deployment.parse("1.50");
669 assert!(result.is_ok());
670 assert_eq!(result.unwrap().amount, U256::from(1_500_000u64)); }
672
673 #[test]
674 fn test_parse_zero_decimals() {
675 let deployment = create_test_deployment(0);
676 let result = deployment.parse("42");
677 assert!(result.is_ok());
678 assert_eq!(result.unwrap().amount, U256::from(42u64));
679 }
680
681 #[test]
682 fn test_parse_precision_too_high() {
683 let deployment = create_test_deployment(2); let result = deployment.parse("1.234"); assert!(result.is_err());
686 let err = result.unwrap_err();
687 assert!(matches!(err, MoneyAmountParseError::WrongPrecision { .. }));
688 }
689
690 #[test]
691 fn test_parse_exact_precision() {
692 let deployment = create_test_deployment(9); let result = deployment.parse("0.123456789");
694 assert!(result.is_ok());
695 assert_eq!(result.unwrap().amount, U256::from(123_456_789u64));
696 }
697
698 #[test]
699 fn test_parse_smallest_amount() {
700 let deployment = create_test_deployment(6);
701 let result = deployment.parse("0.000001");
702 assert!(result.is_ok());
703 assert_eq!(result.unwrap().amount, U256::from(1u64));
704 }
705
706 #[test]
707 fn test_parse_with_currency_symbol() {
708 let deployment = create_test_deployment(6);
709 let result = deployment.parse("$10.50");
710 assert!(result.is_ok());
711 assert_eq!(result.unwrap().amount, U256::from(10_500_000u64));
712 }
713
714 #[test]
715 fn test_parse_with_commas() {
716 let deployment = create_test_deployment(6);
717 let result = deployment.parse("1,000");
718 assert!(result.is_ok());
719 assert_eq!(result.unwrap().amount, U256::from(1_000_000_000u64));
720 }
721
722 #[test]
723 fn test_parse_large_amount() {
724 let deployment = create_test_deployment(6);
725 let result = deployment.parse("999999999");
726 assert!(result.is_ok());
727 assert_eq!(result.unwrap().amount, U256::from(999_999_999_000_000u64));
729 }
730
731 #[test]
732 fn test_parse_very_large_amount_with_high_decimals() {
733 let deployment = create_test_deployment(18); let result = deployment.parse("999999999"); assert!(result.is_ok());
737 let expected = U256::from(999_999_999u64) * U256::from(10).pow(U256::from(18));
739 assert_eq!(result.unwrap().amount, expected);
740 }
741
742 #[test]
743 fn test_parse_matches_solana_behavior() {
744 let eip155_deployment = create_test_deployment(6);
746
747 let solana_chain = SolanaChainReference::solana();
748 let solana_deployment = SolanaTokenDeployment::new(
749 solana_chain,
750 SolanaAddress::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZ5nc4pb").unwrap(),
751 6,
752 );
753
754 let test_cases = ["1", "1.5", "0.01", "100", "999.999"];
756
757 for amount in test_cases {
758 let eip155_result = eip155_deployment.parse(amount);
759 let solana_result = solana_deployment.parse(amount);
760
761 assert_eq!(eip155_result.is_ok(), solana_result.is_ok());
762
763 if let (Ok(eip155), Ok(solana)) = (eip155_result, solana_result) {
764 let eip155_value: u64 = eip155.amount.try_into().unwrap();
766 assert_eq!(eip155_value, solana.amount);
767 }
768 }
769 }
770}