use alloy_primitives::{address, b256, Address, B256, U256};
use alloy_provider::{network::Ethereum, Provider};
use alloy_sol_types::SolCall;
use anyhow::{anyhow, Result};
use wp_evm_base::{chain::Chain, types::SlippageBps};
use wp_evm_ramses_interfaces::periphery::router::IRamsesPeripheryRouter;
use wp_evm_ramses_provider as ramses;
pub use wp_evm_ramses_provider::data::RamsesProtocolConfig;
use wp_evm_v3_provider::plan::PeripherySelectors;
const SELECTORS: PeripherySelectors = PeripherySelectors {
multicall: IRamsesPeripheryRouter::multicallCall::SELECTOR,
unwrap_native: IRamsesPeripheryRouter::unwrapWETH9Call::SELECTOR,
sweep_token: IRamsesPeripheryRouter::sweepTokenCall::SELECTOR,
refund_native: IRamsesPeripheryRouter::refundETHCall::SELECTOR,
};
pub use wp_evm_ramses_provider::data::{
CollectFeesParams, ExactInParams, ExactOutParams, GaugeClaim, GaugeEarnedGrid, PlanFragment,
PoolState, PositionState, Quote, RamsesAddLiquidityParams, RemoveAndCollectParams,
RemoveLiquidityParams,
};
pub use wp_evm_ramses_provider::gauge::gauge_earned_grids;
pub use wp_evm_ramses_provider::position::{position_key, RamsesPositionView};
pub use wp_evm_ramses_provider::position_views::{PositionFees, PositionViewEntry};
pub use wp_evm_ramses_provider::quote::QuoteError;
pub use wp_evm_ramses_provider::Enumeration;
pub use wp_evm_ramses_provider::plan;
pub use wp_evm_ramses_provider::pool_address as pool_address_raw;
pub use wp_evm_v3_provider::pool_views::{PoolReadEntry, PoolViewData};
pub async fn pool_views<P: Provider<Ethereum>>(
provider: &P,
pools: &[Address],
) -> Result<Vec<PoolReadEntry>> {
wp_evm_v3_provider::pool_views::pool_views(
provider,
ramses::MULTICALL3_ADDRESS,
pools,
&ramses::pool_views::RamsesPoolViewSource,
)
.await
}
pub async fn position_token_pair<P: Provider<Ethereum>>(
provider: &P,
nfpm: Address,
token_id: U256,
) -> Result<(Address, Address)> {
ramses::hydrate::position_token_pair(provider, nfpm, token_id).await
}
pub const CONFIG: RamsesProtocolConfig = RamsesProtocolConfig {
factory: address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"),
pool_deployer: address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"),
router: address!("5543c6176feb9b4b179078205d7c29eea2e2d695"),
position_mgr: address!("12E66C8F215DdD5d48d150c8f46aD0c6fB0F4406"),
init_code_hash: b256!("c701ee63862761c31d620a4a083c61bdc1e81761e6b9c9267fd19afd22e0821d"),
tick_spacings: &[1, 5, 10, 50, 100, 200],
multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
quoter: None,
voter: address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D"),
};
pub fn config_for_chain(chain: Chain) -> Option<&'static RamsesProtocolConfig> {
match chain {
Chain::Sonic => Some(&CONFIG),
Chain::Ethereum
| Chain::Arbitrum
| Chain::Optimism
| Chain::Polygon
| Chain::Base
| Chain::Bsc
| Chain::HyperEvm
| Chain::Avalanche
| Chain::Celo => None,
}
}
pub fn factory(chain: Chain) -> Option<Address> {
config_for_chain(chain).map(|c| c.factory)
}
pub fn pool_deployer(chain: Chain) -> Option<Address> {
config_for_chain(chain).map(|c| c.pool_deployer)
}
pub fn position_manager(chain: Chain) -> Option<Address> {
config_for_chain(chain).map(|c| c.position_mgr)
}
pub fn router(chain: Chain) -> Option<Address> {
config_for_chain(chain).map(|c| c.router)
}
pub fn quoter(chain: Chain) -> Option<Address> {
config_for_chain(chain).and_then(|c| c.quoter)
}
pub fn multicall(chain: Chain) -> Option<Address> {
config_for_chain(chain).map(|c| c.multicall)
}
pub fn init_code_hash(chain: Chain) -> Option<B256> {
config_for_chain(chain).map(|c| c.init_code_hash)
}
pub fn voter(chain: Chain) -> Option<Address> {
config_for_chain(chain).map(|c| c.voter)
}
pub fn supports(chain: Chain) -> bool {
config_for_chain(chain).is_some()
}
pub async fn pool_state<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
pool: Address,
) -> Result<PoolState> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
ramses::hydrate::pool_state(provider, cfg.multicall, pool).await
}
pub async fn position_state<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
token_id: U256,
) -> Result<PositionState> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
ramses::hydrate::position_state_shadow(provider, cfg.multicall, cfg.position_mgr, token_id)
.await
}
pub async fn position_views<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
token_ids: &[U256],
) -> Result<Vec<PositionViewEntry<RamsesPositionView>>> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
ramses::position_views::position_views(provider, cfg.multicall, cfg.position_mgr, token_ids)
.await
}
pub async fn position_views_with_nfpm<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
nfpm: Address,
token_ids: &[U256],
) -> Result<Vec<PositionViewEntry<RamsesPositionView>>> {
let multicall =
config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
ramses::position_views::position_views(provider, multicall, nfpm, token_ids).await
}
pub async fn enumerate_owner_token_ids<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
nfpm: Address,
owner: Address,
) -> Result<Enumeration> {
let multicall =
config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
ramses::enumerate_owner_token_ids(provider, multicall, nfpm, owner, chain).await
}
pub async fn populate_positions_fees<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
entries: &mut [PositionViewEntry<RamsesPositionView>],
) -> Result<()> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
ramses::position_views::populate_position_fees(provider, cfg.multicall, entries, |v| {
pool_address(chain, v.token0, v.token1, v.tick_spacing, None)
})
.await
}
pub fn quote_exact_in(s: &PoolState, p: &ExactInParams) -> Result<Quote, QuoteError> {
ramses::quote::exact_in(s, p)
}
pub fn quote_exact_out(s: &PoolState, p: &ExactOutParams) -> Result<Quote, QuoteError> {
ramses::quote::exact_out(s, p)
}
pub async fn populate_ticks<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
pool: Address,
state: &mut PoolState,
) -> Result<()> {
config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
ramses::populate_ticks::populate_ticks(provider, pool, state).await
}
pub async fn quote_online_exact_in<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
state: &PoolState,
params: &ExactInParams,
) -> Result<Quote> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
let quoter = cfg.quoter.ok_or_else(|| anyhow!("Shadow quoter not registered on {chain:?}"))?;
ramses::quote_online::quote_online_exact_in(provider, quoter, state, params).await
}
pub fn plan_swap_exact_in(
s: &PoolState,
q: &Quote,
p: &ExactInParams,
slippage: SlippageBps,
deadline: u64,
chain: Chain,
) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
Ok(ramses::plan::swap_exact_in(s, q, p, slippage, deadline, cfg.router))
}
pub fn plan_add_liquidity(
p: &RamsesAddLiquidityParams,
slippage: SlippageBps,
deadline: u64,
chain: Chain,
) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
Ok(ramses::plan::add_liquidity(p, slippage, deadline, cfg.position_mgr))
}
#[allow(clippy::too_many_arguments)]
pub fn plan_increase_liquidity(
token_id: U256,
token0: Address,
token1: Address,
amount0_desired: U256,
amount1_desired: U256,
slippage: SlippageBps,
deadline: u64,
chain: Chain,
) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
Ok(ramses::plan::increase_liquidity(
token_id,
token0,
token1,
amount0_desired,
amount1_desired,
slippage,
deadline,
cfg.position_mgr,
))
}
pub fn plan_remove_liquidity(
p: &RemoveLiquidityParams,
deadline: u64,
chain: Chain,
) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
Ok(ramses::plan::remove_liquidity(p, deadline, cfg.position_mgr))
}
pub fn plan_remove_liquidity_and_collect(
p: &RemoveAndCollectParams,
deadline: u64,
chain: Chain,
) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_remove_and_collect(
p,
cfg.position_mgr,
chain,
)?;
let mut core_params = (*p).clone();
core_params.recipient = wrap.effective_collect_recipient;
let frag = ramses::plan::remove_liquidity_and_collect(&core_params, deadline, cfg.position_mgr);
wp_evm_v3_provider::plan::compose_native_remove_collect_multicall(frag, &wrap, SELECTORS)
}
pub fn plan_collect_fees(p: &CollectFeesParams, chain: Chain) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_collect(p, cfg.position_mgr, chain)?;
let core_params = CollectFeesParams {
token_id: p.token_id,
recipient: wrap.effective_recipient,
token0: p.token0,
token1: p.token1,
caller: p.caller,
};
let frag = ramses::plan::collect_fees(&core_params, cfg.position_mgr);
Ok(wp_evm_v3_provider::plan::compose_native_collect_multicall(frag, &wrap, SELECTORS))
}
pub fn pool_address(
chain: Chain,
token_a: Address,
token_b: Address,
tick_spacing: i32,
init_code_hash_override: Option<B256>,
) -> Option<Address> {
let cfg = config_for_chain(chain)?;
let init_code_hash = init_code_hash_override.unwrap_or(cfg.init_code_hash);
Some(ramses::pool_address::compute(
cfg.pool_deployer,
init_code_hash,
token_a,
token_b,
tick_spacing,
))
}
pub async fn pending_emissions<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
pool: Address,
token_id: U256,
) -> Result<Option<wp_evm_ramses_provider::gauge::PendingEmissions>> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
wp_evm_ramses_provider::gauge::pending_emissions(
provider,
cfg.multicall,
cfg.voter,
pool,
token_id,
)
.await
}
pub fn plan_claim_cl_gauge_rewards(chain: Chain, claims: &[GaugeClaim]) -> Result<PlanFragment> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
Ok(ramses::plan::claim_cl_gauge_rewards(cfg.voter, claims))
}
pub async fn claim_cl_gauge_rewards_online<P: Provider<Ethereum>>(
provider: &P,
chain: Chain,
positions: &[(Address, U256)],
) -> Result<Option<PlanFragment>> {
let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
let grids =
ramses::gauge::gauge_earned_grids(provider, cfg.multicall, cfg.voter, positions).await?;
Ok(ramses::plan::claim_cl_gauge_rewards_from_grids(cfg.voter, &grids))
}
#[cfg(test)]
mod tests {
use super::*;
use wp_evm_ramses_provider::data::TickInfo;
#[test]
fn gauge_earned_grids_is_reexported_at_facade() {
let _f = crate::gauge_earned_grids::<alloy_provider::RootProvider>;
}
#[test]
fn config_router_is_known_shadow_address() {
assert_eq!(CONFIG.router, address!("5543c6176feb9b4b179078205d7c29eea2e2d695"));
}
#[test]
fn config_factory_is_known_shadow_address() {
assert_eq!(CONFIG.factory, address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"));
}
#[test]
fn config_pool_deployer_matches_factory_ramsesv3pooldeployer_getter() {
assert_eq!(CONFIG.pool_deployer, address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"));
}
#[test]
fn pool_address_matches_canonical_usdce_ws_ts50_sonic() {
let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
let pool = pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
assert_eq!(pool, address!("324963c267C354c7660Ce8CA3F5f167E05649970"));
}
#[test]
fn config_tick_spacings_match_shadow() {
assert_eq!(CONFIG.tick_spacings, &[1, 5, 10, 50, 100, 200]);
}
#[test]
fn plan_add_liquidity_accepts_ramses_params_with_tick_spacing() {
let p = RamsesAddLiquidityParams {
token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
tick_spacing: 50,
tick_lower: -887_272,
tick_upper: 887_272,
amount0_desired: U256::from(1_000_000u64),
amount1_desired: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let frag = plan_add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, Chain::Sonic)
.expect("Sonic supported");
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
assert_eq!(frag.approvals.len(), 2);
assert_eq!(frag.approvals[0].token, p.token0);
assert_eq!(frag.approvals[1].token, p.token1);
assert_eq!(frag.value, U256::ZERO);
}
fn fixture_usdc_weth() -> PoolState {
let sqrt_price_x96 = U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
PoolState {
token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
fee: 3000,
tick_spacing: 50,
sqrt_price_x96,
liquidity: 2_000_000_000_000_000_000_000u128,
tick: 76012,
ticks: vec![
TickInfo {
tick: 74950,
liquidity_net: 1_000_000_000_000_000_000_000i128,
liquidity_gross: 1_000_000_000_000_000_000_000u128,
},
TickInfo {
tick: 75950,
liquidity_net: 1_000_000_000_000_000_000_000i128,
liquidity_gross: 1_000_000_000_000_000_000_000u128,
},
TickInfo {
tick: 76050,
liquidity_net: -2_000_000_000_000_000_000_000i128,
liquidity_gross: 2_000_000_000_000_000_000_000u128,
},
],
}
}
#[test]
fn quote_exact_in_delegates_to_ramses_family() {
let state = fixture_usdc_weth();
let params = ExactInParams {
token_in: state.token0,
token_out: state.token1,
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let quote = quote_exact_in(&state, ¶ms).expect("quote should succeed");
assert!(quote.amount_out > U256::ZERO);
assert_eq!(quote.amount_in, params.amount_in);
}
#[test]
fn plan_swap_exact_in_targets_shadow_router() {
let state = fixture_usdc_weth();
let quote = Quote {
amount_in: U256::from(1_000_000u64),
amount_out: U256::from(500_000_000_000_000u64),
sqrt_price_x96_after: state.sqrt_price_x96,
price_impact_bps: 0,
};
let params = ExactInParams {
token_in: state.token0,
token_out: state.token1,
amount_in: quote.amount_in,
recipient: address!("0000000000000000000000000000000000000099"),
};
let frag = plan_swap_exact_in(
&state,
"e,
¶ms,
SlippageBps::new(50),
u64::MAX,
Chain::Sonic,
)
.expect("Sonic supported");
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, CONFIG.router);
assert_eq!(frag.approvals.len(), 1);
}
#[test]
fn quote_exact_out_delegates_to_ramses_family() {
let state = fixture_usdc_weth();
let params = ExactOutParams {
token_in: state.token0,
token_out: state.token1,
amount_out: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let quote = quote_exact_out(&state, ¶ms).expect("exact-out quote should succeed");
assert!(quote.amount_in > U256::ZERO);
assert_eq!(quote.amount_out, params.amount_out);
}
#[test]
fn plan_remove_liquidity_targets_position_manager_no_approvals() {
let params = RemoveLiquidityParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: None,
amount1_min: None,
};
let frag = plan_remove_liquidity(¶ms, u64::MAX, Chain::Sonic).expect("Sonic supported");
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn plan_collect_fees_targets_position_manager_no_approvals() {
let params = CollectFeesParams {
token_id: U256::from(42u64),
recipient: address!("0000000000000000000000000000000000000099"),
token0: address!("0000000000000000000000000000000000000001"),
token1: address!("0000000000000000000000000000000000000002"),
caller: Address::ZERO,
};
let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn plan_claim_cl_gauge_rewards_targets_voter() {
let claims = vec![GaugeClaim {
gauge: address!("1111111111111111111111111111111111111111"),
reward_tokens: vec![address!("aaaa000000000000000000000000000000000000")],
token_ids: vec![U256::from(1u64)],
}];
let frag = plan_claim_cl_gauge_rewards(Chain::Sonic, &claims).expect("Sonic supported");
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, CONFIG.voter);
assert!(frag.approvals.is_empty());
assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
}
#[test]
fn plan_claim_cl_gauge_rewards_rejects_unsupported_chain() {
let claims = vec![GaugeClaim {
gauge: Address::ZERO,
reward_tokens: vec![Address::ZERO],
token_ids: vec![U256::from(1u64)],
}];
assert!(plan_claim_cl_gauge_rewards(Chain::Base, &claims).is_err());
}
#[test]
fn plan_collect_fees_native_recipient_emits_multicall_with_unwrap_and_sweep() {
let params = CollectFeesParams {
token_id: U256::from(1u64),
recipient: Address::ZERO,
token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
};
let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
assert_eq!(frag.value, U256::ZERO);
assert_eq!(frag.calls[0].value, U256::ZERO);
assert!(
frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
"native collect multicall must include unwrapWETH9(uint256,address) tail"
);
assert!(
frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
"native collect multicall must include sweepToken(address,uint256,address) tail"
);
}
#[test]
fn plan_collect_fees_non_native_recipient_passthrough() {
let params = CollectFeesParams {
token_id: U256::from(1u64),
recipient: address!("0000000000000000000000000000000000000099"),
token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: Address::ZERO,
};
let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
let bare = ramses::plan::collect_fees(¶ms, CONFIG.position_mgr);
assert_ne!(
&frag.calls[0].calldata[..4],
&[0xac, 0x96, 0x50, 0xd8],
"non-native case must NOT be wrapped in multicall"
);
assert_eq!(
frag.calls[0].calldata, bare.calls[0].calldata,
"non-native pass-through must stay byte-identical to bare collect()"
);
}
#[test]
fn plan_collect_fees_no_native_side_rejects() {
let params = CollectFeesParams {
token_id: U256::from(1u64),
recipient: Address::ZERO,
token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("0000000000000000000000000000000000000002"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
};
let err = plan_collect_fees(¶ms, Chain::Sonic).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
}
fn fixture_remove_and_collect_params_ws_paired() -> RemoveAndCollectParams {
RemoveAndCollectParams {
token_id: U256::from(1u64),
liquidity: 1_000_000u128,
amount0_min: Some(U256::from(100u64)),
amount1_min: Some(U256::from(200u64)),
recipient: Address::ZERO,
token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
}
}
#[test]
fn plan_remove_liquidity_and_collect_native_recipient_emits_4_call_multicall() {
let params = fixture_remove_and_collect_params_ws_paired();
let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
.expect("Sonic supported");
assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
assert_eq!(frag.value, U256::ZERO);
assert_eq!(frag.calls[0].value, U256::ZERO);
assert!(frag.approvals.is_empty());
assert!(
frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
"native remove+collect multicall must include unwrapWETH9(uint256,address) tail"
);
assert!(
frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
"native remove+collect multicall must include sweepToken(address,uint256,address) tail"
);
use alloy_sol_types::SolValue;
let (inner,): (Vec<alloy_primitives::Bytes>,) =
<(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
.expect("decode outer multicall params");
assert_eq!(inner.len(), 4, "expected 4 inner calls");
assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
assert_eq!(&inner[2][..4], &[0x49, 0x40, 0x4b, 0x7c], "inner[2] = unwrapWETH9");
assert_eq!(&inner[3][..4], &[0xdf, 0x2a, 0xb5, 0xbb], "inner[3] = sweepToken");
}
#[test]
fn plan_remove_liquidity_and_collect_non_native_recipient_passthrough() {
let mut params = fixture_remove_and_collect_params_ws_paired();
params.recipient = address!("0000000000000000000000000000000000000099");
let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
.expect("Sonic supported");
assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
let bare =
ramses::plan::remove_liquidity_and_collect(¶ms, 9_999_999_999, CONFIG.position_mgr);
assert_eq!(
frag.calls[0].calldata, bare.calls[0].calldata,
"non-native pass-through must stay byte-identical to bare multicall([decrease, collect])"
);
}
#[test]
fn plan_remove_liquidity_and_collect_no_native_side_rejects() {
let mut params = fixture_remove_and_collect_params_ws_paired();
params.token1 = address!("0000000000000000000000000000000000000002");
let err =
plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
}
#[test]
fn config_for_chain_returns_some_for_sonic_only() {
assert_eq!(config_for_chain(Chain::Sonic), Some(&CONFIG));
for unsupported in [
Chain::Ethereum,
Chain::Arbitrum,
Chain::Optimism,
Chain::Polygon,
Chain::Base,
Chain::Bsc,
Chain::Avalanche,
Chain::Celo,
] {
assert!(
config_for_chain(unsupported).is_none(),
"shadow should not surface {unsupported:?}",
);
}
}
#[test]
fn position_view_and_key_are_re_exported() {
let _: fn(
alloy_primitives::Address,
alloy_primitives::U256,
i32,
i32,
) -> alloy_primitives::B256 = position_key;
let _: std::marker::PhantomData<RamsesPositionView> = std::marker::PhantomData;
}
#[test]
fn factory_returns_chain_specific_address_via_layer2() {
assert_eq!(factory(Chain::Sonic), Some(CONFIG.factory));
for unsupported in [
Chain::Ethereum,
Chain::Arbitrum,
Chain::Optimism,
Chain::Polygon,
Chain::Base,
Chain::Bsc,
Chain::Avalanche,
Chain::Celo,
] {
assert_eq!(factory(unsupported), None);
}
}
#[tokio::test]
async fn pool_state_routes_to_chain_specific_multicall() {
let Some(rpc) = std::env::var("SONIC_RPC_URL").ok() else {
eprintln!(
"SKIP pool_state_routes_to_chain_specific_multicall: \
set SONIC_RPC_URL to a Sonic archive RPC to enable"
);
return;
};
let anvil = alloy::node_bindings::Anvil::new().fork(rpc).spawn();
let provider = alloy::providers::ProviderBuilder::new().connect_http(anvil.endpoint_url());
let sonic_pool = address!("324963c267C354c7660Ce8CA3F5f167E05649970");
let state = pool_state(&provider, Chain::Sonic, sonic_pool)
.await
.expect("Sonic pool_state must succeed via Layer 2 chain-aware routing");
assert!(state.liquidity > 0, "Real on-chain Shadow pool should have non-zero liquidity");
}
#[test]
fn pool_address_with_override_uses_override_not_config_hash() {
let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
let custom_hash = b256!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
let with_override =
pool_address(Chain::Sonic, usdc_e, ws, 50, Some(custom_hash)).expect("Sonic supported");
let without_override =
pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
assert_ne!(with_override, without_override);
}
}