pub mod abi;
use std::sync::Arc;
use ethrex_common::{Address, H256, U256};
use ethrex_l2_sdk::calldata::encode_calldata;
use crate::client::{BlockOverrideSet, CallOverrides, ContractClient, TransactionReceipt};
use crate::common::tokens::ETH_SENTINEL;
use crate::error::{Error, Result};
use crate::overrides::{
OverridesSnapshot, OverridesSource, OverridesWsSource, ToStateOverrideOptions,
to_state_override,
};
use abi::Value;
pub const MAX_FEE_BPS: u16 = 100;
#[derive(Debug, Clone, Copy)]
pub struct FrontendFee {
pub bps: u16,
pub recipient: Address,
}
impl FrontendFee {
fn to_value(self) -> Value {
Value::Tuple(vec![
Value::Uint(U256::from(self.bps)),
Value::Address(self.recipient),
])
}
}
#[derive(Debug, Clone)]
pub struct SwapParams {
pub token_in: Address,
pub token_out: Address,
pub amount_in: U256,
pub amount_out_min: U256,
pub recipient: Address,
pub deadline: U256,
}
impl SwapParams {
fn values(&self) -> Vec<Value> {
vec![
Value::Address(self.token_in),
Value::Address(self.token_out),
Value::Uint(self.amount_in),
Value::Uint(self.amount_out_min),
Value::Address(self.recipient),
Value::Uint(self.deadline),
]
}
}
#[derive(Debug, Clone)]
pub struct Quote {
pub amount_out: U256,
pub venue: Address,
}
#[derive(Debug)]
pub struct SwapResult {
pub hash: H256,
pub receipt: TransactionReceipt,
pub amount_in: U256,
pub amount_out: U256,
pub executed_venue: Address,
pub recipient: Address,
pub fee: Option<FeeCharged>,
}
#[derive(Debug, Clone)]
pub struct FeeCharged {
pub recipient: Address,
pub amount: U256,
}
#[derive(Default)]
pub enum QuoteOverrides {
#[default]
Attached,
Skip,
Source(Arc<dyn OverridesSource>),
Snapshot(OverridesSnapshot),
}
#[derive(Default)]
pub struct QuoteOptions {
pub overrides: QuoteOverrides,
pub skip_bebop_default: bool,
pub venues: Option<Vec<Address>>,
}
#[derive(Default)]
pub struct SwapOptions {
pub venues: Option<Vec<Address>>,
pub frontend_fee: Option<FrontendFee>,
pub gas_limit: Option<u64>,
}
pub struct PropAmmRouter {
address: Address,
client: ContractClient,
overrides: Arc<dyn OverridesSource>,
}
impl PropAmmRouter {
pub fn new(client: ContractClient, address: Address) -> Self {
Self::with_overrides(client, address, Arc::new(OverridesWsSource::default()))
}
pub fn with_overrides(
client: ContractClient,
address: Address,
overrides: Arc<dyn OverridesSource>,
) -> Self {
Self {
address,
client,
overrides,
}
}
pub fn address(&self) -> Address {
self.address
}
pub fn client(&self) -> &ContractClient {
&self.client
}
pub fn overrides(&self) -> &Arc<dyn OverridesSource> {
&self.overrides
}
pub async fn quote(
&self,
token_in: Address,
token_out: Address,
amount_in: U256,
) -> Result<Quote> {
self.quote_with(token_in, token_out, amount_in, &QuoteOptions::default())
.await
}
pub async fn quote_with(
&self,
token_in: Address,
token_out: Address,
amount_in: U256,
opts: &QuoteOptions,
) -> Result<Quote> {
let (mode, mut args) = venue_dispatch(opts.venues.as_deref())?;
args.extend([
Value::Address(token_in),
Value::Address(token_out),
Value::Uint(amount_in),
]);
self.quote_call(mode.quote_selector(), &args, opts).await
}
pub async fn swap(&self, params: &SwapParams) -> Result<H256> {
self.swap_with(params, &SwapOptions::default()).await
}
pub async fn swap_with(&self, params: &SwapParams, opts: &SwapOptions) -> Result<H256> {
let (mode, mut args) = venue_dispatch(opts.venues.as_deref())?;
args.extend(params.values());
if let Some(fee) = &opts.frontend_fee {
validate_fee(fee)?;
args.push(fee.to_value());
}
let signature = mode.swap_selector(opts.frontend_fee.is_some());
let value = (params.token_in == ETH_SENTINEL).then_some(params.amount_in);
self.client
.send(
self.address,
signature,
encode(signature, &args)?,
value,
opts.gas_limit,
)
.await
}
pub async fn swap_and_wait(&self, params: &SwapParams) -> Result<SwapResult> {
self.wait_for_swap(self.swap(params).await?).await
}
pub async fn swap_and_wait_with(
&self,
params: &SwapParams,
opts: &SwapOptions,
) -> Result<SwapResult> {
self.wait_for_swap(self.swap_with(params, opts).await?)
.await
}
pub async fn wait_for_swap(&self, hash: H256) -> Result<SwapResult> {
let receipt = self.client.wait_for_transaction(hash).await?;
if !receipt.receipt.status {
return Err(Error::TransactionReverted {
hash,
receipt: Box::new(receipt),
});
}
let swapped_topic = abi::event_topic(abi::SWAPPED_EVENT);
let fee_topic = abi::event_topic(abi::FRONTEND_FEE_CHARGED_EVENT);
let mut swapped = None;
let mut fee = None;
for log in &receipt.logs {
if log.log.address != self.address {
continue;
}
let Some(topic0) = log.log.topics.first() else {
continue;
};
if *topic0 == swapped_topic && swapped.is_none() {
let values = abi::decode_values("uint256,uint256,address,address", &log.log.data)?;
swapped = Some((
abi::as_u256(&values[0])?,
abi::as_u256(&values[1])?,
abi::as_address(&values[2])?,
abi::as_address(&values[3])?,
));
} else if *topic0 == fee_topic && fee.is_none() {
let recipient = log
.log
.topics
.get(1)
.map(abi::topic_as_address)
.ok_or_else(|| Error::Abi("FrontendFeeCharged missing topic".into()))?;
let values = abi::decode_values("uint256", &log.log.data)?;
fee = Some(FeeCharged {
recipient,
amount: abi::as_u256(&values[0])?,
});
}
}
let (amount_in, amount_out, recipient, market_maker) =
swapped.ok_or(Error::MissingEvent {
hash,
event: "Swapped",
})?;
Ok(SwapResult {
hash,
receipt,
amount_in,
amount_out,
executed_venue: market_maker,
recipient,
fee,
})
}
pub async fn approve(&self, token: Address, amount: U256) -> Result<H256> {
let calldata = encode(
abi::ERC20_APPROVE,
&[Value::Address(self.address), Value::Uint(amount)],
)?;
self.client
.send(token, abi::ERC20_APPROVE, calldata, None, None)
.await
}
pub async fn allowance(&self, token: Address, owner: Address) -> Result<U256> {
let calldata = encode(
abi::ERC20_ALLOWANCE,
&[Value::Address(owner), Value::Address(self.address)],
)?;
let data = self
.client
.call(token, calldata, &CallOverrides::default())
.await?;
abi::as_u256(&abi::decode_values("uint256", &data)?[0])
}
pub async fn fallback_swap_router(&self) -> Result<Address> {
self.view_address(abi::FALLBACK_SWAP_ROUTER).await
}
pub async fn fallback_quoter(&self) -> Result<Address> {
self.view_address(abi::FALLBACK_QUOTER).await
}
pub async fn fallback_fee(&self) -> Result<u32> {
let data = self
.call_router(abi::FALLBACK_FEE, &[], &CallOverrides::default())
.await?;
Ok(abi::as_u256(&abi::decode_values("uint24", &data)?[0])?.low_u32())
}
pub async fn get_pair_fee(&self, token_a: Address, token_b: Address) -> Result<u32> {
let args = [Value::Address(token_a), Value::Address(token_b)];
let data = self
.call_router(abi::GET_PAIR_FEE, &args, &CallOverrides::default())
.await?;
Ok(abi::as_u256(&abi::decode_values("uint24", &data)?[0])?.low_u32())
}
pub async fn resolved_fee(&self, token_in: Address, token_out: Address) -> Result<u32> {
let args = [Value::Address(token_in), Value::Address(token_out)];
let data = self
.call_router(abi::RESOLVED_FEE, &args, &CallOverrides::default())
.await?;
Ok(abi::as_u256(&abi::decode_values("uint24", &data)?[0])?.low_u32())
}
pub async fn is_whitelisted_venue(&self, venue: Address) -> Result<bool> {
let data = self
.call_router(
abi::IS_WHITELISTED_VENUE,
&[Value::Address(venue)],
&CallOverrides::default(),
)
.await?;
abi::as_bool(&abi::decode_values("bool", &data)?[0])
}
pub async fn get_whitelisted_venues(&self) -> Result<Vec<Address>> {
let data = self
.call_router(abi::GET_WHITELISTED_VENUES, &[], &CallOverrides::default())
.await?;
abi::as_address_array(&abi::decode_values("address[]", &data)?[0])
}
pub async fn paused(&self) -> Result<bool> {
let data = self
.call_router(abi::PAUSED, &[], &CallOverrides::default())
.await?;
abi::as_bool(&abi::decode_values("bool", &data)?[0])
}
async fn quote_call(
&self,
signature: &str,
args: &[Value],
opts: &QuoteOptions,
) -> Result<Quote> {
let overrides = self.resolve_overrides(opts).await?;
let data = self.call_router(signature, args, &overrides).await?;
let values = abi::decode_values("uint256,address", &data)?;
Ok(Quote {
amount_out: abi::as_u256(&values[0])?,
venue: abi::as_address(&values[1])?,
})
}
async fn call_router(
&self,
signature: &str,
args: &[Value],
overrides: &CallOverrides,
) -> Result<Vec<u8>> {
self.client
.call(self.address, encode(signature, args)?, overrides)
.await
.map_err(decode_revert)
}
async fn view_address(&self, signature: &str) -> Result<Address> {
let data = self
.call_router(signature, &[], &CallOverrides::default())
.await?;
abi::as_address(&abi::decode_values("address", &data)?[0])
}
async fn resolve_overrides(&self, opts: &QuoteOptions) -> Result<CallOverrides> {
let owned;
let snapshot: &OverridesSnapshot = match &opts.overrides {
QuoteOverrides::Skip => return Ok(CallOverrides::default()),
QuoteOverrides::Attached => {
owned = self.overrides.get_overrides().await?;
&owned
}
QuoteOverrides::Source(source) => {
owned = source.get_overrides().await?;
&owned
}
QuoteOverrides::Snapshot(snapshot) => snapshot,
};
let state = to_state_override(
snapshot,
&ToStateOverrideOptions {
pamms: None,
skip_bebop_default: opts.skip_bebop_default,
},
);
if state.is_empty() {
return Ok(CallOverrides::default());
}
Ok(CallOverrides {
state: Some(state),
block: Some(BlockOverrideSet {
number: snapshot.block_number,
time: snapshot.block_time_secs(),
..Default::default()
}),
})
}
}
#[derive(Clone, Copy)]
enum VenueMode {
All,
Single,
Selected,
}
impl VenueMode {
fn quote_selector(self) -> &'static str {
match self {
Self::All => abi::QUOTE,
Self::Single => abi::QUOTE_VENUE,
Self::Selected => abi::QUOTE_SELECTED_VENUES,
}
}
fn swap_selector(self, with_fee: bool) -> &'static str {
match (self, with_fee) {
(Self::All, false) => abi::SWAP,
(Self::All, true) => abi::SWAP_WITH_FEE,
(Self::Single, false) => abi::SWAP_VIA_VENUE,
(Self::Single, true) => abi::SWAP_VIA_VENUE_WITH_FEE,
(Self::Selected, false) => abi::SWAP_VIA_SELECTED_VENUES,
(Self::Selected, true) => abi::SWAP_VIA_SELECTED_VENUES_WITH_FEE,
}
}
}
fn venue_dispatch(venues: Option<&[Address]>) -> Result<(VenueMode, Vec<Value>)> {
match venues {
None => Ok((VenueMode::All, vec![])),
Some([]) => Err(Error::InvalidInput(
"venues must not be empty — omit it to use every whitelisted venue".into(),
)),
Some([venue]) => Ok((VenueMode::Single, vec![Value::Address(*venue)])),
Some(venues) => Ok((VenueMode::Selected, vec![address_array(venues.to_vec())])),
}
}
fn validate_fee(fee: &FrontendFee) -> Result<()> {
if !(1..=MAX_FEE_BPS).contains(&fee.bps) {
return Err(Error::InvalidInput(format!(
"fee bps must be in [1, {MAX_FEE_BPS}], got {}",
fee.bps
)));
}
if fee.recipient == Address::zero() {
return Err(Error::InvalidInput(
"fee recipient must not be the zero address".into(),
));
}
Ok(())
}
fn encode(signature: &str, args: &[Value]) -> Result<Vec<u8>> {
encode_calldata(signature, args)
.map_err(|e| Error::Abi(format!("failed to encode {signature}: {e}")))
}
fn address_array(addresses: Vec<Address>) -> Value {
Value::Array(addresses.into_iter().map(Value::Address).collect())
}
fn decode_revert(error: Error) -> Error {
let Error::Revert {
message,
data: Some(data),
} = &error
else {
return error;
};
match abi::decode_error(data) {
Some(decoded) => Error::Revert {
message: format!("{message} ({decoded})"),
data: Some(data.clone()),
},
None => error,
}
}