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