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