Skip to main content

lichen_client_sdk/
lichenswap.rs

1use crate::client::ReadonlyContractResult;
2use crate::{Client, Error, Keypair, Pubkey, Result};
3use serde::{Deserialize, Serialize};
4use std::sync::{Arc, Mutex};
5
6const PROGRAM_SYMBOL_CANDIDATES: [&str; 2] = ["LICHENSWAP", "lichenswap"];
7const POOL_INFO_SIZE: usize = 24;
8const TWAP_CUMULATIVES_SIZE: usize = 24;
9const VOLUME_TOTALS_SIZE: usize = 16;
10const SWAP_STATS_SIZE: usize = 40;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct LichenSwapPoolInfo {
14    pub reserve_a: u64,
15    pub reserve_b: u64,
16    pub total_liquidity: u64,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub struct LichenSwapVolumeTotals {
21    pub volume_a: u64,
22    pub volume_b: u64,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct LichenSwapProtocolFees {
27    pub fees_a: u64,
28    pub fees_b: u64,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub struct LichenSwapTwapCumulatives {
33    pub cumulative_price_a: u64,
34    pub cumulative_price_b: u64,
35    pub last_updated_at: u64,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct LichenSwapSwapStats {
40    pub swap_count: u64,
41    pub volume_a: u64,
42    pub volume_b: u64,
43    pub pool_count: u64,
44    pub total_liquidity: u64,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub struct LichenSwapStats {
49    pub swap_count: u64,
50    pub volume_a: u64,
51    pub volume_b: u64,
52    pub paused: bool,
53}
54
55#[derive(Debug, Clone)]
56pub struct CreatePoolParams {
57    pub token_a: Pubkey,
58    pub token_b: Pubkey,
59}
60
61#[derive(Debug, Clone)]
62pub struct AddLiquidityParams {
63    pub amount_a: u64,
64    pub amount_b: u64,
65    pub min_liquidity: u64,
66    pub value_spores: Option<u64>,
67}
68
69#[derive(Debug, Clone)]
70pub struct SwapParams {
71    pub amount_in: u64,
72    pub min_amount_out: u64,
73    pub value_spores: Option<u64>,
74}
75
76#[derive(Debug, Clone)]
77pub struct SwapWithDeadlineParams {
78    pub amount_in: u64,
79    pub min_amount_out: u64,
80    pub deadline: u64,
81    pub value_spores: Option<u64>,
82}
83
84#[derive(Debug, Clone)]
85pub struct LichenSwapClient {
86    client: Client,
87    program_id: Arc<Mutex<Option<Pubkey>>>,
88}
89
90fn build_layout_args(layout: &[u8], chunks: &[Vec<u8>]) -> Vec<u8> {
91    let mut out = Vec::with_capacity(
92        1 + layout.len() + chunks.iter().map(|chunk| chunk.len()).sum::<usize>(),
93    );
94    out.push(0xAB);
95    out.extend_from_slice(layout);
96    for chunk in chunks {
97        out.extend_from_slice(chunk);
98    }
99    out
100}
101
102fn encode_create_pool_args(params: &CreatePoolParams) -> Vec<u8> {
103    build_layout_args(
104        &[0x20, 0x20],
105        &[
106            params.token_a.as_ref().to_vec(),
107            params.token_b.as_ref().to_vec(),
108        ],
109    )
110}
111
112fn encode_add_liquidity_args(provider: &Pubkey, params: &AddLiquidityParams) -> Vec<u8> {
113    build_layout_args(
114        &[0x20, 0x08, 0x08, 0x08],
115        &[
116            provider.as_ref().to_vec(),
117            params.amount_a.to_le_bytes().to_vec(),
118            params.amount_b.to_le_bytes().to_vec(),
119            params.min_liquidity.to_le_bytes().to_vec(),
120        ],
121    )
122}
123
124fn encode_swap_args(params: &SwapParams, a_to_b: bool) -> Vec<u8> {
125    build_layout_args(
126        &[0x08, 0x08, 0x04],
127        &[
128            params.amount_in.to_le_bytes().to_vec(),
129            params.min_amount_out.to_le_bytes().to_vec(),
130            (u32::from(a_to_b)).to_le_bytes().to_vec(),
131        ],
132    )
133}
134
135fn encode_directional_swap_args(params: &SwapParams) -> Vec<u8> {
136    build_layout_args(
137        &[0x08, 0x08],
138        &[
139            params.amount_in.to_le_bytes().to_vec(),
140            params.min_amount_out.to_le_bytes().to_vec(),
141        ],
142    )
143}
144
145fn encode_directional_swap_with_deadline_args(params: &SwapWithDeadlineParams) -> Vec<u8> {
146    build_layout_args(
147        &[0x08, 0x08, 0x08],
148        &[
149            params.amount_in.to_le_bytes().to_vec(),
150            params.min_amount_out.to_le_bytes().to_vec(),
151            params.deadline.to_le_bytes().to_vec(),
152        ],
153    )
154}
155
156fn encode_quote_args(amount_in: u64, a_to_b: bool) -> Vec<u8> {
157    build_layout_args(
158        &[0x08, 0x04],
159        &[
160            amount_in.to_le_bytes().to_vec(),
161            (u32::from(a_to_b)).to_le_bytes().to_vec(),
162        ],
163    )
164}
165
166fn encode_provider_args(provider: &Pubkey) -> Vec<u8> {
167    build_layout_args(&[0x20], &[provider.as_ref().to_vec()])
168}
169
170fn encode_amount_args(amount: u64) -> Vec<u8> {
171    build_layout_args(&[0x08], &[amount.to_le_bytes().to_vec()])
172}
173
174fn ensure_readonly_success(
175    result: &ReadonlyContractResult,
176    function_name: &str,
177    allowed_codes: &[u32],
178) -> Result<()> {
179    let code = result.return_code.unwrap_or(0);
180    if !allowed_codes.contains(&code) {
181        return Err(Error::RpcError(result.error.clone().unwrap_or_else(|| {
182            format!("LichenSwap {} returned code {}", function_name, code)
183        })));
184    }
185    if !result.success {
186        return Err(Error::RpcError(
187            result
188                .error
189                .clone()
190                .unwrap_or_else(|| format!("LichenSwap {} failed", function_name)),
191        ));
192    }
193    Ok(())
194}
195
196fn decode_return_data(result: &ReadonlyContractResult, function_name: &str) -> Result<Vec<u8>> {
197    let Some(return_data) = &result.return_data else {
198        return Err(Error::ParseError(format!(
199            "LichenSwap {} did not return payload data",
200            function_name,
201        )));
202    };
203
204    base64::Engine::decode(&base64::engine::general_purpose::STANDARD, return_data)
205        .map_err(|err| Error::ParseError(err.to_string()))
206}
207
208fn decode_u64(bytes: &[u8], start: usize, function_name: &str) -> Result<u64> {
209    let end = start + 8;
210    if bytes.len() < end {
211        return Err(Error::ParseError(format!(
212            "LichenSwap {} payload was shorter than expected",
213            function_name,
214        )));
215    }
216    let slice: [u8; 8] = bytes[start..end].try_into().map_err(|_| {
217        Error::ParseError(format!(
218            "LichenSwap {} payload was malformed",
219            function_name
220        ))
221    })?;
222    Ok(u64::from_le_bytes(slice))
223}
224
225fn decode_u64_result(result: &ReadonlyContractResult, function_name: &str) -> Result<u64> {
226    ensure_readonly_success(result, function_name, &[0])?;
227    let bytes = decode_return_data(result, function_name)?;
228    decode_u64(&bytes, 0, function_name)
229}
230
231fn decode_pool_info(result: &ReadonlyContractResult) -> Result<LichenSwapPoolInfo> {
232    ensure_readonly_success(result, "get_pool_info", &[0, 1])?;
233    let bytes = decode_return_data(result, "get_pool_info")?;
234    if bytes.len() < POOL_INFO_SIZE {
235        return Err(Error::ParseError(
236            "LichenSwap get_pool_info payload was shorter than expected".into(),
237        ));
238    }
239
240    Ok(LichenSwapPoolInfo {
241        reserve_a: decode_u64(&bytes, 0, "get_pool_info")?,
242        reserve_b: decode_u64(&bytes, 8, "get_pool_info")?,
243        total_liquidity: decode_u64(&bytes, 16, "get_pool_info")?,
244    })
245}
246
247fn decode_twap_cumulatives(result: &ReadonlyContractResult) -> Result<LichenSwapTwapCumulatives> {
248    ensure_readonly_success(result, "get_twap_cumulatives", &[0])?;
249    let bytes = decode_return_data(result, "get_twap_cumulatives")?;
250    if bytes.len() < TWAP_CUMULATIVES_SIZE {
251        return Err(Error::ParseError(
252            "LichenSwap get_twap_cumulatives payload was shorter than expected".into(),
253        ));
254    }
255
256    Ok(LichenSwapTwapCumulatives {
257        cumulative_price_a: decode_u64(&bytes, 0, "get_twap_cumulatives")?,
258        cumulative_price_b: decode_u64(&bytes, 8, "get_twap_cumulatives")?,
259        last_updated_at: decode_u64(&bytes, 16, "get_twap_cumulatives")?,
260    })
261}
262
263fn decode_protocol_fees(result: &ReadonlyContractResult) -> Result<LichenSwapProtocolFees> {
264    ensure_readonly_success(result, "get_protocol_fees", &[0])?;
265    let bytes = decode_return_data(result, "get_protocol_fees")?;
266    if bytes.len() < VOLUME_TOTALS_SIZE {
267        return Err(Error::ParseError(
268            "LichenSwap get_protocol_fees payload was shorter than expected".into(),
269        ));
270    }
271
272    Ok(LichenSwapProtocolFees {
273        fees_a: decode_u64(&bytes, 0, "get_protocol_fees")?,
274        fees_b: decode_u64(&bytes, 8, "get_protocol_fees")?,
275    })
276}
277
278fn decode_volume_totals(
279    result: &ReadonlyContractResult,
280    function_name: &str,
281) -> Result<LichenSwapVolumeTotals> {
282    ensure_readonly_success(result, function_name, &[0])?;
283    let bytes = decode_return_data(result, function_name)?;
284    if bytes.len() < VOLUME_TOTALS_SIZE {
285        return Err(Error::ParseError(format!(
286            "LichenSwap {} payload was shorter than expected",
287            function_name,
288        )));
289    }
290
291    Ok(LichenSwapVolumeTotals {
292        volume_a: decode_u64(&bytes, 0, function_name)?,
293        volume_b: decode_u64(&bytes, 8, function_name)?,
294    })
295}
296
297fn decode_swap_stats(result: &ReadonlyContractResult) -> Result<LichenSwapSwapStats> {
298    ensure_readonly_success(result, "get_swap_stats", &[0])?;
299    let bytes = decode_return_data(result, "get_swap_stats")?;
300    if bytes.len() < SWAP_STATS_SIZE {
301        return Err(Error::ParseError(
302            "LichenSwap get_swap_stats payload was shorter than expected".into(),
303        ));
304    }
305
306    Ok(LichenSwapSwapStats {
307        swap_count: decode_u64(&bytes, 0, "get_swap_stats")?,
308        volume_a: decode_u64(&bytes, 8, "get_swap_stats")?,
309        volume_b: decode_u64(&bytes, 16, "get_swap_stats")?,
310        pool_count: decode_u64(&bytes, 24, "get_swap_stats")?,
311        total_liquidity: decode_u64(&bytes, 32, "get_swap_stats")?,
312    })
313}
314
315impl LichenSwapClient {
316    pub fn new(client: Client) -> Self {
317        Self {
318            client,
319            program_id: Arc::new(Mutex::new(None)),
320        }
321    }
322
323    pub fn with_program_id(client: Client, program_id: Pubkey) -> Self {
324        Self {
325            client,
326            program_id: Arc::new(Mutex::new(Some(program_id))),
327        }
328    }
329
330    pub async fn get_program_id(&self) -> Result<Pubkey> {
331        if let Some(program_id) = self
332            .program_id
333            .lock()
334            .map_err(|_| Error::ConfigError("LichenSwapClient program cache lock poisoned".into()))?
335            .clone()
336        {
337            return Ok(program_id);
338        }
339
340        for symbol in PROGRAM_SYMBOL_CANDIDATES {
341            let entry = match self.client.get_symbol_registry(symbol).await {
342                Ok(entry) => entry,
343                Err(_) => continue,
344            };
345            let Some(program) = entry.get("program").and_then(|value| value.as_str()) else {
346                continue;
347            };
348            let program_id = Pubkey::from_base58(program).map_err(Error::ParseError)?;
349            *self.program_id.lock().map_err(|_| {
350                Error::ConfigError("LichenSwapClient program cache lock poisoned".into())
351            })? = Some(program_id);
352            return Ok(program_id);
353        }
354
355        Err(Error::ConfigError(
356            "Unable to resolve the LichenSwap program via getSymbolRegistry(\"LICHENSWAP\")".into(),
357        ))
358    }
359
360    pub async fn get_pool_info(&self) -> Result<LichenSwapPoolInfo> {
361        let result = self
362            .client
363            .call_readonly_contract(
364                &self.get_program_id().await?,
365                "get_pool_info",
366                Vec::new(),
367                None,
368            )
369            .await?;
370        decode_pool_info(&result)
371    }
372
373    pub async fn get_quote(&self, amount_in: u64, a_to_b: bool) -> Result<u64> {
374        let result = self
375            .client
376            .call_readonly_contract(
377                &self.get_program_id().await?,
378                "get_quote",
379                encode_quote_args(amount_in, a_to_b),
380                None,
381            )
382            .await?;
383        decode_u64_result(&result, "get_quote")
384    }
385
386    pub async fn get_liquidity_balance(&self, provider: &Pubkey) -> Result<u64> {
387        let result = self
388            .client
389            .call_readonly_contract(
390                &self.get_program_id().await?,
391                "get_liquidity_balance",
392                encode_provider_args(provider),
393                None,
394            )
395            .await?;
396        decode_u64_result(&result, "get_liquidity_balance")
397    }
398
399    pub async fn get_total_liquidity(&self) -> Result<u64> {
400        let result = self
401            .client
402            .call_readonly_contract(
403                &self.get_program_id().await?,
404                "get_total_liquidity",
405                Vec::new(),
406                None,
407            )
408            .await?;
409        decode_u64_result(&result, "get_total_liquidity")
410    }
411
412    pub async fn get_flash_loan_fee(&self, amount: u64) -> Result<u64> {
413        let result = self
414            .client
415            .call_readonly_contract(
416                &self.get_program_id().await?,
417                "get_flash_loan_fee",
418                encode_amount_args(amount),
419                None,
420            )
421            .await?;
422        decode_u64_result(&result, "get_flash_loan_fee")
423    }
424
425    pub async fn get_twap_cumulatives(&self) -> Result<LichenSwapTwapCumulatives> {
426        let result = self
427            .client
428            .call_readonly_contract(
429                &self.get_program_id().await?,
430                "get_twap_cumulatives",
431                Vec::new(),
432                None,
433            )
434            .await?;
435        decode_twap_cumulatives(&result)
436    }
437
438    pub async fn get_twap_snapshot_count(&self) -> Result<u64> {
439        let result = self
440            .client
441            .call_readonly_contract(
442                &self.get_program_id().await?,
443                "get_twap_snapshot_count",
444                Vec::new(),
445                None,
446            )
447            .await?;
448        decode_u64_result(&result, "get_twap_snapshot_count")
449    }
450
451    pub async fn get_protocol_fees(&self) -> Result<LichenSwapProtocolFees> {
452        let result = self
453            .client
454            .call_readonly_contract(
455                &self.get_program_id().await?,
456                "get_protocol_fees",
457                Vec::new(),
458                None,
459            )
460            .await?;
461        decode_protocol_fees(&result)
462    }
463
464    pub async fn get_pool_count(&self) -> Result<u64> {
465        let result = self
466            .client
467            .call_readonly_contract(
468                &self.get_program_id().await?,
469                "get_pool_count",
470                Vec::new(),
471                None,
472            )
473            .await?;
474        decode_u64_result(&result, "get_pool_count")
475    }
476
477    pub async fn get_swap_count(&self) -> Result<u64> {
478        let result = self
479            .client
480            .call_readonly_contract(
481                &self.get_program_id().await?,
482                "get_swap_count",
483                Vec::new(),
484                None,
485            )
486            .await?;
487        decode_u64_result(&result, "get_swap_count")
488    }
489
490    pub async fn get_total_volume(&self) -> Result<LichenSwapVolumeTotals> {
491        let result = self
492            .client
493            .call_readonly_contract(
494                &self.get_program_id().await?,
495                "get_total_volume",
496                Vec::new(),
497                None,
498            )
499            .await?;
500        decode_volume_totals(&result, "get_total_volume")
501    }
502
503    pub async fn get_swap_stats(&self) -> Result<LichenSwapSwapStats> {
504        let result = self
505            .client
506            .call_readonly_contract(
507                &self.get_program_id().await?,
508                "get_swap_stats",
509                Vec::new(),
510                None,
511            )
512            .await?;
513        decode_swap_stats(&result)
514    }
515
516    pub async fn get_stats(&self) -> Result<LichenSwapStats> {
517        let value = self.client.get_lichenswap_stats().await?;
518        serde_json::from_value(value).map_err(|err| Error::ParseError(err.to_string()))
519    }
520
521    pub async fn create_pool(&self, owner: &Keypair, params: CreatePoolParams) -> Result<String> {
522        let program_id = self.get_program_id().await?;
523        self.client
524            .call_contract(
525                owner,
526                &program_id,
527                "create_pool",
528                encode_create_pool_args(&params),
529                0,
530            )
531            .await
532    }
533
534    pub async fn add_liquidity(
535        &self,
536        provider: &Keypair,
537        params: AddLiquidityParams,
538    ) -> Result<String> {
539        let program_id = self.get_program_id().await?;
540        let value = match params.value_spores {
541            Some(value) => value,
542            None => params
543                .amount_a
544                .checked_add(params.amount_b)
545                .ok_or_else(|| {
546                    Error::BuildError(
547                        "LichenSwap add_liquidity default value overflowed u64".into(),
548                    )
549                })?,
550        };
551        self.client
552            .call_contract(
553                provider,
554                &program_id,
555                "add_liquidity",
556                encode_add_liquidity_args(&provider.pubkey(), &params),
557                value,
558            )
559            .await
560    }
561
562    pub async fn swap(&self, trader: &Keypair, params: SwapParams, a_to_b: bool) -> Result<String> {
563        let program_id = self.get_program_id().await?;
564        let value = params.value_spores.unwrap_or(params.amount_in);
565        self.client
566            .call_contract(
567                trader,
568                &program_id,
569                "swap",
570                encode_swap_args(&params, a_to_b),
571                value,
572            )
573            .await
574    }
575
576    pub async fn swap_a_for_b(&self, trader: &Keypair, params: SwapParams) -> Result<String> {
577        let program_id = self.get_program_id().await?;
578        let value = params.value_spores.unwrap_or(params.amount_in);
579        self.client
580            .call_contract(
581                trader,
582                &program_id,
583                "swap_a_for_b",
584                encode_directional_swap_args(&params),
585                value,
586            )
587            .await
588    }
589
590    pub async fn swap_b_for_a(&self, trader: &Keypair, params: SwapParams) -> Result<String> {
591        let program_id = self.get_program_id().await?;
592        let value = params.value_spores.unwrap_or(params.amount_in);
593        self.client
594            .call_contract(
595                trader,
596                &program_id,
597                "swap_b_for_a",
598                encode_directional_swap_args(&params),
599                value,
600            )
601            .await
602    }
603
604    pub async fn swap_a_for_b_with_deadline(
605        &self,
606        trader: &Keypair,
607        params: SwapWithDeadlineParams,
608    ) -> Result<String> {
609        let program_id = self.get_program_id().await?;
610        let value = params.value_spores.unwrap_or(params.amount_in);
611        self.client
612            .call_contract(
613                trader,
614                &program_id,
615                "swap_a_for_b_with_deadline",
616                encode_directional_swap_with_deadline_args(&params),
617                value,
618            )
619            .await
620    }
621
622    pub async fn swap_b_for_a_with_deadline(
623        &self,
624        trader: &Keypair,
625        params: SwapWithDeadlineParams,
626    ) -> Result<String> {
627        let program_id = self.get_program_id().await?;
628        let value = params.value_spores.unwrap_or(params.amount_in);
629        self.client
630            .call_contract(
631                trader,
632                &program_id,
633                "swap_b_for_a_with_deadline",
634                encode_directional_swap_with_deadline_args(&params),
635                value,
636            )
637            .await
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    fn readonly_result(return_code: u32, bytes: Vec<u8>) -> ReadonlyContractResult {
646        ReadonlyContractResult {
647            success: true,
648            return_data: Some(base64::Engine::encode(
649                &base64::engine::general_purpose::STANDARD,
650                bytes,
651            )),
652            return_code: Some(return_code),
653            logs: Vec::new(),
654            error: None,
655            compute_used: None,
656        }
657    }
658
659    #[test]
660    fn create_pool_encoding_matches_named_export_layout() {
661        let params = CreatePoolParams {
662            token_a: Pubkey([1u8; 32]),
663            token_b: Pubkey([2u8; 32]),
664        };
665
666        let encoded = encode_create_pool_args(&params);
667
668        assert_eq!(&encoded[..3], &[0xAB, 0x20, 0x20]);
669        assert_eq!(&encoded[3..35], &[1u8; 32]);
670        assert_eq!(&encoded[35..67], &[2u8; 32]);
671    }
672
673    #[test]
674    fn add_liquidity_encoding_includes_provider_and_three_u64_values() {
675        let provider = Pubkey([3u8; 32]);
676        let encoded = encode_add_liquidity_args(
677            &provider,
678            &AddLiquidityParams {
679                amount_a: 50,
680                amount_b: 75,
681                min_liquidity: 10,
682                value_spores: None,
683            },
684        );
685
686        assert_eq!(&encoded[..5], &[0xAB, 0x20, 0x08, 0x08, 0x08]);
687        assert_eq!(&encoded[5..37], &[3u8; 32]);
688        assert_eq!(u64::from_le_bytes(encoded[37..45].try_into().unwrap()), 50);
689        assert_eq!(u64::from_le_bytes(encoded[45..53].try_into().unwrap()), 75);
690        assert_eq!(u64::from_le_bytes(encoded[53..61].try_into().unwrap()), 10);
691    }
692
693    #[test]
694    fn swap_encoding_includes_direction_flag() {
695        let encoded = encode_swap_args(
696            &SwapParams {
697                amount_in: 40,
698                min_amount_out: 35,
699                value_spores: None,
700            },
701            false,
702        );
703
704        assert_eq!(&encoded[..4], &[0xAB, 0x08, 0x08, 0x04]);
705        assert_eq!(u64::from_le_bytes(encoded[4..12].try_into().unwrap()), 40);
706        assert_eq!(u64::from_le_bytes(encoded[12..20].try_into().unwrap()), 35);
707        assert_eq!(u32::from_le_bytes(encoded[20..24].try_into().unwrap()), 0);
708    }
709
710    #[test]
711    fn pool_info_decoding_allows_success_code_one() {
712        let result = readonly_result(
713            1,
714            [
715                1_000u64.to_le_bytes().as_slice(),
716                2_000u64.to_le_bytes().as_slice(),
717                3_000u64.to_le_bytes().as_slice(),
718            ]
719            .concat(),
720        );
721
722        let pool = decode_pool_info(&result).unwrap();
723
724        assert_eq!(
725            pool,
726            LichenSwapPoolInfo {
727                reserve_a: 1_000,
728                reserve_b: 2_000,
729                total_liquidity: 3_000,
730            }
731        );
732    }
733
734    #[test]
735    fn swap_stats_decoding_matches_contract_payload_layout() {
736        let result = readonly_result(
737            0,
738            [
739                9u64.to_le_bytes().as_slice(),
740                100u64.to_le_bytes().as_slice(),
741                200u64.to_le_bytes().as_slice(),
742                2u64.to_le_bytes().as_slice(),
743                3_000u64.to_le_bytes().as_slice(),
744            ]
745            .concat(),
746        );
747
748        let stats = decode_swap_stats(&result).unwrap();
749
750        assert_eq!(
751            stats,
752            LichenSwapSwapStats {
753                swap_count: 9,
754                volume_a: 100,
755                volume_b: 200,
756                pool_count: 2,
757                total_liquidity: 3_000,
758            }
759        );
760    }
761}