1use alloy_primitives::{address, b256, Address, B256, U256};
2use alloy_provider::{network::Ethereum, Provider};
3use alloy_sol_types::SolCall;
4use anyhow::{anyhow, Result};
5use wp_evm_base::{chain::Chain, types::SlippageBps};
6use wp_evm_ramses_interfaces::periphery::router::IRamsesPeripheryRouter;
7use wp_evm_ramses_provider as ramses;
8pub use wp_evm_ramses_provider::data::RamsesProtocolConfig;
9use wp_evm_v3_provider::plan::PeripherySelectors;
10
11const SELECTORS: PeripherySelectors = PeripherySelectors {
15 multicall: IRamsesPeripheryRouter::multicallCall::SELECTOR,
16 unwrap_native: IRamsesPeripheryRouter::unwrapWETH9Call::SELECTOR,
17 sweep_token: IRamsesPeripheryRouter::sweepTokenCall::SELECTOR,
18 refund_native: IRamsesPeripheryRouter::refundETHCall::SELECTOR,
19};
20
21pub use wp_evm_ramses_provider::data::{
22 CollectFeesParams, ExactInParams, ExactOutParams, GaugeClaim, GaugeEarnedGrid, PlanFragment,
23 PoolState, PositionState, Quote, RamsesAddLiquidityParams, RemoveAndCollectParams,
24 RemoveLiquidityParams,
25};
26pub use wp_evm_ramses_provider::gauge::gauge_earned_grids;
27pub use wp_evm_ramses_provider::position::{position_key, RamsesPositionView};
28pub use wp_evm_ramses_provider::position_views::{PositionFees, PositionViewEntry};
29pub use wp_evm_ramses_provider::quote::QuoteError;
30pub use wp_evm_ramses_provider::Enumeration;
31pub use wp_evm_ramses_provider::plan;
33pub use wp_evm_ramses_provider::pool_address as pool_address_raw;
34pub use wp_evm_v3_provider::pool_views::{PoolReadEntry, PoolViewData};
35
36pub async fn pool_views<P: Provider<Ethereum>>(
38 provider: &P,
39 pools: &[Address],
40) -> Result<Vec<PoolReadEntry>> {
41 wp_evm_v3_provider::pool_views::pool_views(
42 provider,
43 ramses::MULTICALL3_ADDRESS,
44 pools,
45 &ramses::pool_views::RamsesPoolViewSource,
46 )
47 .await
48}
49
50pub async fn position_token_pair<P: Provider<Ethereum>>(
51 provider: &P,
52 nfpm: Address,
53 token_id: U256,
54) -> Result<(Address, Address)> {
55 ramses::hydrate::position_token_pair(provider, nfpm, token_id).await
56}
57
58pub const CONFIG: RamsesProtocolConfig = RamsesProtocolConfig {
59 factory: address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"),
60 pool_deployer: address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"),
61 router: address!("5543c6176feb9b4b179078205d7c29eea2e2d695"),
62 position_mgr: address!("12E66C8F215DdD5d48d150c8f46aD0c6fB0F4406"),
63 init_code_hash: b256!("c701ee63862761c31d620a4a083c61bdc1e81761e6b9c9267fd19afd22e0821d"),
64 tick_spacings: &[1, 5, 10, 50, 100, 200],
65 multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
66 quoter: None,
67 voter: address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D"),
68};
69
70pub fn config_for_chain(chain: Chain) -> Option<&'static RamsesProtocolConfig> {
71 match chain {
72 Chain::Sonic => Some(&CONFIG),
73 Chain::Ethereum
74 | Chain::Arbitrum
75 | Chain::Optimism
76 | Chain::Polygon
77 | Chain::Base
78 | Chain::Bsc
79 | Chain::HyperEvm
80 | Chain::Avalanche
81 | Chain::Celo => None,
82 }
83}
84
85pub fn factory(chain: Chain) -> Option<Address> {
86 config_for_chain(chain).map(|c| c.factory)
87}
88pub fn pool_deployer(chain: Chain) -> Option<Address> {
89 config_for_chain(chain).map(|c| c.pool_deployer)
90}
91pub fn position_manager(chain: Chain) -> Option<Address> {
92 config_for_chain(chain).map(|c| c.position_mgr)
93}
94pub fn router(chain: Chain) -> Option<Address> {
95 config_for_chain(chain).map(|c| c.router)
96}
97pub fn quoter(chain: Chain) -> Option<Address> {
98 config_for_chain(chain).and_then(|c| c.quoter)
99}
100pub fn multicall(chain: Chain) -> Option<Address> {
101 config_for_chain(chain).map(|c| c.multicall)
102}
103pub fn init_code_hash(chain: Chain) -> Option<B256> {
104 config_for_chain(chain).map(|c| c.init_code_hash)
105}
106pub fn voter(chain: Chain) -> Option<Address> {
107 config_for_chain(chain).map(|c| c.voter)
108}
109pub fn supports(chain: Chain) -> bool {
110 config_for_chain(chain).is_some()
111}
112
113pub async fn pool_state<P: Provider<Ethereum>>(
114 provider: &P,
115 chain: Chain,
116 pool: Address,
117) -> Result<PoolState> {
118 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
119 ramses::hydrate::pool_state(provider, cfg.multicall, pool).await
120}
121
122pub async fn position_state<P: Provider<Ethereum>>(
123 provider: &P,
124 chain: Chain,
125 token_id: U256,
126) -> Result<PositionState> {
127 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
128 ramses::hydrate::position_state_shadow(provider, cfg.multicall, cfg.position_mgr, token_id)
129 .await
130}
131
132pub async fn position_views<P: Provider<Ethereum>>(
133 provider: &P,
134 chain: Chain,
135 token_ids: &[U256],
136) -> Result<Vec<PositionViewEntry<RamsesPositionView>>> {
137 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
138 ramses::position_views::position_views(provider, cfg.multicall, cfg.position_mgr, token_ids)
139 .await
140}
141
142pub async fn position_views_with_nfpm<P: Provider<Ethereum>>(
143 provider: &P,
144 chain: Chain,
145 nfpm: Address,
146 token_ids: &[U256],
147) -> Result<Vec<PositionViewEntry<RamsesPositionView>>> {
148 let multicall =
149 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
150 ramses::position_views::position_views(provider, multicall, nfpm, token_ids).await
151}
152
153pub async fn enumerate_owner_token_ids<P: Provider<Ethereum>>(
154 provider: &P,
155 chain: Chain,
156 nfpm: Address,
157 owner: Address,
158) -> Result<Enumeration> {
159 let multicall =
160 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
161 ramses::enumerate_owner_token_ids(provider, multicall, nfpm, owner, chain).await
162}
163
164pub async fn populate_positions_fees<P: Provider<Ethereum>>(
165 provider: &P,
166 chain: Chain,
167 entries: &mut [PositionViewEntry<RamsesPositionView>],
168) -> Result<()> {
169 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
170 ramses::position_views::populate_position_fees(provider, cfg.multicall, entries, |v| {
171 pool_address(chain, v.token0, v.token1, v.tick_spacing, None)
172 })
173 .await
174}
175
176pub fn quote_exact_in(s: &PoolState, p: &ExactInParams) -> Result<Quote, QuoteError> {
177 ramses::quote::exact_in(s, p)
178}
179pub fn quote_exact_out(s: &PoolState, p: &ExactOutParams) -> Result<Quote, QuoteError> {
180 ramses::quote::exact_out(s, p)
181}
182
183pub async fn populate_ticks<P: Provider<Ethereum>>(
184 provider: &P,
185 chain: Chain,
186 pool: Address,
187 state: &mut PoolState,
188) -> Result<()> {
189 config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
190 ramses::populate_ticks::populate_ticks(provider, pool, state).await
191}
192
193pub async fn quote_online_exact_in<P: Provider<Ethereum>>(
194 provider: &P,
195 chain: Chain,
196 state: &PoolState,
197 params: &ExactInParams,
198) -> Result<Quote> {
199 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
200 let quoter = cfg.quoter.ok_or_else(|| anyhow!("Shadow quoter not registered on {chain:?}"))?;
201 ramses::quote_online::quote_online_exact_in(provider, quoter, state, params).await
202}
203
204pub fn plan_swap_exact_in(
205 s: &PoolState,
206 q: &Quote,
207 p: &ExactInParams,
208 slippage: SlippageBps,
209 deadline: u64,
210 chain: Chain,
211) -> Result<PlanFragment> {
212 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
213 Ok(ramses::plan::swap_exact_in(s, q, p, slippage, deadline, cfg.router))
214}
215
216pub fn plan_add_liquidity(
217 p: &RamsesAddLiquidityParams,
218 slippage: SlippageBps,
219 deadline: u64,
220 chain: Chain,
221) -> Result<PlanFragment> {
222 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
223 Ok(ramses::plan::add_liquidity(p, slippage, deadline, cfg.position_mgr))
224}
225
226#[allow(clippy::too_many_arguments)]
227pub fn plan_increase_liquidity(
228 token_id: U256,
229 token0: Address,
230 token1: Address,
231 amount0_desired: U256,
232 amount1_desired: U256,
233 slippage: SlippageBps,
234 deadline: u64,
235 chain: Chain,
236) -> Result<PlanFragment> {
237 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
238 Ok(ramses::plan::increase_liquidity(
239 token_id,
240 token0,
241 token1,
242 amount0_desired,
243 amount1_desired,
244 slippage,
245 deadline,
246 cfg.position_mgr,
247 ))
248}
249
250pub fn plan_remove_liquidity(
251 p: &RemoveLiquidityParams,
252 deadline: u64,
253 chain: Chain,
254) -> Result<PlanFragment> {
255 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
256 Ok(ramses::plan::remove_liquidity(p, deadline, cfg.position_mgr))
257}
258
259pub fn plan_remove_liquidity_and_collect(
281 p: &RemoveAndCollectParams,
282 deadline: u64,
283 chain: Chain,
284) -> Result<PlanFragment> {
285 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
286
287 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_remove_and_collect(
291 p,
292 cfg.position_mgr,
293 chain,
294 )?;
295
296 let mut core_params = (*p).clone();
300 core_params.recipient = wrap.effective_collect_recipient;
301
302 let frag = ramses::plan::remove_liquidity_and_collect(&core_params, deadline, cfg.position_mgr);
303 wp_evm_v3_provider::plan::compose_native_remove_collect_multicall(frag, &wrap, SELECTORS)
304}
305
306pub fn plan_collect_fees(p: &CollectFeesParams, chain: Chain) -> Result<PlanFragment> {
307 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
308
309 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_collect(p, cfg.position_mgr, chain)?;
322
323 let core_params = CollectFeesParams {
328 token_id: p.token_id,
329 recipient: wrap.effective_recipient,
330 token0: p.token0,
331 token1: p.token1,
332 caller: p.caller,
333 };
334
335 let frag = ramses::plan::collect_fees(&core_params, cfg.position_mgr);
336 Ok(wp_evm_v3_provider::plan::compose_native_collect_multicall(frag, &wrap, SELECTORS))
337}
338
339pub fn pool_address(
340 chain: Chain,
341 token_a: Address,
342 token_b: Address,
343 tick_spacing: i32,
344 init_code_hash_override: Option<B256>,
345) -> Option<Address> {
346 let cfg = config_for_chain(chain)?;
347 let init_code_hash = init_code_hash_override.unwrap_or(cfg.init_code_hash);
348 Some(ramses::pool_address::compute(
349 cfg.pool_deployer,
350 init_code_hash,
351 token_a,
352 token_b,
353 tick_spacing,
354 ))
355}
356
357pub async fn pending_emissions<P: Provider<Ethereum>>(
358 provider: &P,
359 chain: Chain,
360 pool: Address,
361 token_id: U256,
362) -> Result<Option<wp_evm_ramses_provider::gauge::PendingEmissions>> {
363 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
364 wp_evm_ramses_provider::gauge::pending_emissions(
365 provider,
366 cfg.multicall,
367 cfg.voter,
368 pool,
369 token_id,
370 )
371 .await
372}
373
374pub fn plan_claim_cl_gauge_rewards(chain: Chain, claims: &[GaugeClaim]) -> Result<PlanFragment> {
377 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
378 Ok(ramses::plan::claim_cl_gauge_rewards(cfg.voter, claims))
379}
380
381pub async fn claim_cl_gauge_rewards_online<P: Provider<Ethereum>>(
387 provider: &P,
388 chain: Chain,
389 positions: &[(Address, U256)],
390) -> Result<Option<PlanFragment>> {
391 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
392 let grids =
393 ramses::gauge::gauge_earned_grids(provider, cfg.multicall, cfg.voter, positions).await?;
394 Ok(ramses::plan::claim_cl_gauge_rewards_from_grids(cfg.voter, &grids))
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use wp_evm_ramses_provider::data::TickInfo;
403
404 #[test]
405 fn gauge_earned_grids_is_reexported_at_facade() {
406 let _f = crate::gauge_earned_grids::<alloy_provider::RootProvider>;
408 }
409
410 #[test]
411 fn config_router_is_known_shadow_address() {
412 assert_eq!(CONFIG.router, address!("5543c6176feb9b4b179078205d7c29eea2e2d695"));
413 }
414
415 #[test]
416 fn config_factory_is_known_shadow_address() {
417 assert_eq!(CONFIG.factory, address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"));
418 }
419
420 #[test]
421 fn config_pool_deployer_matches_factory_ramsesv3pooldeployer_getter() {
422 assert_eq!(CONFIG.pool_deployer, address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"));
426 }
427
428 #[test]
429 fn pool_address_matches_canonical_usdce_ws_ts50_sonic() {
430 let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
431 let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
432 let pool = pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
433 assert_eq!(pool, address!("324963c267C354c7660Ce8CA3F5f167E05649970"));
434 }
435
436 #[test]
437 fn config_tick_spacings_match_shadow() {
438 assert_eq!(CONFIG.tick_spacings, &[1, 5, 10, 50, 100, 200]);
439 }
440
441 #[test]
444 fn plan_add_liquidity_accepts_ramses_params_with_tick_spacing() {
445 let p = RamsesAddLiquidityParams {
446 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
447 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
448 tick_spacing: 50,
449 tick_lower: -887_272,
450 tick_upper: 887_272,
451 amount0_desired: U256::from(1_000_000u64),
452 amount1_desired: U256::from(500_000_000_000_000u64),
453 recipient: address!("0000000000000000000000000000000000000099"),
454 };
455 let frag = plan_add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, Chain::Sonic)
456 .expect("Sonic supported");
457 assert_eq!(frag.calls.len(), 1);
458 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
459 assert_eq!(frag.approvals.len(), 2);
460 assert_eq!(frag.approvals[0].token, p.token0);
461 assert_eq!(frag.approvals[1].token, p.token1);
462 assert_eq!(frag.value, U256::ZERO);
463 }
464
465 fn fixture_usdc_weth() -> PoolState {
466 let sqrt_price_x96 = U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
467 PoolState {
468 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
469 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
470 fee: 3000,
471 tick_spacing: 50,
472 sqrt_price_x96,
473 liquidity: 2_000_000_000_000_000_000_000u128,
474 tick: 76012,
475 ticks: vec![
476 TickInfo {
477 tick: 74950,
478 liquidity_net: 1_000_000_000_000_000_000_000i128,
479 liquidity_gross: 1_000_000_000_000_000_000_000u128,
480 },
481 TickInfo {
482 tick: 75950,
483 liquidity_net: 1_000_000_000_000_000_000_000i128,
484 liquidity_gross: 1_000_000_000_000_000_000_000u128,
485 },
486 TickInfo {
487 tick: 76050,
488 liquidity_net: -2_000_000_000_000_000_000_000i128,
489 liquidity_gross: 2_000_000_000_000_000_000_000u128,
490 },
491 ],
492 }
493 }
494
495 #[test]
496 fn quote_exact_in_delegates_to_ramses_family() {
497 let state = fixture_usdc_weth();
498 let params = ExactInParams {
499 token_in: state.token0,
500 token_out: state.token1,
501 amount_in: U256::from(1_000_000u64),
502 recipient: address!("0000000000000000000000000000000000000099"),
503 };
504 let quote = quote_exact_in(&state, ¶ms).expect("quote should succeed");
505 assert!(quote.amount_out > U256::ZERO);
506 assert_eq!(quote.amount_in, params.amount_in);
507 }
508
509 #[test]
510 fn plan_swap_exact_in_targets_shadow_router() {
511 let state = fixture_usdc_weth();
512 let quote = Quote {
513 amount_in: U256::from(1_000_000u64),
514 amount_out: U256::from(500_000_000_000_000u64),
515 sqrt_price_x96_after: state.sqrt_price_x96,
516 price_impact_bps: 0,
517 };
518 let params = ExactInParams {
519 token_in: state.token0,
520 token_out: state.token1,
521 amount_in: quote.amount_in,
522 recipient: address!("0000000000000000000000000000000000000099"),
523 };
524 let frag = plan_swap_exact_in(
525 &state,
526 "e,
527 ¶ms,
528 SlippageBps::new(50),
529 u64::MAX,
530 Chain::Sonic,
531 )
532 .expect("Sonic supported");
533 assert_eq!(frag.calls.len(), 1);
534 assert_eq!(frag.calls[0].target, CONFIG.router);
535 assert_eq!(frag.approvals.len(), 1);
536 }
537
538 #[test]
539 fn quote_exact_out_delegates_to_ramses_family() {
540 let state = fixture_usdc_weth();
541 let params = ExactOutParams {
542 token_in: state.token0,
543 token_out: state.token1,
544 amount_out: U256::from(500_000_000_000_000u64),
545 recipient: address!("0000000000000000000000000000000000000099"),
546 };
547 let quote = quote_exact_out(&state, ¶ms).expect("exact-out quote should succeed");
548 assert!(quote.amount_in > U256::ZERO);
549 assert_eq!(quote.amount_out, params.amount_out);
550 }
551
552 #[test]
553 fn plan_remove_liquidity_targets_position_manager_no_approvals() {
554 let params = RemoveLiquidityParams {
555 token_id: U256::from(42u64),
556 liquidity: 1_000_000_000_000u128,
557 amount0_min: None,
558 amount1_min: None,
559 };
560 let frag = plan_remove_liquidity(¶ms, u64::MAX, Chain::Sonic).expect("Sonic supported");
561 assert_eq!(frag.calls.len(), 1);
562 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
563 assert!(frag.approvals.is_empty());
564 assert_eq!(frag.value, U256::ZERO);
565 }
566
567 #[test]
568 fn plan_collect_fees_targets_position_manager_no_approvals() {
569 let params = CollectFeesParams {
570 token_id: U256::from(42u64),
571 recipient: address!("0000000000000000000000000000000000000099"),
572 token0: address!("0000000000000000000000000000000000000001"),
573 token1: address!("0000000000000000000000000000000000000002"),
574 caller: Address::ZERO,
575 };
576 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
577 assert_eq!(frag.calls.len(), 1);
578 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
579 assert!(frag.approvals.is_empty());
580 assert_eq!(frag.value, U256::ZERO);
581 }
582
583 #[test]
584 fn plan_claim_cl_gauge_rewards_targets_voter() {
585 let claims = vec![GaugeClaim {
586 gauge: address!("1111111111111111111111111111111111111111"),
587 reward_tokens: vec![address!("aaaa000000000000000000000000000000000000")],
588 token_ids: vec![U256::from(1u64)],
589 }];
590 let frag = plan_claim_cl_gauge_rewards(Chain::Sonic, &claims).expect("Sonic supported");
591 assert_eq!(frag.calls.len(), 1);
592 assert_eq!(frag.calls[0].target, CONFIG.voter);
593 assert!(frag.approvals.is_empty());
594 assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
595 }
596
597 #[test]
598 fn plan_claim_cl_gauge_rewards_rejects_unsupported_chain() {
599 let claims = vec![GaugeClaim {
600 gauge: Address::ZERO,
601 reward_tokens: vec![Address::ZERO],
602 token_ids: vec![U256::from(1u64)],
603 }];
604 assert!(plan_claim_cl_gauge_rewards(Chain::Base, &claims).is_err());
605 }
606
607 #[test]
608 fn plan_collect_fees_native_recipient_emits_multicall_with_unwrap_and_sweep() {
609 let params = CollectFeesParams {
614 token_id: U256::from(1u64),
615 recipient: Address::ZERO,
616 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
619 };
620 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
621
622 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
623 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
624 assert_eq!(frag.value, U256::ZERO);
625 assert_eq!(frag.calls[0].value, U256::ZERO);
626 assert!(
627 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
628 "native collect multicall must include unwrapWETH9(uint256,address) tail"
629 );
630 assert!(
631 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
632 "native collect multicall must include sweepToken(address,uint256,address) tail"
633 );
634 }
635
636 #[test]
637 fn plan_collect_fees_non_native_recipient_passthrough() {
638 let params = CollectFeesParams {
639 token_id: U256::from(1u64),
640 recipient: address!("0000000000000000000000000000000000000099"),
641 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: Address::ZERO,
644 };
645 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
646 let bare = ramses::plan::collect_fees(¶ms, CONFIG.position_mgr);
647
648 assert_ne!(
649 &frag.calls[0].calldata[..4],
650 &[0xac, 0x96, 0x50, 0xd8],
651 "non-native case must NOT be wrapped in multicall"
652 );
653 assert_eq!(
654 frag.calls[0].calldata, bare.calls[0].calldata,
655 "non-native pass-through must stay byte-identical to bare collect()"
656 );
657 }
658
659 #[test]
660 fn plan_collect_fees_no_native_side_rejects() {
661 let params = CollectFeesParams {
664 token_id: U256::from(1u64),
665 recipient: Address::ZERO,
666 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("0000000000000000000000000000000000000002"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
669 };
670 let err = plan_collect_fees(¶ms, Chain::Sonic).unwrap_err();
671 let msg = format!("{err:#}");
672 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
673 }
674
675 fn fixture_remove_and_collect_params_ws_paired() -> RemoveAndCollectParams {
681 RemoveAndCollectParams {
682 token_id: U256::from(1u64),
683 liquidity: 1_000_000u128,
684 amount0_min: Some(U256::from(100u64)),
685 amount1_min: Some(U256::from(200u64)),
686 recipient: Address::ZERO,
687 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
690 }
691 }
692
693 #[test]
694 fn plan_remove_liquidity_and_collect_native_recipient_emits_4_call_multicall() {
695 let params = fixture_remove_and_collect_params_ws_paired();
700 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
701 .expect("Sonic supported");
702
703 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
704 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
705 assert_eq!(frag.value, U256::ZERO);
706 assert_eq!(frag.calls[0].value, U256::ZERO);
707 assert!(frag.approvals.is_empty());
708
709 assert!(
712 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
713 "native remove+collect multicall must include unwrapWETH9(uint256,address) tail"
714 );
715 assert!(
717 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
718 "native remove+collect multicall must include sweepToken(address,uint256,address) tail"
719 );
720
721 use alloy_sol_types::SolValue;
723 let (inner,): (Vec<alloy_primitives::Bytes>,) =
724 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
725 .expect("decode outer multicall params");
726 assert_eq!(inner.len(), 4, "expected 4 inner calls");
727 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
728 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
729 assert_eq!(&inner[2][..4], &[0x49, 0x40, 0x4b, 0x7c], "inner[2] = unwrapWETH9");
730 assert_eq!(&inner[3][..4], &[0xdf, 0x2a, 0xb5, 0xbb], "inner[3] = sweepToken");
731 }
732
733 #[test]
734 fn plan_remove_liquidity_and_collect_non_native_recipient_passthrough() {
735 let mut params = fixture_remove_and_collect_params_ws_paired();
739 params.recipient = address!("0000000000000000000000000000000000000099");
740 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
741 .expect("Sonic supported");
742
743 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
745 let bare =
749 ramses::plan::remove_liquidity_and_collect(¶ms, 9_999_999_999, CONFIG.position_mgr);
750 assert_eq!(
751 frag.calls[0].calldata, bare.calls[0].calldata,
752 "non-native pass-through must stay byte-identical to bare multicall([decrease, collect])"
753 );
754 }
755
756 #[test]
757 fn plan_remove_liquidity_and_collect_no_native_side_rejects() {
758 let mut params = fixture_remove_and_collect_params_ws_paired();
761 params.token1 = address!("0000000000000000000000000000000000000002");
762 let err =
763 plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic).unwrap_err();
764 let msg = format!("{err:#}");
765 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
766 }
767
768 #[test]
769 fn config_for_chain_returns_some_for_sonic_only() {
770 assert_eq!(config_for_chain(Chain::Sonic), Some(&CONFIG));
771
772 for unsupported in [
773 Chain::Ethereum,
774 Chain::Arbitrum,
775 Chain::Optimism,
776 Chain::Polygon,
777 Chain::Base,
778 Chain::Bsc,
779 Chain::Avalanche,
780 Chain::Celo,
781 ] {
782 assert!(
783 config_for_chain(unsupported).is_none(),
784 "shadow should not surface {unsupported:?}",
785 );
786 }
787 }
788
789 #[test]
790 fn position_view_and_key_are_re_exported() {
791 let _: fn(
793 alloy_primitives::Address,
794 alloy_primitives::U256,
795 i32,
796 i32,
797 ) -> alloy_primitives::B256 = position_key;
798 let _: std::marker::PhantomData<RamsesPositionView> = std::marker::PhantomData;
799 }
800
801 #[test]
806 fn factory_returns_chain_specific_address_via_layer2() {
807 assert_eq!(factory(Chain::Sonic), Some(CONFIG.factory));
808 for unsupported in [
809 Chain::Ethereum,
810 Chain::Arbitrum,
811 Chain::Optimism,
812 Chain::Polygon,
813 Chain::Base,
814 Chain::Bsc,
815 Chain::Avalanche,
816 Chain::Celo,
817 ] {
818 assert_eq!(factory(unsupported), None);
819 }
820 }
821
822 #[tokio::test]
831 async fn pool_state_routes_to_chain_specific_multicall() {
832 let Some(rpc) = std::env::var("SONIC_RPC_URL").ok() else {
833 eprintln!(
834 "SKIP pool_state_routes_to_chain_specific_multicall: \
835 set SONIC_RPC_URL to a Sonic archive RPC to enable"
836 );
837 return;
838 };
839 let anvil = alloy::node_bindings::Anvil::new().fork(rpc).spawn();
840 let provider = alloy::providers::ProviderBuilder::new().connect_http(anvil.endpoint_url());
841
842 let sonic_pool = address!("324963c267C354c7660Ce8CA3F5f167E05649970");
845 let state = pool_state(&provider, Chain::Sonic, sonic_pool)
846 .await
847 .expect("Sonic pool_state must succeed via Layer 2 chain-aware routing");
848 assert!(state.liquidity > 0, "Real on-chain Shadow pool should have non-zero liquidity");
849 }
850
851 #[test]
852 fn pool_address_with_override_uses_override_not_config_hash() {
853 let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
854 let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
855 let custom_hash = b256!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
856
857 let with_override =
858 pool_address(Chain::Sonic, usdc_e, ws, 50, Some(custom_hash)).expect("Sonic supported");
859 let without_override =
860 pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
861
862 assert_ne!(with_override, without_override);
863 }
864}