pub mod pending_nonce_manager;
pub mod types;
use alloy_network::{Ethereum as AlloyEthereum, EthereumWallet, NetworkWallet, TransactionBuilder};
use alloy_primitives::{Address, B256, Bytes, U256};
use alloy_provider::fillers::{
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller,
};
use alloy_provider::{
Identity, PendingTransactionError, Provider, ProviderBuilder, RootProvider, WalletProvider,
};
use alloy_rpc_client::RpcClient;
use alloy_rpc_types_eth::{BlockId, TransactionReceipt, TransactionRequest};
use alloy_signer::Signer;
use alloy_signer_local::PrivateKeySigner;
use alloy_transport::TransportError;
use alloy_transport::layers::{FallbackLayer, ThrottleLayer};
use alloy_transport_http::Http;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::num::NonZeroUsize;
use std::ops::Mul;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tower::ServiceBuilder;
use tracing::Instrument;
use crate::chain::{ChainId, ChainProviderOps, DeployedTokenAmount};
use crate::config::Eip155ChainConfig;
use crate::util::money_amount::{MoneyAmount, MoneyAmountParseError};
pub use pending_nonce_manager::*;
pub use types::*;
pub type InnerFiller = JoinFill<
GasFiller,
JoinFill<BlobGasFiller, JoinFill<NonceFiller<PendingNonceManager>, ChainIdFiller>>,
>;
pub type InnerProvider = FillProvider<
JoinFill<JoinFill<Identity, InnerFiller>, WalletFiller<EthereumWallet>>,
RootProvider,
>;
pub const EIP155_NAMESPACE: &str = "eip155";
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Eip155ChainReference(u64);
impl Eip155ChainReference {
pub fn as_chain_id(&self) -> ChainId {
ChainId::new(EIP155_NAMESPACE, self.0.to_string())
}
}
impl From<Eip155ChainReference> for ChainId {
fn from(value: Eip155ChainReference) -> Self {
ChainId::new(EIP155_NAMESPACE, value.0.to_string())
}
}
impl From<&Eip155ChainReference> for ChainId {
fn from(value: &Eip155ChainReference) -> Self {
ChainId::new(EIP155_NAMESPACE, value.0.to_string())
}
}
impl TryFrom<ChainId> for Eip155ChainReference {
type Error = Eip155ChainReferenceFormatError;
fn try_from(value: ChainId) -> Result<Self, Self::Error> {
if value.namespace != EIP155_NAMESPACE {
return Err(Eip155ChainReferenceFormatError::InvalidNamespace(
value.namespace,
));
}
let chain_id: u64 = value.reference.parse().map_err(|_| {
Eip155ChainReferenceFormatError::InvalidReference(value.reference.clone())
})?;
Ok(Eip155ChainReference(chain_id))
}
}
impl TryFrom<&ChainId> for Eip155ChainReference {
type Error = Eip155ChainReferenceFormatError;
fn try_from(value: &ChainId) -> Result<Self, Self::Error> {
if value.namespace != EIP155_NAMESPACE {
return Err(Eip155ChainReferenceFormatError::InvalidNamespace(
value.namespace.clone(),
));
}
let chain_id: u64 = value.reference.parse().map_err(|_| {
Eip155ChainReferenceFormatError::InvalidReference(value.reference.clone())
})?;
Ok(Eip155ChainReference(chain_id))
}
}
#[derive(Debug, thiserror::Error)]
pub enum Eip155ChainReferenceFormatError {
#[error("Invalid namespace {0}, expected eip155")]
InvalidNamespace(String),
#[error("Invalid eip155 chain reference {0}")]
InvalidReference(String),
}
impl Eip155ChainReference {
pub fn new(chain_id: u64) -> Self {
Self(chain_id)
}
pub fn inner(&self) -> u64 {
self.0
}
}
impl Display for Eip155ChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(dead_code)] pub struct Eip155TokenDeployment {
pub chain_reference: Eip155ChainReference,
pub address: Address,
pub decimals: u8,
pub eip712: Option<TokenDeploymentEip712>,
}
#[allow(dead_code)] impl Eip155TokenDeployment {
pub fn amount<V: Into<TokenAmount>>(
&self,
v: V,
) -> DeployedTokenAmount<U256, Eip155TokenDeployment> {
DeployedTokenAmount {
amount: v.into().0,
token: self.clone(),
}
}
pub fn parse<V>(
&self,
v: V,
) -> Result<DeployedTokenAmount<U256, Eip155TokenDeployment>, MoneyAmountParseError>
where
V: TryInto<MoneyAmount>,
MoneyAmountParseError: From<<V as TryInto<MoneyAmount>>::Error>,
{
let money_amount = v.try_into()?;
let scale = money_amount.scale();
let token_scale = self.decimals as u32;
if scale > token_scale {
return Err(MoneyAmountParseError::WrongPrecision {
money: scale,
token: token_scale,
});
}
let scale_diff = token_scale - scale;
let multiplier = U256::from(10).pow(U256::from(scale_diff));
let digits = money_amount.mantissa();
let value = U256::from(digits).mul(multiplier);
Ok(DeployedTokenAmount {
amount: value,
token: self.clone(),
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[allow(dead_code)] pub struct TokenDeploymentEip712 {
pub name: String,
pub version: String,
}
#[derive(Debug)]
pub struct Eip155ChainProvider {
chain: Eip155ChainReference,
eip1559: bool,
flashblocks: bool,
receipt_timeout_secs: u64,
inner: InnerProvider,
signer_addresses: Arc<Vec<Address>>,
signer_cursor: Arc<AtomicUsize>,
nonce_manager: PendingNonceManager,
}
impl Eip155ChainProvider {
pub async fn from_config(
config: &Eip155ChainConfig,
) -> Result<Self, Box<dyn std::error::Error>> {
let signers = config
.signers()
.iter()
.map(|s| B256::from_slice(s.inner().as_bytes()))
.map(|b| {
PrivateKeySigner::from_bytes(&b)
.map(|s| s.with_chain_id(Some(config.chain_reference().inner())))
})
.collect::<Result<Vec<_>, _>>()?;
if signers.is_empty() {
return Err("at least one signer should be provided".into());
}
let wallet = {
let mut iter = signers.into_iter();
let first_signer = iter
.next()
.expect("iterator contains at least one element by construction");
let mut wallet = EthereumWallet::from(first_signer);
for signer in iter {
wallet.register_signer(signer);
}
wallet
};
let signer_addresses =
NetworkWallet::<AlloyEthereum>::signer_addresses(&wallet).collect::<Vec<_>>();
let signer_addresses = Arc::new(signer_addresses);
let signer_cursor = Arc::new(AtomicUsize::new(0));
let transports = config
.rpc()
.iter()
.filter_map(|provider_config| {
let scheme = provider_config.http.scheme();
let is_http = scheme == "http" || scheme == "https";
if !is_http {
return None;
}
let rpc_url = provider_config.http.clone();
tracing::info!(chain=%config.chain_id(), rpc_url=%rpc_url, rate_limit=?provider_config.rate_limit, "Using HTTP transport");
let rate_limit = provider_config.rate_limit.unwrap_or(u32::MAX);
let service = ServiceBuilder::new()
.layer(ThrottleLayer::new(rate_limit))
.service(Http::new(rpc_url));
Some(service)
})
.collect::<Vec<_>>();
let fallback = ServiceBuilder::new()
.layer(
FallbackLayer::default().with_active_transport_count(
NonZeroUsize::new(transports.len())
.expect("Non-zero amount of stateless transports"),
),
)
.service(transports);
let client = RpcClient::new(fallback, false);
let nonce_manager = PendingNonceManager::default();
let filler = JoinFill::new(
GasFiller,
JoinFill::new(
BlobGasFiller::default(),
JoinFill::new(
NonceFiller::new(nonce_manager.clone()),
ChainIdFiller::default(),
),
),
);
let inner: InnerProvider = ProviderBuilder::default()
.filler(filler)
.wallet(wallet)
.connect_client(client);
tracing::info!(chain=%config.chain_id(), signers=?signer_addresses, "Initialized EVM provider");
Ok(Self {
chain: config.chain_reference(),
eip1559: config.eip1559(),
flashblocks: config.flashblocks(),
receipt_timeout_secs: config.receipt_timeout_secs(),
inner,
signer_addresses,
signer_cursor,
nonce_manager,
})
}
fn next_signer_address(&self) -> Address {
debug_assert!(!self.signer_addresses.is_empty());
if self.signer_addresses.len() == 1 {
self.signer_addresses[0]
} else {
let next =
self.signer_cursor.fetch_add(1, Ordering::Relaxed) % self.signer_addresses.len();
self.signer_addresses[next]
}
}
}
impl Eip155MetaTransactionProvider for &Eip155ChainProvider {
type Error = MetaTransactionSendError;
type Inner = InnerProvider;
fn inner(&self) -> &Self::Inner {
(*self).inner()
}
fn chain(&self) -> &Eip155ChainReference {
(*self).chain()
}
fn send_transaction(
&self,
tx: MetaTransaction,
) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send {
(*self).send_transaction(tx)
}
}
impl Eip155MetaTransactionProvider for Eip155ChainProvider {
type Error = MetaTransactionSendError;
type Inner = InnerProvider;
fn inner(&self) -> &Self::Inner {
&self.inner
}
fn chain(&self) -> &Eip155ChainReference {
&self.chain
}
async fn send_transaction(
&self,
tx: MetaTransaction,
) -> Result<TransactionReceipt, Self::Error> {
let from_address = self.next_signer_address();
let mut txr = TransactionRequest::default()
.with_to(tx.to)
.with_from(from_address)
.with_input(tx.calldata);
if !self.eip1559 {
let provider = &self.inner;
let gas: u128 = provider
.get_gas_price()
.instrument(tracing::info_span!("get_gas_price"))
.await?;
txr.set_gas_price(gas);
}
if txr.gas.is_none() {
let block_id = if self.flashblocks {
BlockId::latest()
} else {
BlockId::pending()
};
let gas_limit = self.inner.estimate_gas(txr.clone()).block(block_id).await?;
txr.set_gas_limit(gas_limit)
}
let pending_tx = match self.inner.send_transaction(txr).await {
Ok(pending) => pending,
Err(e) => {
self.nonce_manager.reset_nonce(from_address).await;
return Err(MetaTransactionSendError::Transport(e));
}
};
let timeout = std::time::Duration::from_secs(self.receipt_timeout_secs);
let watcher = pending_tx
.with_required_confirmations(tx.confirmations)
.with_timeout(Some(timeout));
match watcher.get_receipt().await {
Ok(receipt) => Ok(receipt),
Err(e) => {
self.nonce_manager.reset_nonce(from_address).await;
Err(MetaTransactionSendError::PendingTransaction(e))
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MetaTransactionSendError {
#[error(transparent)]
Transport(#[from] TransportError),
#[error(transparent)]
PendingTransaction(#[from] PendingTransactionError),
}
impl ChainProviderOps for Eip155ChainProvider {
fn signer_addresses(&self) -> Vec<String> {
self.inner
.signer_addresses()
.map(|a| a.to_string())
.collect()
}
fn chain_id(&self) -> ChainId {
self.chain.into()
}
}
pub struct MetaTransaction {
pub to: Address,
pub calldata: Bytes,
pub confirmations: u64,
}
pub trait Eip155MetaTransactionProvider {
type Error;
type Inner: Provider;
fn inner(&self) -> &Self::Inner;
fn chain(&self) -> &Eip155ChainReference;
fn send_transaction(
&self,
tx: MetaTransaction,
) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chain::solana::{
Address as SolanaAddress, SolanaChainReference, SolanaTokenDeployment,
};
use crate::networks::KnownNetworkSolana;
use std::str::FromStr;
fn create_test_deployment(decimals: u8) -> Eip155TokenDeployment {
let chain_ref = Eip155ChainReference::new(1); Eip155TokenDeployment {
chain_reference: chain_ref,
address: alloy_primitives::Address::ZERO,
decimals,
eip712: None,
}
}
#[test]
fn test_parse_whole_number() {
let deployment = create_test_deployment(6); let result = deployment.parse("100");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(100_000_000u64)); }
#[test]
fn test_parse_with_decimals() {
let deployment = create_test_deployment(6);
let result = deployment.parse("1.50");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(1_500_000u64)); }
#[test]
fn test_parse_zero_decimals() {
let deployment = create_test_deployment(0);
let result = deployment.parse("42");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(42u64));
}
#[test]
fn test_parse_precision_too_high() {
let deployment = create_test_deployment(2); let result = deployment.parse("1.234"); assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, MoneyAmountParseError::WrongPrecision { .. }));
}
#[test]
fn test_parse_exact_precision() {
let deployment = create_test_deployment(9); let result = deployment.parse("0.123456789");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(123_456_789u64));
}
#[test]
fn test_parse_smallest_amount() {
let deployment = create_test_deployment(6);
let result = deployment.parse("0.000001");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(1u64));
}
#[test]
fn test_parse_with_currency_symbol() {
let deployment = create_test_deployment(6);
let result = deployment.parse("$10.50");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(10_500_000u64));
}
#[test]
fn test_parse_with_commas() {
let deployment = create_test_deployment(6);
let result = deployment.parse("1,000");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(1_000_000_000u64));
}
#[test]
fn test_parse_large_amount() {
let deployment = create_test_deployment(6);
let result = deployment.parse("999999999");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, U256::from(999_999_999_000_000u64));
}
#[test]
fn test_parse_very_large_amount_with_high_decimals() {
let deployment = create_test_deployment(18); let result = deployment.parse("999999999"); assert!(result.is_ok());
let expected = U256::from(999_999_999u64) * U256::from(10).pow(U256::from(18));
assert_eq!(result.unwrap().amount, expected);
}
#[test]
fn test_parse_matches_solana_behavior() {
let eip155_deployment = create_test_deployment(6);
let solana_chain = SolanaChainReference::solana();
let solana_deployment = SolanaTokenDeployment::new(
solana_chain,
SolanaAddress::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZ5nc4pb").unwrap(),
6,
);
let test_cases = ["1", "1.5", "0.01", "100", "999.999"];
for amount in test_cases {
let eip155_result = eip155_deployment.parse(amount);
let solana_result = solana_deployment.parse(amount);
assert_eq!(eip155_result.is_ok(), solana_result.is_ok());
if let (Ok(eip155), Ok(solana)) = (eip155_result, solana_result) {
let eip155_value: u64 = eip155.amount.try_into().unwrap();
assert_eq!(eip155_value, solana.amount);
}
}
}
}