penumbra_sdk_dex/lp/
nft.rs1use penumbra_sdk_asset::asset;
2use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
3use regex::Regex;
4
5use super::position::{Id, State};
6
7#[derive(Debug, Clone)]
23pub struct LpNft {
24 position_id: Id,
25 state: State,
26 base_denom: asset::Metadata,
27}
28
29impl LpNft {
30 pub fn new(position_id: Id, state: State) -> Self {
31 let base_denom = asset::REGISTRY
32 .parse_denom(&format!("lpnft_{state}_{position_id}"))
33 .expect("base denom format is valid");
34
35 Self {
36 position_id,
37 state,
38 base_denom,
39 }
40 }
41
42 pub fn denom(&self) -> asset::Metadata {
43 self.base_denom.clone()
44 }
45
46 pub fn asset_id(&self) -> asset::Id {
47 self.base_denom.id()
48 }
49
50 pub fn position_id(&self) -> Id {
51 self.position_id
52 }
53
54 pub fn state(&self) -> State {
55 self.state
56 }
57}
58
59impl TryFrom<asset::Metadata> for LpNft {
60 type Error = anyhow::Error;
61
62 fn try_from(base_denom: asset::Metadata) -> Result<Self, Self::Error> {
63 let base_denom_string = base_denom.to_string();
66 let captures = Regex::new("^lpnft_(?P<state>[a-z_0-9]+)_(?P<id>plpid1[a-zA-HJ-NP-Z0-9]+)$")
67 .expect("regex is valid")
68 .captures(&base_denom_string)
69 .ok_or_else(|| {
70 anyhow::anyhow!(
71 "base denom {} is not a delegation token",
72 base_denom.to_string()
73 )
74 })?;
75
76 let position_id = captures
77 .name("id")
78 .expect("id is a named capture")
79 .as_str()
80 .parse()?;
81 let state = captures
82 .name("state")
83 .expect("state is a named capture")
84 .as_str()
85 .parse()?;
86
87 Ok(Self {
88 position_id,
89 state,
90 base_denom,
91 })
92 }
93}
94
95impl std::fmt::Display for LpNft {
96 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
97 self.base_denom.fmt(f)
98 }
99}
100
101impl std::str::FromStr for LpNft {
102 type Err = anyhow::Error;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 let base_denom = asset::REGISTRY
106 .parse_denom(s)
107 .ok_or_else(|| anyhow::anyhow!("invalid denom string"))?;
108 base_denom.try_into()
109 }
110}
111
112impl std::cmp::PartialEq for LpNft {
113 fn eq(&self, other: &Self) -> bool {
114 self.position_id == other.position_id && self.state == other.state
115 }
116}
117
118impl std::cmp::Eq for LpNft {}
119
120impl DomainType for LpNft {
121 type Proto = pb::LpNft;
122}
123
124impl TryFrom<pb::LpNft> for LpNft {
125 type Error = anyhow::Error;
126
127 fn try_from(value: pb::LpNft) -> Result<Self, Self::Error> {
128 let position_id = value
129 .position_id
130 .ok_or_else(|| anyhow::anyhow!("missing position id"))?
131 .try_into()?;
132 let state = value
133 .state
134 .ok_or_else(|| anyhow::anyhow!("missing position state"))?
135 .try_into()?;
136
137 Ok(Self::new(position_id, state))
138 }
139}
140
141impl From<LpNft> for pb::LpNft {
142 fn from(v: LpNft) -> Self {
143 pb::LpNft {
144 position_id: Some(v.position_id.into()),
145 state: Some(v.state.into()),
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use penumbra_sdk_asset::STAKING_TOKEN_ASSET_ID;
154
155 use super::super::{super::DirectedTradingPair, position::*};
156
157 #[test]
158 fn lpnft_denom_parsing_roundtrip() {
159 let pair = DirectedTradingPair {
160 start: *STAKING_TOKEN_ASSET_ID,
161 end: asset::Cache::with_known_assets()
162 .get_unit("cube")
163 .unwrap()
164 .id(),
165 };
166
167 let position = Position::new(
168 rand_core::OsRng,
169 pair,
170 1u32,
171 1u64.into(),
172 1u64.into(),
173 crate::lp::Reserves {
174 r1: 1u64.into(),
175 r2: 1u64.into(),
176 },
177 );
178 let position_id = position.id();
179
180 let lpnft1 = LpNft::new(position_id, State::Opened);
181 let lpnft1_string = lpnft1.denom().to_string();
182 assert_eq!(lpnft1.to_string(), lpnft1_string);
183
184 let lpnft2_denom = asset::REGISTRY.parse_denom(&lpnft1_string).unwrap();
185 let lpnft2 = LpNft::try_from(lpnft2_denom).unwrap();
186 assert_eq!(lpnft1, lpnft2);
187
188 let lpnft3: LpNft = lpnft1_string.parse().unwrap();
189 assert_eq!(lpnft1, lpnft3);
190
191 let lpnft_c = LpNft::new(position_id, State::Closed);
192 let lpnft_c_string = lpnft_c.denom().to_string();
193 let lpnft_c_2 = lpnft_c_string.parse().unwrap();
194 assert_eq!(lpnft_c, lpnft_c_2);
195
196 let lpnft_w0 = LpNft::new(position_id, State::Withdrawn { sequence: 0 });
197 let lpnft_w0_string = lpnft_w0.denom().to_string();
198 let lpnft_w0_2 = lpnft_w0_string.parse().unwrap();
199 assert_eq!(lpnft_w0, lpnft_w0_2);
200
201 let lpnft_w1 = LpNft::new(position_id, State::Withdrawn { sequence: 1 });
202 let lpnft_w1_string = lpnft_w1.denom().to_string();
203 let lpnft_w1_2 = lpnft_w1_string.parse().unwrap();
204 assert_eq!(lpnft_w1, lpnft_w1_2);
205 }
206}