penumbra_sdk_governance/
proposal_nft.rs

1use std::str::FromStr;
2
3use regex::Regex;
4
5use penumbra_sdk_asset::asset;
6
7/// Unbonding tokens represent staking tokens that are currently unbonding and
8/// subject to slashing.
9///
10/// Unbonding tokens are parameterized by the validator identity, the epoch at
11/// which unbonding began, and the epoch at which unbonding ends.
12pub 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        // This format string needs to be in sync with the asset registry
57        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    /// Make a new proposal NFT in the deposit state.
72    pub fn deposit(proposal_id: u64) -> Self {
73        Self::new(proposal_id, Kind::Deposit)
74    }
75
76    /// Make a new proposal NFT in the unbonding deposit state.
77    pub fn unbonding_deposit(proposal_id: u64) -> Self {
78        Self::new(proposal_id, Kind::UnbondingDeposit)
79    }
80
81    /// Make a new proposal NFT in the slashed state.
82    pub fn slashed(proposal_id: u64) -> Self {
83        Self::new(proposal_id, Kind::Slashed)
84    }
85
86    /// Make a new proposal NFT in the failed state.
87    pub fn failed(proposal_id: u64) -> Self {
88        Self::new(proposal_id, Kind::Failed)
89    }
90
91    /// Make a new proposal NFT in the passed state.
92    pub fn passed(proposal_id: u64) -> Self {
93        Self::new(proposal_id, Kind::Passed)
94    }
95
96    /// Get the base denomination for this delegation token.
97    pub fn denom(&self) -> asset::Metadata {
98        self.base_denom.clone()
99    }
100
101    /// Get the default display denomination for this delegation token.
102    pub fn default_unit(&self) -> asset::Unit {
103        self.base_denom.default_unit()
104    }
105
106    /// Get the asset ID for this delegation token.
107    pub fn id(&self) -> asset::Id {
108        self.base_denom.id()
109    }
110
111    /// Get the proposal ID for this proposal token.
112    pub fn proposal_id(&self) -> u64 {
113        self.proposal_id
114    }
115
116    /// Get the proposal state for this proposal token.
117    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        // Note: this regex must be in sync with asset::REGISTRY
129        // The data capture group is used by asset::REGISTRY
130        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}