penumbra_sdk_governance/
proposal_nft.rs1use std::str::FromStr;
2
3use regex::Regex;
4
5use penumbra_sdk_asset::asset;
6
7pub struct ProposalNft {
13 proposal_id: u64,
14 proposal_state: Kind,
15 base_denom: asset::Metadata,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Kind {
20 Deposit,
21 UnbondingDeposit,
22 Slashed,
23 Failed,
24 Passed,
25}
26
27impl Kind {
28 pub const fn display_static(&self) -> &'static str {
29 match self {
30 Kind::Deposit => "deposit",
31 Kind::UnbondingDeposit => "unbonding_deposit",
32 Kind::Slashed => "slashed",
33 Kind::Failed => "failed",
34 Kind::Passed => "passed",
35 }
36 }
37}
38
39impl FromStr for Kind {
40 type Err = anyhow::Error;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 match s {
44 "deposit" => Ok(Kind::Deposit),
45 "unbonding_deposit" => Ok(Kind::UnbondingDeposit),
46 "slashed" => Ok(Kind::Slashed),
47 "failed" => Ok(Kind::Failed),
48 "passed" => Ok(Kind::Passed),
49 _ => Err(anyhow::anyhow!("invalid proposal token state")),
50 }
51 }
52}
53
54impl ProposalNft {
55 fn new(proposal_id: u64, proposal_state: Kind) -> Self {
56 let base_denom = asset::REGISTRY
58 .parse_denom(&format!(
59 "proposal_{}_{}",
60 proposal_id,
61 proposal_state.display_static()
62 ))
63 .expect("base denom format is valid");
64 ProposalNft {
65 proposal_id,
66 proposal_state,
67 base_denom,
68 }
69 }
70
71 pub fn deposit(proposal_id: u64) -> Self {
73 Self::new(proposal_id, Kind::Deposit)
74 }
75
76 pub fn unbonding_deposit(proposal_id: u64) -> Self {
78 Self::new(proposal_id, Kind::UnbondingDeposit)
79 }
80
81 pub fn slashed(proposal_id: u64) -> Self {
83 Self::new(proposal_id, Kind::Slashed)
84 }
85
86 pub fn failed(proposal_id: u64) -> Self {
88 Self::new(proposal_id, Kind::Failed)
89 }
90
91 pub fn passed(proposal_id: u64) -> Self {
93 Self::new(proposal_id, Kind::Passed)
94 }
95
96 pub fn denom(&self) -> asset::Metadata {
98 self.base_denom.clone()
99 }
100
101 pub fn default_unit(&self) -> asset::Unit {
103 self.base_denom.default_unit()
104 }
105
106 pub fn id(&self) -> asset::Id {
108 self.base_denom.id()
109 }
110
111 pub fn proposal_id(&self) -> u64 {
113 self.proposal_id
114 }
115
116 pub fn proposal_state(&self) -> Kind {
118 self.proposal_state
119 }
120}
121
122impl TryFrom<asset::Metadata> for ProposalNft {
123 type Error = anyhow::Error;
124
125 fn try_from(base_denom: asset::Metadata) -> Result<Self, Self::Error> {
126 let base_string = base_denom.to_string();
127
128 let captures = Regex::new("^proposal_(?P<data>(?P<proposal_id>[0-9]+)_(?P<proposal_state>deposit|unbonding_deposit|passed|failed|slashed))$")
131 .expect("regex is valid")
132 .captures(base_string.as_ref())
133 .ok_or_else(|| {
134 anyhow::anyhow!(
135 "base denom {} is not a proposal token",
136 base_denom.to_string()
137 )
138 })?;
139
140 let proposal_id = captures
141 .name("proposal_id")
142 .expect("proposal_id is a named capture")
143 .as_str()
144 .parse()?;
145
146 let proposal_state = captures
147 .name("proposal_state")
148 .expect("proposal_state is a named capture")
149 .as_str()
150 .parse()?;
151
152 Ok(Self {
153 base_denom,
154 proposal_state,
155 proposal_id,
156 })
157 }
158}
159
160impl FromStr for ProposalNft {
161 type Err = anyhow::Error;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 asset::REGISTRY
165 .parse_denom(s)
166 .ok_or_else(|| anyhow::anyhow!("could not parse {} as base denomination", s))?
167 .try_into()
168 }
169}
170
171impl std::fmt::Display for ProposalNft {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 self.base_denom.fmt(f)
174 }
175}
176
177impl std::fmt::Debug for ProposalNft {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 self.base_denom.fmt(f)
180 }
181}
182
183impl PartialEq for ProposalNft {
184 fn eq(&self, other: &Self) -> bool {
185 self.base_denom.eq(&other.base_denom)
186 }
187}
188
189impl Eq for ProposalNft {}
190
191impl std::hash::Hash for ProposalNft {
192 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
193 self.base_denom.hash(state)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn proposal_token_denomination_round_trip() {
203 let tokens = [
204 ProposalNft::deposit(1),
205 ProposalNft::unbonding_deposit(1),
206 ProposalNft::passed(1),
207 ProposalNft::failed(1),
208 ProposalNft::slashed(1),
209 ];
210
211 for token in tokens {
212 let denom = token.to_string();
213 println!("denom: {denom}");
214 let token2 = ProposalNft::from_str(&denom).unwrap();
215 let denom2 = token2.to_string();
216
217 assert_eq!(denom, denom2);
218 assert_eq!(token, token2);
219 }
220 }
221}