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(¶ms),
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(), ¶ms),
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(¶ms, 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(¶ms),
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(¶ms),
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(¶ms),
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(¶ms),
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(¶ms);
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}