abstract_os/objects/
lp_token.rs

1use crate::{
2    constants::{ASSET_DELIMITER, TYPE_DELIMITER},
3    objects::{AssetEntry, PoolMetadata},
4};
5use cosmwasm_std::StdError;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::fmt::Display;
9
10pub type DexName = String;
11
12/// A key for the token that represents Liquidity Pool shares on a dex
13/// @todo: move into dex package
14#[derive(
15    Deserialize, Serialize, Clone, Debug, PartialEq, Eq, JsonSchema, PartialOrd, Ord, Default,
16)]
17pub struct LpToken {
18    pub dex: DexName,
19    pub assets: Vec<AssetEntry>,
20}
21
22impl LpToken {
23    pub fn new<T: ToString, U: Into<AssetEntry> + Clone>(dex_name: T, assets: Vec<U>) -> Self {
24        Self {
25            dex: dex_name.to_string(),
26            assets: assets.into_iter().map(|a| Into::into(a)).collect(),
27        }
28    }
29}
30
31/// Try from an asset entry that should be formatted as "dex_name/asset1,asset2"
32impl TryFrom<AssetEntry> for LpToken {
33    type Error = StdError;
34
35    fn try_from(asset: AssetEntry) -> Result<Self, Self::Error> {
36        let segments = asset.as_str().split(TYPE_DELIMITER).collect::<Vec<_>>();
37
38        if segments.len() != 2 {
39            return Err(StdError::generic_err(format!(
40                "Invalid asset entry: {asset}"
41            )));
42        }
43
44        // get the dex name, like "junoswap"
45        let dex_name = segments[0].to_string();
46
47        // get the assets, like "crab,junox" and split them
48        let assets: Vec<AssetEntry> = segments[1]
49            .split(ASSET_DELIMITER)
50            .map(AssetEntry::from)
51            .collect();
52
53        if assets.len() < 2 {
54            return Err(StdError::generic_err(format!(
55                "Must be at least 2 assets in an LP token: {asset}"
56            )));
57        }
58
59        Ok(Self {
60            dex: dex_name,
61            assets,
62        })
63    }
64}
65
66/// Transform into a string formatted as "dex_name/asset1,asset2"
67impl Display for LpToken {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        let assets = self
70            .assets
71            .iter()
72            .map(|a| a.as_str())
73            .collect::<Vec<&str>>()
74            .join(ASSET_DELIMITER);
75
76        write!(f, "{}{}{}", self.dex, TYPE_DELIMITER, assets)
77    }
78}
79
80impl From<LpToken> for AssetEntry {
81    fn from(lp_token: LpToken) -> Self {
82        AssetEntry::from(lp_token.to_string())
83    }
84}
85
86/// Build the LP token from pool metadata.
87impl From<PoolMetadata> for LpToken {
88    fn from(pool: PoolMetadata) -> Self {
89        Self {
90            dex: pool.dex,
91            assets: pool.assets,
92        }
93    }
94}
95
96#[cfg(test)]
97mod test {
98    use super::*;
99    use speculoos::prelude::*;
100
101    mod implementation {
102        use super::*;
103
104        #[test]
105        fn new_works() {
106            let dex_name = "junoswap";
107            let assets = vec![AssetEntry::from("crab"), AssetEntry::from("junox")];
108            let actual = LpToken::new(dex_name, assets.clone());
109
110            let expected = LpToken {
111                dex: dex_name.to_string(),
112                assets,
113            };
114            assert_that!(actual).is_equal_to(expected);
115        }
116
117        #[test]
118        fn assets_returns_asset_entries() {
119            let dex_name = "junoswap";
120            let assets = vec![AssetEntry::from("crab"), AssetEntry::from("junox")];
121            let lp_token = LpToken::new(dex_name, assets);
122            let expected = vec![AssetEntry::from("crab"), AssetEntry::from("junox")];
123
124            assert_that!(lp_token.assets).is_equal_to(expected);
125        }
126    }
127
128    mod from_asset_entry {
129        use super::*;
130
131        #[test]
132        fn test_from_asset_entry() {
133            let lp_token = LpToken::try_from(AssetEntry::new("junoswap/crab,junox")).unwrap();
134            assert_that!(lp_token.dex).is_equal_to("junoswap".to_string());
135            assert_that!(lp_token.assets)
136                .is_equal_to(vec![AssetEntry::from("crab"), AssetEntry::from("junox")]);
137        }
138
139        #[test]
140        fn test_from_invalid_asset_entry() {
141            let lp_token = LpToken::try_from(AssetEntry::new("junoswap/"));
142            assert_that!(&lp_token).is_err();
143        }
144
145        #[test]
146        fn test_fewer_than_two_assets() {
147            let lp_token = LpToken::try_from(AssetEntry::new("junoswap/crab"));
148            assert_that!(&lp_token).is_err();
149        }
150    }
151
152    mod into_asset_entry {
153        use super::*;
154
155        #[test]
156        fn into_asset_entry_works() {
157            let lp_token = LpToken::new("junoswap", vec!["crab".to_string(), "junox".to_string()]);
158            let expected = AssetEntry::new("junoswap/crab,junox");
159
160            assert_that!(lp_token.into()).is_equal_to(expected);
161        }
162    }
163
164    mod from_pool_metadata {
165        use super::*;
166        use crate::objects::PoolType;
167
168        #[test]
169        fn test_from_pool_metadata() {
170            let assets: Vec<AssetEntry> = vec!["crab".into(), "junox".into()];
171            let dex = "junoswap".to_string();
172
173            let pool = PoolMetadata {
174                dex: dex.clone(),
175                pool_type: PoolType::Stable,
176                assets: assets.clone(),
177            };
178            let lp_token = LpToken::from(pool);
179            assert_that!(lp_token.dex).is_equal_to(dex);
180            assert_that!(lp_token.assets).is_equal_to(assets);
181        }
182    }
183}