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};
35pub use wp_evm_ramses_provider::plan::{mintCall, ShadowMintParams};
39pub use wp_evm_v3_provider::plan::{
40 burnCall, collectCall, decreaseLiquidityCall, increaseLiquidityCall, CollectParams,
41 DecreaseLiquidityParams, IncreaseLiquidityParams,
42};
43
44pub async fn pool_views<P: Provider<Ethereum>>(
46 provider: &P,
47 pools: &[Address],
48) -> Result<Vec<PoolReadEntry>> {
49 wp_evm_v3_provider::pool_views::pool_views(
50 provider,
51 ramses::MULTICALL3_ADDRESS,
52 pools,
53 &ramses::pool_views::RamsesPoolViewSource,
54 )
55 .await
56}
57
58pub async fn position_token_pair<P: Provider<Ethereum>>(
59 provider: &P,
60 nfpm: Address,
61 token_id: U256,
62) -> Result<(Address, Address)> {
63 ramses::hydrate::position_token_pair(provider, nfpm, token_id).await
64}
65
66pub const CONFIG: RamsesProtocolConfig = RamsesProtocolConfig {
67 factory: address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"),
68 pool_deployer: address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"),
69 router: address!("5543c6176feb9b4b179078205d7c29eea2e2d695"),
70 position_mgr: address!("12E66C8F215DdD5d48d150c8f46aD0c6fB0F4406"),
71 init_code_hash: b256!("c701ee63862761c31d620a4a083c61bdc1e81761e6b9c9267fd19afd22e0821d"),
72 tick_spacings: &[1, 5, 10, 50, 100, 200],
73 multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
74 quoter: None,
75 voter: address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D"),
76};
77
78pub fn config_for_chain(chain: Chain) -> Option<&'static RamsesProtocolConfig> {
79 match chain {
80 Chain::Sonic => Some(&CONFIG),
81 Chain::Ethereum
82 | Chain::Arbitrum
83 | Chain::Optimism
84 | Chain::Polygon
85 | Chain::Base
86 | Chain::Bsc
87 | Chain::HyperEvm
88 | Chain::Avalanche
89 | Chain::Celo => None,
90 }
91}
92
93pub fn factory(chain: Chain) -> Option<Address> {
94 config_for_chain(chain).map(|c| c.factory)
95}
96pub fn pool_deployer(chain: Chain) -> Option<Address> {
97 config_for_chain(chain).map(|c| c.pool_deployer)
98}
99pub fn position_manager(chain: Chain) -> Option<Address> {
100 config_for_chain(chain).map(|c| c.position_mgr)
101}
102pub fn router(chain: Chain) -> Option<Address> {
103 config_for_chain(chain).map(|c| c.router)
104}
105pub fn quoter(chain: Chain) -> Option<Address> {
106 config_for_chain(chain).and_then(|c| c.quoter)
107}
108pub fn multicall(chain: Chain) -> Option<Address> {
109 config_for_chain(chain).map(|c| c.multicall)
110}
111pub fn init_code_hash(chain: Chain) -> Option<B256> {
112 config_for_chain(chain).map(|c| c.init_code_hash)
113}
114pub fn voter(chain: Chain) -> Option<Address> {
115 config_for_chain(chain).map(|c| c.voter)
116}
117pub fn supports(chain: Chain) -> bool {
118 config_for_chain(chain).is_some()
119}
120
121pub async fn pool_state<P: Provider<Ethereum>>(
122 provider: &P,
123 chain: Chain,
124 pool: Address,
125) -> Result<PoolState> {
126 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
127 ramses::hydrate::pool_state(provider, cfg.multicall, pool).await
128}
129
130pub async fn position_state<P: Provider<Ethereum>>(
131 provider: &P,
132 chain: Chain,
133 token_id: U256,
134) -> Result<PositionState> {
135 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
136 ramses::hydrate::position_state_shadow(provider, cfg.multicall, cfg.position_mgr, token_id)
137 .await
138}
139
140pub async fn position_views<P: Provider<Ethereum>>(
141 provider: &P,
142 chain: Chain,
143 token_ids: &[U256],
144) -> Result<Vec<PositionViewEntry<RamsesPositionView>>> {
145 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
146 ramses::position_views::position_views(provider, cfg.multicall, cfg.position_mgr, token_ids)
147 .await
148}
149
150pub async fn position_views_with_nfpm<P: Provider<Ethereum>>(
151 provider: &P,
152 chain: Chain,
153 nfpm: Address,
154 token_ids: &[U256],
155) -> Result<Vec<PositionViewEntry<RamsesPositionView>>> {
156 let multicall =
157 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
158 ramses::position_views::position_views(provider, multicall, nfpm, token_ids).await
159}
160
161pub async fn enumerate_owner_token_ids<P: Provider<Ethereum>>(
162 provider: &P,
163 chain: Chain,
164 nfpm: Address,
165 owner: Address,
166) -> Result<Enumeration> {
167 let multicall =
168 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
169 ramses::enumerate_owner_token_ids(provider, multicall, nfpm, owner, chain).await
170}
171
172pub async fn populate_positions_fees<P: Provider<Ethereum>>(
173 provider: &P,
174 chain: Chain,
175 entries: &mut [PositionViewEntry<RamsesPositionView>],
176) -> Result<()> {
177 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
178 ramses::position_views::populate_position_fees(provider, cfg.multicall, entries, |v| {
179 pool_address(chain, v.token0, v.token1, v.tick_spacing, None)
180 })
181 .await
182}
183
184pub fn quote_exact_in(s: &PoolState, p: &ExactInParams) -> Result<Quote, QuoteError> {
185 ramses::quote::exact_in(s, p)
186}
187pub fn quote_exact_out(s: &PoolState, p: &ExactOutParams) -> Result<Quote, QuoteError> {
188 ramses::quote::exact_out(s, p)
189}
190
191pub async fn populate_ticks<P: Provider<Ethereum>>(
192 provider: &P,
193 chain: Chain,
194 pool: Address,
195 state: &mut PoolState,
196) -> Result<()> {
197 config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
198 ramses::populate_ticks::populate_ticks(provider, pool, state).await
199}
200
201pub async fn quote_online_exact_in<P: Provider<Ethereum>>(
202 provider: &P,
203 chain: Chain,
204 state: &PoolState,
205 params: &ExactInParams,
206) -> Result<Quote> {
207 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
208 let quoter = cfg.quoter.ok_or_else(|| anyhow!("Shadow quoter not registered on {chain:?}"))?;
209 ramses::quote_online::quote_online_exact_in(provider, quoter, state, params).await
210}
211
212pub fn plan_swap_exact_in(
213 s: &PoolState,
214 q: &Quote,
215 p: &ExactInParams,
216 slippage: SlippageBps,
217 deadline: u64,
218 chain: Chain,
219) -> Result<PlanFragment> {
220 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
221 Ok(ramses::plan::swap_exact_in(s, q, p, slippage, deadline, cfg.router))
222}
223
224pub fn plan_add_liquidity(
225 p: &RamsesAddLiquidityParams,
226 slippage: SlippageBps,
227 deadline: u64,
228 chain: Chain,
229) -> Result<PlanFragment> {
230 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
231 Ok(ramses::plan::add_liquidity(p, slippage, deadline, cfg.position_mgr))
232}
233
234#[allow(clippy::too_many_arguments)]
235pub fn plan_increase_liquidity(
236 token_id: U256,
237 token0: Address,
238 token1: Address,
239 amount0_desired: U256,
240 amount1_desired: U256,
241 slippage: SlippageBps,
242 deadline: u64,
243 chain: Chain,
244) -> Result<PlanFragment> {
245 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
246 Ok(ramses::plan::increase_liquidity(
247 token_id,
248 token0,
249 token1,
250 amount0_desired,
251 amount1_desired,
252 slippage,
253 deadline,
254 cfg.position_mgr,
255 ))
256}
257
258pub fn plan_remove_liquidity(
259 p: &RemoveLiquidityParams,
260 deadline: u64,
261 chain: Chain,
262) -> Result<PlanFragment> {
263 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
264 Ok(ramses::plan::remove_liquidity(p, deadline, cfg.position_mgr))
265}
266
267pub fn plan_remove_liquidity_and_collect(
289 p: &RemoveAndCollectParams,
290 deadline: u64,
291 chain: Chain,
292) -> Result<PlanFragment> {
293 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
294
295 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_remove_and_collect(
299 p,
300 cfg.position_mgr,
301 chain,
302 )?;
303
304 let mut core_params = (*p).clone();
308 core_params.recipient = wrap.effective_collect_recipient;
309
310 let frag = ramses::plan::remove_liquidity_and_collect(&core_params, deadline, cfg.position_mgr);
311 wp_evm_v3_provider::plan::compose_native_remove_collect_multicall(frag, &wrap, SELECTORS)
312}
313
314pub fn plan_collect_fees(p: &CollectFeesParams, chain: Chain) -> Result<PlanFragment> {
315 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
316
317 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_collect(p, cfg.position_mgr, chain)?;
330
331 let core_params = CollectFeesParams {
336 token_id: p.token_id,
337 recipient: wrap.effective_recipient,
338 token0: p.token0,
339 token1: p.token1,
340 caller: p.caller,
341 };
342
343 let frag = ramses::plan::collect_fees(&core_params, cfg.position_mgr);
344 Ok(wp_evm_v3_provider::plan::compose_native_collect_multicall(frag, &wrap, SELECTORS))
345}
346
347pub fn pool_address(
348 chain: Chain,
349 token_a: Address,
350 token_b: Address,
351 tick_spacing: i32,
352 init_code_hash_override: Option<B256>,
353) -> Option<Address> {
354 let cfg = config_for_chain(chain)?;
355 let init_code_hash = init_code_hash_override.unwrap_or(cfg.init_code_hash);
356 Some(ramses::pool_address::compute(
357 cfg.pool_deployer,
358 init_code_hash,
359 token_a,
360 token_b,
361 tick_spacing,
362 ))
363}
364
365pub async fn pending_emissions<P: Provider<Ethereum>>(
366 provider: &P,
367 chain: Chain,
368 pool: Address,
369 token_id: U256,
370) -> Result<Option<wp_evm_ramses_provider::gauge::PendingEmissions>> {
371 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
372 wp_evm_ramses_provider::gauge::pending_emissions(
373 provider,
374 cfg.multicall,
375 cfg.voter,
376 pool,
377 token_id,
378 )
379 .await
380}
381
382pub fn plan_claim_cl_gauge_rewards(chain: Chain, claims: &[GaugeClaim]) -> Result<PlanFragment> {
385 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
386 Ok(ramses::plan::claim_cl_gauge_rewards(cfg.voter, claims))
387}
388
389pub async fn claim_cl_gauge_rewards_online<P: Provider<Ethereum>>(
395 provider: &P,
396 chain: Chain,
397 positions: &[(Address, U256)],
398) -> Result<Option<PlanFragment>> {
399 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
400 let grids =
401 ramses::gauge::gauge_earned_grids(provider, cfg.multicall, cfg.voter, positions).await?;
402 Ok(ramses::plan::claim_cl_gauge_rewards_from_grids(cfg.voter, &grids))
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use wp_evm_ramses_provider::data::TickInfo;
411
412 #[test]
413 fn gauge_earned_grids_is_reexported_at_facade() {
414 let _f = crate::gauge_earned_grids::<alloy_provider::RootProvider>;
416 }
417
418 #[test]
419 fn config_router_is_known_shadow_address() {
420 assert_eq!(CONFIG.router, address!("5543c6176feb9b4b179078205d7c29eea2e2d695"));
421 }
422
423 #[test]
424 fn config_factory_is_known_shadow_address() {
425 assert_eq!(CONFIG.factory, address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"));
426 }
427
428 #[test]
429 fn config_pool_deployer_matches_factory_ramsesv3pooldeployer_getter() {
430 assert_eq!(CONFIG.pool_deployer, address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"));
434 }
435
436 #[test]
437 fn pool_address_matches_canonical_usdce_ws_ts50_sonic() {
438 let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
439 let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
440 let pool = pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
441 assert_eq!(pool, address!("324963c267C354c7660Ce8CA3F5f167E05649970"));
442 }
443
444 #[test]
445 fn config_tick_spacings_match_shadow() {
446 assert_eq!(CONFIG.tick_spacings, &[1, 5, 10, 50, 100, 200]);
447 }
448
449 #[test]
452 fn plan_add_liquidity_accepts_ramses_params_with_tick_spacing() {
453 let p = RamsesAddLiquidityParams {
454 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
455 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
456 tick_spacing: 50,
457 tick_lower: -887_272,
458 tick_upper: 887_272,
459 amount0_desired: U256::from(1_000_000u64),
460 amount1_desired: U256::from(500_000_000_000_000u64),
461 recipient: address!("0000000000000000000000000000000000000099"),
462 };
463 let frag = plan_add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, Chain::Sonic)
464 .expect("Sonic supported");
465 assert_eq!(frag.calls.len(), 1);
466 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
467 assert_eq!(frag.approvals.len(), 2);
468 assert_eq!(frag.approvals[0].token, p.token0);
469 assert_eq!(frag.approvals[1].token, p.token1);
470 assert_eq!(frag.value, U256::ZERO);
471 }
472
473 fn fixture_usdc_weth() -> PoolState {
474 let sqrt_price_x96 = U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
475 PoolState {
476 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
477 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
478 fee: 3000,
479 tick_spacing: 50,
480 sqrt_price_x96,
481 liquidity: 2_000_000_000_000_000_000_000u128,
482 tick: 76012,
483 ticks: vec![
484 TickInfo {
485 tick: 74950,
486 liquidity_net: 1_000_000_000_000_000_000_000i128,
487 liquidity_gross: 1_000_000_000_000_000_000_000u128,
488 },
489 TickInfo {
490 tick: 75950,
491 liquidity_net: 1_000_000_000_000_000_000_000i128,
492 liquidity_gross: 1_000_000_000_000_000_000_000u128,
493 },
494 TickInfo {
495 tick: 76050,
496 liquidity_net: -2_000_000_000_000_000_000_000i128,
497 liquidity_gross: 2_000_000_000_000_000_000_000u128,
498 },
499 ],
500 }
501 }
502
503 #[test]
504 fn quote_exact_in_delegates_to_ramses_family() {
505 let state = fixture_usdc_weth();
506 let params = ExactInParams {
507 token_in: state.token0,
508 token_out: state.token1,
509 amount_in: U256::from(1_000_000u64),
510 recipient: address!("0000000000000000000000000000000000000099"),
511 };
512 let quote = quote_exact_in(&state, ¶ms).expect("quote should succeed");
513 assert!(quote.amount_out > U256::ZERO);
514 assert_eq!(quote.amount_in, params.amount_in);
515 }
516
517 #[test]
518 fn plan_swap_exact_in_targets_shadow_router() {
519 let state = fixture_usdc_weth();
520 let quote = Quote {
521 amount_in: U256::from(1_000_000u64),
522 amount_out: U256::from(500_000_000_000_000u64),
523 sqrt_price_x96_after: state.sqrt_price_x96,
524 price_impact_bps: 0,
525 };
526 let params = ExactInParams {
527 token_in: state.token0,
528 token_out: state.token1,
529 amount_in: quote.amount_in,
530 recipient: address!("0000000000000000000000000000000000000099"),
531 };
532 let frag = plan_swap_exact_in(
533 &state,
534 "e,
535 ¶ms,
536 SlippageBps::new(50),
537 u64::MAX,
538 Chain::Sonic,
539 )
540 .expect("Sonic supported");
541 assert_eq!(frag.calls.len(), 1);
542 assert_eq!(frag.calls[0].target, CONFIG.router);
543 assert_eq!(frag.approvals.len(), 1);
544 }
545
546 #[test]
547 fn quote_exact_out_delegates_to_ramses_family() {
548 let state = fixture_usdc_weth();
549 let params = ExactOutParams {
550 token_in: state.token0,
551 token_out: state.token1,
552 amount_out: U256::from(500_000_000_000_000u64),
553 recipient: address!("0000000000000000000000000000000000000099"),
554 };
555 let quote = quote_exact_out(&state, ¶ms).expect("exact-out quote should succeed");
556 assert!(quote.amount_in > U256::ZERO);
557 assert_eq!(quote.amount_out, params.amount_out);
558 }
559
560 #[test]
561 fn plan_remove_liquidity_targets_position_manager_no_approvals() {
562 let params = RemoveLiquidityParams {
563 token_id: U256::from(42u64),
564 liquidity: 1_000_000_000_000u128,
565 amount0_min: None,
566 amount1_min: None,
567 };
568 let frag = plan_remove_liquidity(¶ms, u64::MAX, Chain::Sonic).expect("Sonic supported");
569 assert_eq!(frag.calls.len(), 1);
570 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
571 assert!(frag.approvals.is_empty());
572 assert_eq!(frag.value, U256::ZERO);
573 }
574
575 #[test]
576 fn plan_collect_fees_targets_position_manager_no_approvals() {
577 let params = CollectFeesParams {
578 token_id: U256::from(42u64),
579 recipient: address!("0000000000000000000000000000000000000099"),
580 token0: address!("0000000000000000000000000000000000000001"),
581 token1: address!("0000000000000000000000000000000000000002"),
582 caller: Address::ZERO,
583 };
584 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
585 assert_eq!(frag.calls.len(), 1);
586 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
587 assert!(frag.approvals.is_empty());
588 assert_eq!(frag.value, U256::ZERO);
589 }
590
591 #[test]
592 fn plan_claim_cl_gauge_rewards_targets_voter() {
593 let claims = vec![GaugeClaim {
594 gauge: address!("1111111111111111111111111111111111111111"),
595 reward_tokens: vec![address!("aaaa000000000000000000000000000000000000")],
596 token_ids: vec![U256::from(1u64)],
597 }];
598 let frag = plan_claim_cl_gauge_rewards(Chain::Sonic, &claims).expect("Sonic supported");
599 assert_eq!(frag.calls.len(), 1);
600 assert_eq!(frag.calls[0].target, CONFIG.voter);
601 assert!(frag.approvals.is_empty());
602 assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
603 }
604
605 #[test]
606 fn plan_claim_cl_gauge_rewards_rejects_unsupported_chain() {
607 let claims = vec![GaugeClaim {
608 gauge: Address::ZERO,
609 reward_tokens: vec![Address::ZERO],
610 token_ids: vec![U256::from(1u64)],
611 }];
612 assert!(plan_claim_cl_gauge_rewards(Chain::Base, &claims).is_err());
613 }
614
615 #[test]
616 fn plan_collect_fees_native_recipient_emits_multicall_with_unwrap_and_sweep() {
617 let params = CollectFeesParams {
622 token_id: U256::from(1u64),
623 recipient: Address::ZERO,
624 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
627 };
628 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
629
630 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
631 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
632 assert_eq!(frag.value, U256::ZERO);
633 assert_eq!(frag.calls[0].value, U256::ZERO);
634 assert!(
635 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
636 "native collect multicall must include unwrapWETH9(uint256,address) tail"
637 );
638 assert!(
639 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
640 "native collect multicall must include sweepToken(address,uint256,address) tail"
641 );
642 }
643
644 #[test]
645 fn plan_collect_fees_non_native_recipient_passthrough() {
646 let params = CollectFeesParams {
647 token_id: U256::from(1u64),
648 recipient: address!("0000000000000000000000000000000000000099"),
649 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: Address::ZERO,
652 };
653 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
654 let bare = ramses::plan::collect_fees(¶ms, CONFIG.position_mgr);
655
656 assert_ne!(
657 &frag.calls[0].calldata[..4],
658 &[0xac, 0x96, 0x50, 0xd8],
659 "non-native case must NOT be wrapped in multicall"
660 );
661 assert_eq!(
662 frag.calls[0].calldata, bare.calls[0].calldata,
663 "non-native pass-through must stay byte-identical to bare collect()"
664 );
665 }
666
667 #[test]
668 fn plan_collect_fees_no_native_side_rejects() {
669 let params = CollectFeesParams {
672 token_id: U256::from(1u64),
673 recipient: Address::ZERO,
674 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("0000000000000000000000000000000000000002"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
677 };
678 let err = plan_collect_fees(¶ms, Chain::Sonic).unwrap_err();
679 let msg = format!("{err:#}");
680 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
681 }
682
683 fn fixture_remove_and_collect_params_ws_paired() -> RemoveAndCollectParams {
689 RemoveAndCollectParams {
690 token_id: U256::from(1u64),
691 liquidity: 1_000_000u128,
692 amount0_min: Some(U256::from(100u64)),
693 amount1_min: Some(U256::from(200u64)),
694 recipient: Address::ZERO,
695 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
698 burn: false,
699 }
700 }
701
702 #[test]
703 fn plan_remove_liquidity_and_collect_native_recipient_emits_4_call_multicall() {
704 let params = fixture_remove_and_collect_params_ws_paired();
709 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
710 .expect("Sonic supported");
711
712 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
713 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
714 assert_eq!(frag.value, U256::ZERO);
715 assert_eq!(frag.calls[0].value, U256::ZERO);
716 assert!(frag.approvals.is_empty());
717
718 assert!(
721 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
722 "native remove+collect multicall must include unwrapWETH9(uint256,address) tail"
723 );
724 assert!(
726 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
727 "native remove+collect multicall must include sweepToken(address,uint256,address) tail"
728 );
729
730 use alloy_sol_types::SolValue;
732 let (inner,): (Vec<alloy_primitives::Bytes>,) =
733 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
734 .expect("decode outer multicall params");
735 assert_eq!(inner.len(), 4, "expected 4 inner calls");
736 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
737 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
738 assert_eq!(&inner[2][..4], &[0x49, 0x40, 0x4b, 0x7c], "inner[2] = unwrapWETH9");
739 assert_eq!(&inner[3][..4], &[0xdf, 0x2a, 0xb5, 0xbb], "inner[3] = sweepToken");
740 }
741
742 #[test]
743 fn plan_remove_liquidity_and_collect_non_native_recipient_passthrough() {
744 let mut params = fixture_remove_and_collect_params_ws_paired();
748 params.recipient = address!("0000000000000000000000000000000000000099");
749 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
750 .expect("Sonic supported");
751
752 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
754 let bare =
758 ramses::plan::remove_liquidity_and_collect(¶ms, 9_999_999_999, CONFIG.position_mgr);
759 assert_eq!(
760 frag.calls[0].calldata, bare.calls[0].calldata,
761 "non-native pass-through must stay byte-identical to bare multicall([decrease, collect])"
762 );
763 }
764
765 #[test]
766 fn plan_remove_liquidity_and_collect_no_native_side_rejects() {
767 let mut params = fixture_remove_and_collect_params_ws_paired();
770 params.token1 = address!("0000000000000000000000000000000000000002");
771 let err =
772 plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic).unwrap_err();
773 let msg = format!("{err:#}");
774 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
775 }
776
777 #[test]
778 fn config_for_chain_returns_some_for_sonic_only() {
779 assert_eq!(config_for_chain(Chain::Sonic), Some(&CONFIG));
780
781 for unsupported in [
782 Chain::Ethereum,
783 Chain::Arbitrum,
784 Chain::Optimism,
785 Chain::Polygon,
786 Chain::Base,
787 Chain::Bsc,
788 Chain::Avalanche,
789 Chain::Celo,
790 ] {
791 assert!(
792 config_for_chain(unsupported).is_none(),
793 "shadow should not surface {unsupported:?}",
794 );
795 }
796 }
797
798 #[test]
799 fn position_view_and_key_are_re_exported() {
800 let _: fn(
802 alloy_primitives::Address,
803 alloy_primitives::U256,
804 i32,
805 i32,
806 ) -> alloy_primitives::B256 = position_key;
807 let _: std::marker::PhantomData<RamsesPositionView> = std::marker::PhantomData;
808 }
809
810 #[test]
815 fn factory_returns_chain_specific_address_via_layer2() {
816 assert_eq!(factory(Chain::Sonic), Some(CONFIG.factory));
817 for unsupported in [
818 Chain::Ethereum,
819 Chain::Arbitrum,
820 Chain::Optimism,
821 Chain::Polygon,
822 Chain::Base,
823 Chain::Bsc,
824 Chain::Avalanche,
825 Chain::Celo,
826 ] {
827 assert_eq!(factory(unsupported), None);
828 }
829 }
830
831 #[tokio::test]
840 async fn pool_state_routes_to_chain_specific_multicall() {
841 let Some(rpc) = std::env::var("SONIC_RPC_URL").ok() else {
842 eprintln!(
843 "SKIP pool_state_routes_to_chain_specific_multicall: \
844 set SONIC_RPC_URL to a Sonic archive RPC to enable"
845 );
846 return;
847 };
848 let anvil = alloy::node_bindings::Anvil::new().fork(rpc).spawn();
849 let provider = alloy::providers::ProviderBuilder::new().connect_http(anvil.endpoint_url());
850
851 let sonic_pool = address!("324963c267C354c7660Ce8CA3F5f167E05649970");
854 let state = pool_state(&provider, Chain::Sonic, sonic_pool)
855 .await
856 .expect("Sonic pool_state must succeed via Layer 2 chain-aware routing");
857 assert!(state.liquidity > 0, "Real on-chain Shadow pool should have non-zero liquidity");
858 }
859
860 #[test]
861 fn pool_address_with_override_uses_override_not_config_hash() {
862 let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
863 let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
864 let custom_hash = b256!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
865
866 let with_override =
867 pool_address(Chain::Sonic, usdc_e, ws, 50, Some(custom_hash)).expect("Sonic supported");
868 let without_override =
869 pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
870
871 assert_ne!(with_override, without_override);
872 }
873}
874
875#[cfg(test)]
876mod write_struct_reexport_tests {
877 use alloy_primitives::{aliases::I24, Address, U256};
878 use alloy_sol_types::SolCall;
879
880 #[test]
881 fn write_structs_reachable_on_facade() {
882 let mint_params = crate::ShadowMintParams {
884 token0: Address::ZERO,
885 token1: Address::ZERO,
886 tickSpacing: I24::ZERO,
887 tickLower: I24::ZERO,
888 tickUpper: I24::ZERO,
889 amount0Desired: U256::ZERO,
890 amount1Desired: U256::ZERO,
891 amount0Min: U256::ZERO,
892 amount1Min: U256::ZERO,
893 recipient: Address::ZERO,
894 deadline: U256::ZERO,
895 };
896 let _m = crate::mintCall { params: mint_params };
897 let _i = crate::increaseLiquidityCall {
898 params: crate::IncreaseLiquidityParams {
899 tokenId: U256::ZERO,
900 amount0Desired: U256::ZERO,
901 amount1Desired: U256::ZERO,
902 amount0Min: U256::ZERO,
903 amount1Min: U256::ZERO,
904 deadline: U256::ZERO,
905 },
906 };
907 let _d = crate::decreaseLiquidityCall {
908 params: crate::DecreaseLiquidityParams {
909 tokenId: U256::ZERO,
910 liquidity: 0u128,
911 amount0Min: U256::ZERO,
912 amount1Min: U256::ZERO,
913 deadline: U256::ZERO,
914 },
915 };
916 let _c = crate::collectCall {
917 params: crate::CollectParams {
918 tokenId: U256::ZERO,
919 recipient: Address::ZERO,
920 amount0Max: 0u128,
921 amount1Max: 0u128,
922 },
923 };
924 let _b = crate::burnCall { tokenId: U256::ZERO };
925 assert_eq!(crate::burnCall::SELECTOR, [0x42, 0x96, 0x6c, 0x68]);
926 }
927}