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_provider as ramses;
7use wp_evm_ramses_provider::data::RamsesProtocolConfig;
8use wp_evm_v3_provider::plan::PeripherySelectors;
9use wp_evm_velodrome_interfaces::periphery::router::ISlipstreamPeripheryRouter;
10
11const SELECTORS: PeripherySelectors = PeripherySelectors {
15 multicall: ISlipstreamPeripheryRouter::multicallCall::SELECTOR,
16 unwrap_native: ISlipstreamPeripheryRouter::unwrapWETH9Call::SELECTOR,
17 sweep_token: ISlipstreamPeripheryRouter::sweepTokenCall::SELECTOR,
18 refund_native: ISlipstreamPeripheryRouter::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::{
26 position_key, RamsesPositionView, VelodromePositionRow,
27};
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;
34pub use wp_evm_v3_provider::pool_views::{PoolReadEntry, PoolViewData};
35
36pub async fn pool_views<P: Provider<Ethereum>>(
38 provider: &P,
39 pools: &[Address],
40) -> Result<Vec<PoolReadEntry>> {
41 wp_evm_v3_provider::pool_views::pool_views(
42 provider,
43 ramses::MULTICALL3_ADDRESS,
44 pools,
45 &ramses::pool_views::VelodromePoolViewSource,
46 )
47 .await
48}
49
50pub async fn position_token_pair<P: Provider<Ethereum>>(
51 provider: &P,
52 nfpm: Address,
53 token_id: U256,
54) -> Result<(Address, Address)> {
55 ramses::hydrate::velodrome_position_token_pair(provider, nfpm, token_id).await
56}
57
58pub const CONFIG: RamsesProtocolConfig = RamsesProtocolConfig {
59 factory: address!("Cc0bDDB707055e04e497aB22a59c2aF4391cd12F"),
60 pool_deployer: Address::ZERO,
61 router: address!("0792a633F0c19c351081CF4B211F68F79bCc9676"),
62 position_mgr: address!("416b433906b1B72FA758e166e239c43d68dC6F29"),
63 init_code_hash: b256!("339492e30b7a68609e535da9b0773082bfe60230ca47639ee5566007d525f5a7"),
64 tick_spacings: &[1, 50, 100, 200, 2000],
65 multicall: address!("cA11bde05977b3631167028862bE2a173976CA11"),
66 quoter: None,
67 voter: address!("41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C"),
68};
69
70pub fn config_for_chain(chain: Chain) -> Option<&'static RamsesProtocolConfig> {
71 match chain {
72 Chain::Optimism => Some(&CONFIG),
73 Chain::Ethereum
74 | Chain::Arbitrum
75 | Chain::Polygon
76 | Chain::Base
77 | Chain::Bsc
78 | Chain::Sonic
79 | Chain::HyperEvm
80 | Chain::Avalanche
81 | Chain::Celo => None,
82 }
83}
84
85pub fn factory(chain: Chain) -> Option<Address> {
86 config_for_chain(chain).map(|c| c.factory)
87}
88pub fn pool_deployer(chain: Chain) -> Option<Address> {
89 config_for_chain(chain).map(|c| c.pool_deployer)
90}
91pub fn position_manager(chain: Chain) -> Option<Address> {
92 config_for_chain(chain).map(|c| c.position_mgr)
93}
94pub fn router(chain: Chain) -> Option<Address> {
95 config_for_chain(chain).map(|c| c.router)
96}
97pub fn quoter(chain: Chain) -> Option<Address> {
98 config_for_chain(chain).and_then(|c| c.quoter)
99}
100pub fn multicall(chain: Chain) -> Option<Address> {
101 config_for_chain(chain).map(|c| c.multicall)
102}
103pub fn init_code_hash(chain: Chain) -> Option<B256> {
104 config_for_chain(chain).map(|c| c.init_code_hash)
105}
106pub fn voter(chain: Chain) -> Option<Address> {
107 config_for_chain(chain).map(|c| c.voter)
108}
109pub fn supports(chain: Chain) -> bool {
110 config_for_chain(chain).is_some()
111}
112
113pub async fn pool_state<P: Provider<Ethereum>>(
114 provider: &P,
115 chain: Chain,
116 pool: Address,
117) -> Result<PoolState> {
118 let cfg =
119 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
120 ramses::hydrate::pool_state_velodrome(provider, cfg.multicall, pool).await
123}
124
125pub async fn position_state<P: Provider<Ethereum>>(
126 provider: &P,
127 chain: Chain,
128 token_id: U256,
129) -> Result<PositionState> {
130 let cfg =
131 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
132 ramses::hydrate::position_state_slipstream(provider, cfg.multicall, cfg.position_mgr, token_id)
133 .await
134}
135
136pub async fn position_views<P: Provider<Ethereum>>(
137 provider: &P,
138 chain: Chain,
139 token_ids: &[U256],
140) -> Result<Vec<PositionViewEntry<VelodromePositionRow>>> {
141 let cfg =
142 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
143 ramses::position_views::velodrome_position_views(
144 provider,
145 cfg.multicall,
146 cfg.position_mgr,
147 token_ids,
148 )
149 .await
150}
151
152pub async fn position_views_with_nfpm<P: Provider<Ethereum>>(
153 provider: &P,
154 chain: Chain,
155 nfpm: Address,
156 token_ids: &[U256],
157) -> Result<Vec<PositionViewEntry<VelodromePositionRow>>> {
158 let multicall =
159 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
160 ramses::position_views::velodrome_position_views(provider, multicall, nfpm, token_ids).await
161}
162
163pub async fn enumerate_owner_token_ids<P: Provider<Ethereum>>(
164 provider: &P,
165 chain: Chain,
166 nfpm: Address,
167 owner: Address,
168) -> Result<Enumeration> {
169 let multicall =
170 config_for_chain(chain).map(|cfg| cfg.multicall).unwrap_or(ramses::MULTICALL3_ADDRESS);
171 ramses::enumerate_owner_token_ids(provider, multicall, nfpm, owner, chain).await
172}
173
174pub async fn populate_positions_fees<P: Provider<Ethereum>>(
175 provider: &P,
176 chain: Chain,
177 entries: &mut [PositionViewEntry<VelodromePositionRow>],
178) -> Result<()> {
179 let cfg =
180 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
181 ramses::position_views::velodrome_populate_position_fees(
182 provider,
183 cfg.multicall,
184 entries,
185 |v| pool_address(chain, v.token0, v.token1, v.tick_spacing, None),
186 )
187 .await
188}
189
190pub fn quote_exact_in(s: &PoolState, p: &ExactInParams) -> Result<Quote, QuoteError> {
191 ramses::quote::exact_in(s, p)
192}
193pub fn quote_exact_out(s: &PoolState, p: &ExactOutParams) -> Result<Quote, QuoteError> {
194 ramses::quote::exact_out(s, p)
195}
196
197pub async fn populate_ticks<P: Provider<Ethereum>>(
198 provider: &P,
199 chain: Chain,
200 pool: Address,
201 state: &mut PoolState,
202) -> Result<()> {
203 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
204 ramses::populate_ticks::populate_ticks(provider, pool, state).await
205}
206
207pub async fn quote_online_exact_in<P: Provider<Ethereum>>(
208 provider: &P,
209 chain: Chain,
210 state: &PoolState,
211 params: &ExactInParams,
212) -> Result<Quote> {
213 let cfg =
214 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
215 let quoter = cfg
216 .quoter
217 .ok_or_else(|| anyhow!("Velodrome Slipstream quoter not registered on {chain:?}"))?;
218 ramses::quote_online::quote_online_exact_in(provider, quoter, state, params).await
219}
220
221pub fn plan_swap_exact_in(
222 s: &PoolState,
223 q: &Quote,
224 p: &ExactInParams,
225 slippage: SlippageBps,
226 deadline: u64,
227 chain: Chain,
228) -> Result<PlanFragment> {
229 let cfg =
230 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
231 Ok(ramses::plan::swap_exact_in(s, q, p, slippage, deadline, cfg.router))
232}
233
234pub fn plan_add_liquidity(
235 p: &RamsesAddLiquidityParams,
236 slippage: SlippageBps,
237 deadline: u64,
238 chain: Chain,
239) -> Result<PlanFragment> {
240 let cfg =
241 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
242 Ok(ramses::plan::add_liquidity_slipstream(p, slippage, deadline, cfg.position_mgr))
243}
244
245pub fn plan_add_liquidity_with_min(
250 p: &RamsesAddLiquidityParams,
251 amount0_min: U256,
252 amount1_min: U256,
253 deadline: u64,
254 chain: Chain,
255) -> Result<PlanFragment> {
256 let cfg =
257 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
258 Ok(ramses::plan::add_liquidity_slipstream_with_min(
259 p,
260 amount0_min,
261 amount1_min,
262 deadline,
263 cfg.position_mgr,
264 ))
265}
266
267#[allow(clippy::too_many_arguments)]
268pub fn plan_increase_liquidity(
269 token_id: U256,
270 token0: Address,
271 token1: Address,
272 amount0_desired: U256,
273 amount1_desired: U256,
274 slippage: SlippageBps,
275 deadline: u64,
276 chain: Chain,
277) -> Result<PlanFragment> {
278 let cfg =
279 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
280 Ok(ramses::plan::increase_liquidity(
281 token_id,
282 token0,
283 token1,
284 amount0_desired,
285 amount1_desired,
286 slippage,
287 deadline,
288 cfg.position_mgr,
289 ))
290}
291
292pub fn plan_remove_liquidity(
293 p: &RemoveLiquidityParams,
294 deadline: u64,
295 chain: Chain,
296) -> Result<PlanFragment> {
297 let cfg =
298 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
299 Ok(ramses::plan::remove_liquidity(p, deadline, cfg.position_mgr))
300}
301
302pub fn plan_remove_liquidity_and_collect(
319 p: &RemoveAndCollectParams,
320 deadline: u64,
321 chain: Chain,
322) -> Result<PlanFragment> {
323 let cfg =
324 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
325
326 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_remove_and_collect(
327 p,
328 cfg.position_mgr,
329 chain,
330 )?;
331
332 let mut core_params = (*p).clone();
333 core_params.recipient = wrap.effective_collect_recipient;
334
335 let frag = ramses::plan::remove_liquidity_and_collect(&core_params, deadline, cfg.position_mgr);
336 wp_evm_v3_provider::plan::compose_native_remove_collect_multicall(frag, &wrap, SELECTORS)
337}
338
339pub fn plan_collect_fees(p: &CollectFeesParams, chain: Chain) -> Result<PlanFragment> {
340 let cfg =
341 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
342
343 let wrap = wp_evm_v3_provider::plan::resolve_native_wrap_collect(p, cfg.position_mgr, chain)?;
354
355 let core_params = CollectFeesParams {
361 token_id: p.token_id,
362 recipient: wrap.effective_recipient,
363 token0: p.token0,
364 token1: p.token1,
365 caller: p.caller,
366 };
367
368 let frag = ramses::plan::collect_fees(&core_params, cfg.position_mgr);
369 Ok(wp_evm_v3_provider::plan::compose_native_collect_multicall(frag, &wrap, SELECTORS))
370}
371
372pub fn pool_address(
373 chain: Chain,
374 token_a: Address,
375 token_b: Address,
376 tick_spacing: i32,
377 init_code_hash_override: Option<B256>,
378) -> Option<Address> {
379 let cfg = config_for_chain(chain)?;
380 let init_code_hash = init_code_hash_override.unwrap_or(cfg.init_code_hash);
381 Some(ramses::pool_address::compute(cfg.factory, init_code_hash, token_a, token_b, tick_spacing))
382}
383
384pub async fn pending_emissions<P: Provider<Ethereum>>(
385 provider: &P,
386 chain: Chain,
387 pool: Address,
388 account: Address,
389 token_id: U256,
390) -> Result<Option<wp_evm_ramses_provider::velodrome_gauge::VelodromePendingEmissions>> {
391 let cfg =
392 config_for_chain(chain).ok_or_else(|| anyhow!("Velodrome not deployed on {chain:?}"))?;
393 wp_evm_ramses_provider::velodrome_gauge::pending_emissions(
394 provider,
395 cfg.multicall,
396 cfg.voter,
397 pool,
398 account,
399 token_id,
400 )
401 .await
402}
403
404pub fn plan_claim_gauge(gauge: Address, token_id: U256) -> PlanFragment {
405 wp_evm_ramses_provider::plan::claim_gauge(gauge, token_id)
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use wp_evm_ramses_provider::data::TickInfo;
415
416 #[test]
417 fn config_router_is_known_slipstream_address() {
418 assert_eq!(CONFIG.router, address!("0792a633F0c19c351081CF4B211F68F79bCc9676"));
419 }
420
421 #[test]
422 fn config_factory_is_known_slipstream_address() {
423 assert_eq!(CONFIG.factory, address!("Cc0bDDB707055e04e497aB22a59c2aF4391cd12F"));
424 }
425
426 #[test]
427 fn config_tick_spacings_match_cl_factory_constructor() {
428 assert_eq!(CONFIG.tick_spacings, &[1, 50, 100, 200, 2000]);
429 }
430
431 #[test]
432 fn config_for_chain_returns_some_for_optimism_only() {
433 assert_eq!(config_for_chain(Chain::Optimism), Some(&CONFIG));
434 for unsupported in [
435 Chain::Ethereum,
436 Chain::Arbitrum,
437 Chain::Polygon,
438 Chain::Base,
439 Chain::Bsc,
440 Chain::Sonic,
441 Chain::Avalanche,
442 Chain::Celo,
443 ] {
444 assert!(
445 config_for_chain(unsupported).is_none(),
446 "slipstream should not surface {unsupported:?}",
447 );
448 }
449 }
450
451 #[test]
452 fn pool_address_matches_canonical_velodrome_usdc_weth_cl100() {
453 let pool = pool_address(
454 Chain::Optimism,
455 address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"),
456 address!("4200000000000000000000000000000000000006"),
457 100,
458 None,
459 )
460 .expect("Optimism supported");
461 assert_eq!(pool, address!("478946BcD4a5a22b316470F5486fAfb928C0bA25"));
462 }
463
464 #[test]
465 fn pool_address_token_order_independent() {
466 let usdc = address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85");
467 let weth = address!("4200000000000000000000000000000000000006");
468 assert_eq!(
469 pool_address(Chain::Optimism, usdc, weth, 100, None),
470 pool_address(Chain::Optimism, weth, usdc, 100, None)
471 );
472 }
473
474 #[test]
475 fn plan_claim_gauge_targets_gauge_with_get_reward_selector() {
476 let gauge = address!("4444444444444444444444444444444444444444");
477 let token_id = U256::from(42u64);
478 let frag = plan_claim_gauge(gauge, token_id);
479
480 assert_eq!(frag.calls.len(), 1);
481 assert_eq!(frag.calls[0].target, gauge);
482 assert!(frag.approvals.is_empty());
483 assert_eq!(frag.value, U256::ZERO);
484
485 assert_eq!(&frag.calls[0].calldata[..4], &[0x1c, 0x4b, 0x77, 0x4b]);
488 }
489
490 #[test]
493 fn plan_add_liquidity_accepts_ramses_params_with_tick_spacing() {
494 let p = RamsesAddLiquidityParams {
495 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
496 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
497 tick_spacing: 100,
498 tick_lower: -887_200,
499 tick_upper: 887_200,
500 amount0_desired: U256::from(1_000_000u64),
501 amount1_desired: U256::from(500_000_000_000_000u64),
502 recipient: address!("0000000000000000000000000000000000000099"),
503 };
504 let frag = plan_add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, Chain::Optimism)
505 .expect("Optimism supported");
506 assert_eq!(frag.calls.len(), 1);
507 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
508 assert_eq!(frag.approvals.len(), 2);
509 assert_eq!(frag.approvals[0].token, p.token0);
510 assert_eq!(frag.approvals[1].token, p.token1);
511 assert_eq!(frag.value, U256::ZERO);
512 }
513
514 #[test]
519 fn plan_add_liquidity_with_min_threads_precomputed_mins_at_position_manager() {
520 let p = RamsesAddLiquidityParams {
521 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
522 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
523 tick_spacing: 100,
524 tick_lower: -887_200,
525 tick_upper: 887_200,
526 amount0_desired: U256::from(1_000_000u64),
527 amount1_desired: U256::from(500_000_000_000_000u64),
528 recipient: address!("0000000000000000000000000000000000000099"),
529 };
530 let m0 = U256::from(123_456u64);
531 let m1 = U256::from(789_012u64);
532 let frag = plan_add_liquidity_with_min(&p, m0, m1, 9_999_999_999, Chain::Optimism)
533 .expect("Optimism supported");
534 assert_eq!(frag.calls.len(), 1);
535 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
536 assert_eq!(frag.approvals.len(), 2);
537
538 let bare = ramses::plan::add_liquidity_slipstream_with_min(
541 &p,
542 m0,
543 m1,
544 9_999_999_999,
545 CONFIG.position_mgr,
546 );
547 assert_eq!(frag.calls[0].calldata, bare.calls[0].calldata);
548 let other = ramses::plan::add_liquidity_slipstream_with_min(
550 &p,
551 m0 + U256::from(1u64),
552 m1,
553 9_999_999_999,
554 CONFIG.position_mgr,
555 );
556 assert_ne!(frag.calls[0].calldata, other.calls[0].calldata);
557 }
558
559 fn fixture_usdc_weth() -> PoolState {
560 let sqrt_price_x96 = U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
561 PoolState {
562 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
563 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
564 fee: 3000,
565 tick_spacing: 100,
566 sqrt_price_x96,
567 liquidity: 2_000_000_000_000_000_000_000u128,
568 tick: 76012,
569 ticks: vec![
570 TickInfo {
571 tick: 74900,
572 liquidity_net: 1_000_000_000_000_000_000_000i128,
573 liquidity_gross: 1_000_000_000_000_000_000_000u128,
574 },
575 TickInfo {
576 tick: 75900,
577 liquidity_net: 1_000_000_000_000_000_000_000i128,
578 liquidity_gross: 1_000_000_000_000_000_000_000u128,
579 },
580 TickInfo {
581 tick: 76100,
582 liquidity_net: -2_000_000_000_000_000_000_000i128,
583 liquidity_gross: 2_000_000_000_000_000_000_000u128,
584 },
585 ],
586 }
587 }
588
589 #[test]
590 fn quote_exact_in_delegates_to_ramses_family() {
591 let state = fixture_usdc_weth();
592 let params = ExactInParams {
593 token_in: state.token0,
594 token_out: state.token1,
595 amount_in: U256::from(1_000_000u64),
596 recipient: address!("0000000000000000000000000000000000000099"),
597 };
598 let quote = quote_exact_in(&state, ¶ms).expect("quote should succeed");
599 assert!(quote.amount_out > U256::ZERO);
600 assert_eq!(quote.amount_in, params.amount_in);
601 }
602
603 #[test]
604 fn plan_swap_exact_in_targets_slipstream_router() {
605 let state = fixture_usdc_weth();
606 let quote = Quote {
607 amount_in: U256::from(1_000_000u64),
608 amount_out: U256::from(500_000_000_000_000u64),
609 sqrt_price_x96_after: state.sqrt_price_x96,
610 price_impact_bps: 0,
611 };
612 let params = ExactInParams {
613 token_in: state.token0,
614 token_out: state.token1,
615 amount_in: quote.amount_in,
616 recipient: address!("0000000000000000000000000000000000000099"),
617 };
618 let frag = plan_swap_exact_in(
619 &state,
620 "e,
621 ¶ms,
622 SlippageBps::new(50),
623 u64::MAX,
624 Chain::Optimism,
625 )
626 .expect("Optimism supported");
627 assert_eq!(frag.calls.len(), 1);
628 assert_eq!(frag.calls[0].target, CONFIG.router);
629 assert_eq!(frag.approvals.len(), 1);
630 }
631
632 #[test]
633 fn quote_exact_out_delegates_to_ramses_family() {
634 let state = fixture_usdc_weth();
635 let params = ExactOutParams {
636 token_in: state.token0,
637 token_out: state.token1,
638 amount_out: U256::from(500_000_000_000_000u64),
639 recipient: address!("0000000000000000000000000000000000000099"),
640 };
641 let quote = quote_exact_out(&state, ¶ms).expect("exact-out quote should succeed");
642 assert!(quote.amount_in > U256::ZERO);
643 assert_eq!(quote.amount_out, params.amount_out);
644 }
645
646 #[test]
647 fn plan_remove_liquidity_targets_position_manager_no_approvals() {
648 let params = RemoveLiquidityParams {
649 token_id: U256::from(42u64),
650 liquidity: 1_000_000_000_000u128,
651 amount0_min: None,
652 amount1_min: None,
653 };
654 let frag =
655 plan_remove_liquidity(¶ms, u64::MAX, Chain::Optimism).expect("Optimism supported");
656 assert_eq!(frag.calls.len(), 1);
657 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
658 assert!(frag.approvals.is_empty());
659 assert_eq!(frag.value, U256::ZERO);
660 }
661
662 #[test]
663 fn plan_collect_fees_targets_position_manager_no_approvals() {
664 let params = CollectFeesParams {
665 token_id: U256::from(42u64),
666 recipient: address!("0000000000000000000000000000000000000099"),
667 token0: address!("0000000000000000000000000000000000000001"),
668 token1: address!("0000000000000000000000000000000000000002"),
669 caller: Address::ZERO,
670 };
671 let frag = plan_collect_fees(¶ms, Chain::Optimism).expect("Optimism supported");
672 assert_eq!(frag.calls.len(), 1);
673 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
674 assert!(frag.approvals.is_empty());
675 assert_eq!(frag.value, U256::ZERO);
676 }
677
678 #[test]
679 fn plan_collect_fees_native_recipient_emits_multicall_with_unwrap_and_sweep() {
680 let params = CollectFeesParams {
684 token_id: U256::from(1u64),
685 recipient: Address::ZERO,
686 token0: address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), token1: address!("4200000000000000000000000000000000000006"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
689 };
690 let frag = plan_collect_fees(¶ms, Chain::Optimism).expect("Optimism supported");
691
692 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
693 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
694 assert_eq!(frag.value, U256::ZERO);
695 assert_eq!(frag.calls[0].value, U256::ZERO);
696 assert!(
697 frag.calls[0].calldata.windows(4).any(|w| w == [0x49, 0x40, 0x4b, 0x7c]),
698 "native collect multicall must include unwrapWETH9(uint256,address) tail"
699 );
700 assert!(
701 frag.calls[0].calldata.windows(4).any(|w| w == [0xdf, 0x2a, 0xb5, 0xbb]),
702 "native collect multicall must include sweepToken(address,uint256,address) tail"
703 );
704 }
705
706 #[test]
707 fn plan_collect_fees_non_native_recipient_passthrough() {
708 let params = CollectFeesParams {
709 token_id: U256::from(1u64),
710 recipient: address!("0000000000000000000000000000000000000099"),
711 token0: address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"),
712 token1: address!("4200000000000000000000000000000000000006"),
713 caller: Address::ZERO,
714 };
715 let frag = plan_collect_fees(¶ms, Chain::Optimism).expect("Optimism supported");
716 let bare = ramses::plan::collect_fees(¶ms, CONFIG.position_mgr);
717
718 assert_ne!(
719 &frag.calls[0].calldata[..4],
720 &[0xac, 0x96, 0x50, 0xd8],
721 "non-native case must NOT be wrapped in multicall"
722 );
723 assert_eq!(
724 frag.calls[0].calldata, bare.calls[0].calldata,
725 "non-native pass-through must stay byte-identical to bare collect()"
726 );
727 }
728
729 #[test]
730 fn plan_collect_fees_no_native_side_rejects() {
731 let params = CollectFeesParams {
734 token_id: U256::from(1u64),
735 recipient: Address::ZERO,
736 token0: address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), token1: address!("94b008aA00579c1307B0EF2c499aD98a8ce58e58"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
739 };
740 let err = plan_collect_fees(¶ms, Chain::Optimism).unwrap_err();
741 let msg = format!("{err:#}");
742 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
743 }
744
745 fn fixture_remove_and_collect_params_weth_paired() -> RemoveAndCollectParams {
751 RemoveAndCollectParams {
752 token_id: U256::from(1u64),
753 liquidity: 1_000_000u128,
754 amount0_min: Some(U256::from(100u64)),
755 amount1_min: Some(U256::from(200u64)),
756 recipient: Address::ZERO,
757 token0: address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85"), token1: address!("4200000000000000000000000000000000000006"), caller: address!("dEaDbEEFdeAdBeEfDEadBeEFDeaDbEEfdeadbEEF"),
760 burn: false,
761 }
762 }
763
764 #[test]
765 fn plan_remove_liquidity_and_collect_native_recipient_emits_4_call_multicall() {
766 let params = fixture_remove_and_collect_params_weth_paired();
767 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Optimism)
768 .expect("Optimism supported");
769
770 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
771 assert_eq!(frag.calls[0].target, CONFIG.position_mgr);
772 assert_eq!(frag.value, U256::ZERO);
773 assert!(frag.approvals.is_empty());
774
775 use alloy_sol_types::SolValue;
776 let (inner,): (Vec<alloy_primitives::Bytes>,) =
777 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
778 .expect("decode outer multicall params");
779 assert_eq!(inner.len(), 4, "expected 4 inner calls");
780 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
781 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
782 assert_eq!(&inner[2][..4], &[0x49, 0x40, 0x4b, 0x7c], "inner[2] = unwrapWETH9");
783 assert_eq!(&inner[3][..4], &[0xdf, 0x2a, 0xb5, 0xbb], "inner[3] = sweepToken");
784 }
785
786 #[test]
787 fn plan_remove_liquidity_and_collect_non_native_recipient_passthrough() {
788 let mut params = fixture_remove_and_collect_params_weth_paired();
789 params.recipient = address!("0000000000000000000000000000000000000099");
790 let frag = plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Optimism)
791 .expect("Optimism supported");
792
793 assert_eq!(&frag.calls[0].calldata[..4], &[0xac, 0x96, 0x50, 0xd8]);
795 let bare =
796 ramses::plan::remove_liquidity_and_collect(¶ms, 9_999_999_999, CONFIG.position_mgr);
797 assert_eq!(
798 frag.calls[0].calldata, bare.calls[0].calldata,
799 "non-native pass-through must stay byte-identical to bare multicall([decrease, collect])"
800 );
801 }
802
803 #[test]
804 fn plan_remove_liquidity_and_collect_no_native_side_rejects() {
805 let mut params = fixture_remove_and_collect_params_weth_paired();
806 params.token1 = address!("94b008aA00579c1307B0EF2c499aD98a8ce58e58"); let err =
808 plan_remove_liquidity_and_collect(¶ms, 9_999_999_999, Chain::Optimism).unwrap_err();
809 let msg = format!("{err:#}");
810 assert!(msg.contains("neither") || msg.contains("native"), "got: {msg}");
811 }
812
813 #[test]
818 fn factory_returns_chain_specific_address_via_layer2() {
819 assert_eq!(factory(Chain::Optimism), Some(CONFIG.factory));
820 for unsupported in [
821 Chain::Ethereum,
822 Chain::Arbitrum,
823 Chain::Polygon,
824 Chain::Base,
825 Chain::Bsc,
826 Chain::Sonic,
827 Chain::Avalanche,
828 Chain::Celo,
829 ] {
830 assert_eq!(factory(unsupported), None);
831 }
832 }
833
834 #[tokio::test]
839 async fn pool_state_routes_to_chain_specific_multicall() {
840 let Some(rpc) = std::env::var("OPTIMISM_RPC_URL").ok() else {
841 eprintln!(
842 "SKIP pool_state_routes_to_chain_specific_multicall: \
843 set OPTIMISM_RPC_URL to an Optimism archive RPC to enable"
844 );
845 return;
846 };
847 let anvil = alloy::node_bindings::Anvil::new().fork(rpc).spawn();
848 let provider = alloy::providers::ProviderBuilder::new().connect_http(anvil.endpoint_url());
849
850 let optimism_pool = address!("478946BcD4a5a22b316470F5486fAfb928C0bA25");
853 let state = pool_state(&provider, Chain::Optimism, optimism_pool)
854 .await
855 .expect("Optimism pool_state must succeed via Layer 2 chain-aware routing");
856 assert!(
857 state.liquidity > 0,
858 "Real on-chain Velodrome Slipstream pool should have non-zero liquidity"
859 );
860 }
861
862 #[test]
863 fn pool_address_with_override_uses_override_not_config_hash() {
864 let usdc = address!("0b2C639c533813f4Aa9D7837CAf62653d097Ff85");
865 let weth = address!("4200000000000000000000000000000000000006");
866 let custom_hash = b256!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
867
868 let with_override = pool_address(Chain::Optimism, usdc, weth, 100, Some(custom_hash))
869 .expect("Optimism supported");
870 let without_override =
871 pool_address(Chain::Optimism, usdc, weth, 100, None).expect("Optimism supported");
872
873 assert_ne!(with_override, without_override);
874 }
875}