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;
8use 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::quote::QuoteError;
27
28pub const CONFIG: RamsesProtocolConfig = RamsesProtocolConfig {
29 factory: address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"),
30 pool_deployer: address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"),
31 router: address!("5543c6176feb9b4b179078205d7c29eea2e2d695"),
32 position_mgr: address!("12E66C8F215DdD5d48d150c8f46aD0c6fB0F4406"),
33 init_code_hash: b256!("c701ee63862761c31d620a4a083c61bdc1e81761e6b9c9267fd19afd22e0821d"),
34 tick_spacings: &[1, 5, 10, 50, 100, 200],
35 multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
36 quoter: None,
37 voter: address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D"),
38};
39
40pub fn config_for_chain(chain: Chain) -> Option<&'static RamsesProtocolConfig> {
41 match chain {
42 Chain::Sonic => Some(&CONFIG),
43 Chain::Ethereum
44 | Chain::Arbitrum
45 | Chain::Optimism
46 | Chain::Polygon
47 | Chain::Base
48 | Chain::Bsc
49 | Chain::HyperEvm
50 | Chain::Avalanche
51 | Chain::Celo => None,
52 }
53}
54
55pub fn factory(chain: Chain) -> Option<Address> {
56 config_for_chain(chain).map(|c| c.factory)
57}
58pub fn pool_deployer(chain: Chain) -> Option<Address> {
59 config_for_chain(chain).map(|c| c.pool_deployer)
60}
61pub fn position_manager(chain: Chain) -> Option<Address> {
62 config_for_chain(chain).map(|c| c.position_mgr)
63}
64pub fn router(chain: Chain) -> Option<Address> {
65 config_for_chain(chain).map(|c| c.router)
66}
67pub fn quoter(chain: Chain) -> Option<Address> {
68 config_for_chain(chain).and_then(|c| c.quoter)
69}
70pub fn multicall(chain: Chain) -> Option<Address> {
71 config_for_chain(chain).map(|c| c.multicall)
72}
73pub fn init_code_hash(chain: Chain) -> Option<B256> {
74 config_for_chain(chain).map(|c| c.init_code_hash)
75}
76pub fn voter(chain: Chain) -> Option<Address> {
77 config_for_chain(chain).map(|c| c.voter)
78}
79pub fn supports(chain: Chain) -> bool {
80 config_for_chain(chain).is_some()
81}
82
83pub async fn pool_state<P: Provider<Ethereum>>(
84 provider: &P,
85 chain: Chain,
86 pool: Address,
87) -> Result<PoolState> {
88 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
89 ramses::hydrate::pool_state(provider, cfg.multicall, pool).await
90}
91
92pub async fn position_state<P: Provider<Ethereum>>(
93 provider: &P,
94 chain: Chain,
95 token_id: U256,
96) -> Result<PositionState> {
97 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
98 ramses::hydrate::position_state_shadow(provider, cfg.multicall, cfg.position_mgr, token_id)
99 .await
100}
101
102pub fn quote_exact_in(s: &PoolState, p: &ExactInParams) -> Result<Quote, QuoteError> {
103 ramses::quote::exact_in(s, p)
104}
105pub fn quote_exact_out(s: &PoolState, p: &ExactOutParams) -> Result<Quote, QuoteError> {
106 ramses::quote::exact_out(s, p)
107}
108
109pub async fn populate_ticks<P: Provider<Ethereum>>(
110 provider: &P,
111 chain: Chain,
112 pool: Address,
113 state: &mut PoolState,
114) -> Result<()> {
115 config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
116 ramses::populate_ticks::populate_ticks(provider, pool, state).await
117}
118
119pub async fn quote_online_exact_in<P: Provider<Ethereum>>(
120 provider: &P,
121 chain: Chain,
122 state: &PoolState,
123 params: &ExactInParams,
124) -> Result<Quote> {
125 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
126 let quoter = cfg.quoter.ok_or_else(|| anyhow!("Shadow quoter not registered on {chain:?}"))?;
127 ramses::quote_online::quote_online_exact_in(provider, quoter, state, params).await
128}
129
130pub fn plan_swap_exact_in(
131 s: &PoolState,
132 q: &Quote,
133 p: &ExactInParams,
134 slippage: SlippageBps,
135 deadline: u64,
136 chain: Chain,
137) -> Result<PlanFragment> {
138 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
139 Ok(ramses::plan::swap_exact_in(s, q, p, slippage, deadline, cfg.router))
140}
141
142pub fn plan_add_liquidity(
143 p: &RamsesAddLiquidityParams,
144 slippage: SlippageBps,
145 deadline: u64,
146 chain: Chain,
147) -> Result<PlanFragment> {
148 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
149 Ok(ramses::plan::add_liquidity(p, slippage, deadline, cfg.position_mgr))
150}
151
152#[allow(clippy::too_many_arguments)]
153pub fn plan_increase_liquidity(
154 token_id: U256,
155 token0: Address,
156 token1: Address,
157 amount0_desired: U256,
158 amount1_desired: U256,
159 slippage: SlippageBps,
160 deadline: u64,
161 chain: Chain,
162) -> Result<PlanFragment> {
163 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
164 Ok(ramses::plan::increase_liquidity(
165 token_id,
166 token0,
167 token1,
168 amount0_desired,
169 amount1_desired,
170 slippage,
171 deadline,
172 cfg.position_mgr,
173 ))
174}
175
176pub fn plan_remove_liquidity(
177 p: &RemoveLiquidityParams,
178 deadline: u64,
179 chain: Chain,
180) -> Result<PlanFragment> {
181 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
182 Ok(ramses::plan::remove_liquidity(p, deadline, cfg.position_mgr))
183}
184
185pub fn plan_remove_liquidity_and_collect(
207 p: &RemoveAndCollectParams,
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
213 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_remove_and_collect(
217 p,
218 cfg.position_mgr,
219 chain,
220 )?;
221
222 let mut core_params = (*p).clone();
226 core_params.recipient = wrap.effective_collect_recipient;
227
228 let frag = ramses::plan::remove_liquidity_and_collect(&core_params, deadline, cfg.position_mgr);
229 wp_evm_v3_provider::plan::compose_native_remove_collect_multicall(frag, &wrap, SELECTORS)
230}
231
232pub fn plan_collect_fees(p: &CollectFeesParams, chain: Chain) -> Result<PlanFragment> {
233 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
234
235 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_collect(p, cfg.position_mgr, chain)?;
248
249 let core_params = CollectFeesParams {
254 token_id: p.token_id,
255 recipient: wrap.effective_recipient,
256 token0: p.token0,
257 token1: p.token1,
258 caller: p.caller,
259 };
260
261 let frag = ramses::plan::collect_fees(&core_params, cfg.position_mgr);
262 Ok(wp_evm_v3_provider::plan::compose_native_collect_multicall(frag, &wrap, SELECTORS))
263}
264
265pub fn pool_address(
266 chain: Chain,
267 token_a: Address,
268 token_b: Address,
269 tick_spacing: i32,
270 init_code_hash_override: Option<B256>,
271) -> Option<Address> {
272 let cfg = config_for_chain(chain)?;
273 let init_code_hash = init_code_hash_override.unwrap_or(cfg.init_code_hash);
274 Some(ramses::pool_address::compute(
275 cfg.pool_deployer,
276 init_code_hash,
277 token_a,
278 token_b,
279 tick_spacing,
280 ))
281}
282
283pub async fn pending_emissions<P: Provider<Ethereum>>(
284 provider: &P,
285 chain: Chain,
286 pool: Address,
287 token_id: U256,
288) -> Result<Option<wp_evm_ramses_provider::gauge::PendingEmissions>> {
289 let cfg = config_for_chain(chain).ok_or_else(|| anyhow!("Shadow not deployed on {chain:?}"))?;
290 wp_evm_ramses_provider::gauge::pending_emissions(
291 provider,
292 cfg.multicall,
293 cfg.voter,
294 pool,
295 token_id,
296 )
297 .await
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use wp_evm_ramses_provider::data::TickInfo;
304
305 #[test]
306 fn config_router_is_known_shadow_address() {
307 assert_eq!(CONFIG.router, address!("5543c6176feb9b4b179078205d7c29eea2e2d695"));
308 }
309
310 #[test]
311 fn config_factory_is_known_shadow_address() {
312 assert_eq!(CONFIG.factory, address!("cD2d0637c94fe77C2896BbCBB174cefFb08DE6d7"));
313 }
314
315 #[test]
316 fn config_pool_deployer_matches_factory_ramsesv3pooldeployer_getter() {
317 assert_eq!(CONFIG.pool_deployer, address!("8BBDc15759a8eCf99A92E004E0C64ea9A5142d59"));
321 }
322
323 #[test]
324 fn pool_address_matches_canonical_usdce_ws_ts50_sonic() {
325 let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
326 let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
327 let pool = pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
328 assert_eq!(pool, address!("324963c267C354c7660Ce8CA3F5f167E05649970"));
329 }
330
331 #[test]
332 fn config_tick_spacings_match_shadow() {
333 assert_eq!(CONFIG.tick_spacings, &[1, 5, 10, 50, 100, 200]);
334 }
335
336 #[test]
339 fn plan_add_liquidity_accepts_ramses_params_with_tick_spacing() {
340 let p = RamsesAddLiquidityParams {
341 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
342 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
343 tick_spacing: 50,
344 tick_lower: -887_272,
345 tick_upper: 887_272,
346 amount0_desired: U256::from(1_000_000u64),
347 amount1_desired: U256::from(500_000_000_000_000u64),
348 recipient: address!("0000000000000000000000000000000000000099"),
349 };
350 let frag = plan_add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, Chain::Sonic)
351 .expect("Sonic supported");
352 assert_eq!(frag.calls.len(), 1);
353 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
354 assert_eq!(frag.approvals.len(), 2);
355 assert_eq!(frag.approvals[0].token, p.token0);
356 assert_eq!(frag.approvals[1].token, p.token1);
357 assert_eq!(frag.value, U256::ZERO);
358 }
359
360 fn fixture_usdc_weth() -> PoolState {
361 let sqrt_price_x96 = U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
362 PoolState {
363 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
364 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
365 fee: 3000,
366 tick_spacing: 50,
367 sqrt_price_x96,
368 liquidity: 2_000_000_000_000_000_000_000u128,
369 tick: 76012,
370 ticks: vec![
371 TickInfo {
372 tick: 74950,
373 liquidity_net: 1_000_000_000_000_000_000_000i128,
374 liquidity_gross: 1_000_000_000_000_000_000_000u128,
375 },
376 TickInfo {
377 tick: 75950,
378 liquidity_net: 1_000_000_000_000_000_000_000i128,
379 liquidity_gross: 1_000_000_000_000_000_000_000u128,
380 },
381 TickInfo {
382 tick: 76050,
383 liquidity_net: -2_000_000_000_000_000_000_000i128,
384 liquidity_gross: 2_000_000_000_000_000_000_000u128,
385 },
386 ],
387 }
388 }
389
390 #[test]
391 fn quote_exact_in_delegates_to_ramses_family() {
392 let state = fixture_usdc_weth();
393 let params = ExactInParams {
394 token_in: state.token0,
395 token_out: state.token1,
396 amount_in: U256::from(1_000_000u64),
397 recipient: address!("0000000000000000000000000000000000000099"),
398 };
399 let quote = quote_exact_in(&state, ¶ms).expect("quote should succeed");
400 assert!(quote.amount_out > U256::ZERO);
401 assert_eq!(quote.amount_in, params.amount_in);
402 }
403
404 #[test]
405 fn plan_swap_exact_in_targets_shadow_router() {
406 let state = fixture_usdc_weth();
407 let quote = Quote {
408 amount_in: U256::from(1_000_000u64),
409 amount_out: U256::from(500_000_000_000_000u64),
410 sqrt_price_x96_after: state.sqrt_price_x96,
411 price_impact_bps: 0,
412 };
413 let params = ExactInParams {
414 token_in: state.token0,
415 token_out: state.token1,
416 amount_in: quote.amount_in,
417 recipient: address!("0000000000000000000000000000000000000099"),
418 };
419 let frag = plan_swap_exact_in(
420 &state,
421 "e,
422 ¶ms,
423 SlippageBps::new(50),
424 u64::MAX,
425 Chain::Sonic,
426 )
427 .expect("Sonic supported");
428 assert_eq!(frag.calls.len(), 1);
429 assert_eq!(frag.calls[0].target, CONFIG.router);
430 assert_eq!(frag.approvals.len(), 1);
431 }
432
433 #[test]
434 fn quote_exact_out_delegates_to_ramses_family() {
435 let state = fixture_usdc_weth();
436 let params = ExactOutParams {
437 token_in: state.token0,
438 token_out: state.token1,
439 amount_out: U256::from(500_000_000_000_000u64),
440 recipient: address!("0000000000000000000000000000000000000099"),
441 };
442 let quote = quote_exact_out(&state, ¶ms).expect("exact-out quote should succeed");
443 assert!(quote.amount_in > U256::ZERO);
444 assert_eq!(quote.amount_out, params.amount_out);
445 }
446
447 #[test]
448 fn plan_remove_liquidity_targets_position_manager_no_approvals() {
449 let params = RemoveLiquidityParams {
450 token_id: U256::from(42u64),
451 liquidity: 1_000_000_000_000u128,
452 amount0_min: None,
453 amount1_min: None,
454 };
455 let frag = plan_remove_liquidity(¶ms, u64::MAX, Chain::Sonic).expect("Sonic supported");
456 assert_eq!(frag.calls.len(), 1);
457 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
458 assert!(frag.approvals.is_empty());
459 assert_eq!(frag.value, U256::ZERO);
460 }
461
462 #[test]
463 fn plan_collect_fees_targets_position_manager_no_approvals() {
464 let params = CollectFeesParams {
465 token_id: U256::from(42u64),
466 recipient: address!("0000000000000000000000000000000000000099"),
467 token0: address!("0000000000000000000000000000000000000001"),
468 token1: address!("0000000000000000000000000000000000000002"),
469 caller: Address::ZERO,
470 };
471 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
472 assert_eq!(frag.calls.len(), 1);
473 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
474 assert!(frag.approvals.is_empty());
475 assert_eq!(frag.value, U256::ZERO);
476 }
477
478 #[test]
479 fn plan_collect_fees_native_recipient_emits_multicall_with_unwrap_and_sweep() {
480 let params = CollectFeesParams {
485 token_id: U256::from(1u64),
486 recipient: Address::ZERO,
487 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
490 };
491 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
492
493 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
494 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
495 assert_eq!(frag.value, U256::ZERO);
496 assert_eq!(frag.calls[0].value, U256::ZERO);
497 assert!(
498 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
499 "native collect multicall must include unwrapWETH9(uint256,address) tail"
500 );
501 assert!(
502 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
503 "native collect multicall must include sweepToken(address,uint256,address) tail"
504 );
505 }
506
507 #[test]
508 fn plan_collect_fees_non_native_recipient_passthrough() {
509 let params = CollectFeesParams {
510 token_id: U256::from(1u64),
511 recipient: address!("0000000000000000000000000000000000000099"),
512 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: Address::ZERO,
515 };
516 let frag = plan_collect_fees(¶ms, Chain::Sonic).expect("Sonic supported");
517 let bare = ramses::plan::collect_fees(¶ms, CONFIG.position_mgr);
518
519 assert_ne!(
520 &frag.calls[0].calldata[..4],
521 &[0xac, 0x96, 0x50, 0xd8],
522 "non-native case must NOT be wrapped in multicall"
523 );
524 assert_eq!(
525 frag.calls[0].calldata, bare.calls[0].calldata,
526 "non-native pass-through must stay byte-identical to bare collect()"
527 );
528 }
529
530 #[test]
531 fn plan_collect_fees_no_native_side_rejects() {
532 let params = CollectFeesParams {
535 token_id: U256::from(1u64),
536 recipient: Address::ZERO,
537 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("0000000000000000000000000000000000000002"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
540 };
541 let err = plan_collect_fees(¶ms, Chain::Sonic).unwrap_err();
542 let msg = format!("{err:#}");
543 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
544 }
545
546 fn fixture_remove_and_collect_params_ws_paired() -> RemoveAndCollectParams {
552 RemoveAndCollectParams {
553 token_id: U256::from(1u64),
554 liquidity: 1_000_000u128,
555 amount0_min: Some(U256::from(100u64)),
556 amount1_min: Some(U256::from(200u64)),
557 recipient: Address::ZERO,
558 token0: address!("29219dd400f2bf60e5a23d13be72b486d4038894"), token1: address!("039e2fb66102314ce7b64ce5ce3e5183bc94ad38"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
561 }
562 }
563
564 #[test]
565 fn plan_remove_liquidity_and_collect_native_recipient_emits_4_call_multicall() {
566 let params = fixture_remove_and_collect_params_ws_paired();
571 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
572 .expect("Sonic supported");
573
574 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
575 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
576 assert_eq!(frag.value, U256::ZERO);
577 assert_eq!(frag.calls[0].value, U256::ZERO);
578 assert!(frag.approvals.is_empty());
579
580 assert!(
583 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
584 "native remove+collect multicall must include unwrapWETH9(uint256,address) tail"
585 );
586 assert!(
588 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
589 "native remove+collect multicall must include sweepToken(address,uint256,address) tail"
590 );
591
592 use alloy_sol_types::SolValue;
594 let (inner,): (Vec<alloy_primitives::Bytes>,) =
595 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
596 .expect("decode outer multicall params");
597 assert_eq!(inner.len(), 4, "expected 4 inner calls");
598 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
599 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
600 assert_eq!(&inner[2][..4], &[0x49, 0x40, 0x4b, 0x7c], "inner[2] = unwrapWETH9");
601 assert_eq!(&inner[3][..4], &[0xdf, 0x2a, 0xb5, 0xbb], "inner[3] = sweepToken");
602 }
603
604 #[test]
605 fn plan_remove_liquidity_and_collect_non_native_recipient_passthrough() {
606 let mut params = fixture_remove_and_collect_params_ws_paired();
610 params.recipient = address!("0000000000000000000000000000000000000099");
611 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic)
612 .expect("Sonic supported");
613
614 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
616 let bare =
620 ramses::plan::remove_liquidity_and_collect(¶ms, 9_999_999_999, CONFIG.position_mgr);
621 assert_eq!(
622 frag.calls[0].calldata, bare.calls[0].calldata,
623 "non-native pass-through must stay byte-identical to bare multicall([decrease, collect])"
624 );
625 }
626
627 #[test]
628 fn plan_remove_liquidity_and_collect_no_native_side_rejects() {
629 let mut params = fixture_remove_and_collect_params_ws_paired();
632 params.token1 = address!("0000000000000000000000000000000000000002");
633 let err =
634 plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Sonic).unwrap_err();
635 let msg = format!("{err:#}");
636 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
637 }
638
639 #[test]
640 fn config_for_chain_returns_some_for_sonic_only() {
641 assert_eq!(config_for_chain(Chain::Sonic), Some(&CONFIG));
642
643 for unsupported in [
644 Chain::Ethereum,
645 Chain::Arbitrum,
646 Chain::Optimism,
647 Chain::Polygon,
648 Chain::Base,
649 Chain::Bsc,
650 Chain::Avalanche,
651 Chain::Celo,
652 ] {
653 assert!(
654 config_for_chain(unsupported).is_none(),
655 "shadow should not surface {unsupported:?}",
656 );
657 }
658 }
659
660 #[test]
661 fn position_view_and_key_are_re_exported() {
662 let _: fn(
664 alloy_primitives::Address,
665 alloy_primitives::U256,
666 i32,
667 i32,
668 ) -> alloy_primitives::B256 = position_key;
669 let _: std::marker::PhantomData<RamsesPositionView> = std::marker::PhantomData;
670 }
671
672 #[test]
677 fn factory_returns_chain_specific_address_via_layer2() {
678 assert_eq!(factory(Chain::Sonic), Some(CONFIG.factory));
679 for unsupported in [
680 Chain::Ethereum,
681 Chain::Arbitrum,
682 Chain::Optimism,
683 Chain::Polygon,
684 Chain::Base,
685 Chain::Bsc,
686 Chain::Avalanche,
687 Chain::Celo,
688 ] {
689 assert_eq!(factory(unsupported), None);
690 }
691 }
692
693 #[tokio::test]
702 async fn pool_state_routes_to_chain_specific_multicall() {
703 let Some(rpc) = std::env::var("SONIC_RPC_URL").ok() else {
704 eprintln!(
705 "SKIP pool_state_routes_to_chain_specific_multicall: \
706 set SONIC_RPC_URL to a Sonic archive RPC to enable"
707 );
708 return;
709 };
710 let anvil = alloy::node_bindings::Anvil::new().fork(rpc).spawn();
711 let provider = alloy::providers::ProviderBuilder::new().connect_http(anvil.endpoint_url());
712
713 let sonic_pool = address!("324963c267C354c7660Ce8CA3F5f167E05649970");
716 let state = pool_state(&provider, Chain::Sonic, sonic_pool)
717 .await
718 .expect("Sonic pool_state must succeed via Layer 2 chain-aware routing");
719 assert!(state.liquidity > 0, "Real on-chain Shadow pool should have non-zero liquidity");
720 }
721
722 #[test]
723 fn pool_address_with_override_uses_override_not_config_hash() {
724 let usdc_e = address!("29219dD400f2Bf60E5a23d13Be72B486D4038894");
725 let ws = address!("039e2fb66102314Ce7b64Ce5CE3E5183bc94aD38");
726 let custom_hash = b256!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
727
728 let with_override =
729 pool_address(Chain::Sonic, usdc_e, ws, 50, Some(custom_hash)).expect("Sonic supported");
730 let without_override =
731 pool_address(Chain::Sonic, usdc_e, ws, 50, None).expect("Sonic supported");
732
733 assert_ne!(with_override, without_override);
734 }
735}