penumbra_sdk_governance/
voting_receipt_token.rs

1use std::str::FromStr;
2
3use regex::Regex;
4
5use penumbra_sdk_asset::asset;
6
7/// Voting receipt tokens represent proof of participation in a governance vote.
8///
9/// Voting receipt tokens are parameterized by the proposal ID they are associated with.
10pub struct VotingReceiptToken {
11    proposal_id: u64,
12    base_denom: asset::Metadata,
13}
14
15impl VotingReceiptToken {
16    pub fn new(proposal_id: u64) -> Self {
17        // This format string needs to be in sync with the asset registry
18        let base_denom = asset::REGISTRY
19            .parse_denom(&format!("uvoted_on_{proposal_id}"))
20            .expect("base denom format is valid");
21        VotingReceiptToken {
22            proposal_id,
23            base_denom,
24        }
25    }
26
27    /// Get the base denomination for this voting receipt token.
28    pub fn denom(&self) -> asset::Metadata {
29        self.base_denom.clone()
30    }
31
32    /// Get the default display denomination for this voting receipt token.
33    pub fn default_unit(&self) -> asset::Unit {
34        self.base_denom.default_unit()
35    }
36
37    /// Get the asset ID for this voting receipt token.
38    pub fn id(&self) -> asset::Id {
39        self.base_denom.id()
40    }
41
42    /// Get the proposal ID this voting receipt token is associated with.
43    pub fn proposal_id(&self) -> u64 {
44        self.proposal_id
45    }
46}
47
48impl TryFrom<asset::Metadata> for VotingReceiptToken {
49    type Error = anyhow::Error;
50
51    fn try_from(base_denom: asset::Metadata) -> Result<Self, Self::Error> {
52        let base_string = base_denom.to_string();
53
54        // Note: this regex must be in sync with both asset::REGISTRY
55        // and VALIDATOR_IDENTITY_BECH32_PREFIX
56        // The data capture group is used by asset::REGISTRY
57        let captures = Regex::new("^uvoted_on_(?P<data>(?P<proposal_id>[0-9]+))$")
58            .expect("regex is valid")
59            .captures(base_string.as_ref())
60            .ok_or_else(|| {
61                anyhow::anyhow!(
62                    "base denom {} is not an unbonding token",
63                    base_denom.to_string()
64                )
65            })?;
66
67        let proposal_id: u64 = captures
68            .name("proposal_id")
69            .expect("proposal_id is a named capture")
70            .as_str()
71            .parse()?;
72
73        Ok(Self {
74            proposal_id,
75            base_denom,
76        })
77    }
78}
79
80impl FromStr for VotingReceiptToken {
81    type Err = anyhow::Error;
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        asset::REGISTRY
84            .parse_denom(s)
85            .ok_or_else(|| anyhow::anyhow!("could not parse {} as base denomination", s))?
86            .try_into()
87    }
88}
89
90impl std::fmt::Display for VotingReceiptToken {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        self.base_denom.fmt(f)
93    }
94}
95
96impl std::fmt::Debug for VotingReceiptToken {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        self.base_denom.fmt(f)
99    }
100}
101
102impl PartialEq for VotingReceiptToken {
103    fn eq(&self, other: &Self) -> bool {
104        self.base_denom.eq(&other.base_denom)
105    }
106}
107
108impl Eq for VotingReceiptToken {}
109
110impl std::hash::Hash for VotingReceiptToken {
111    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
112        self.base_denom.hash(state)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn unbonding_token_denomination_round_trip() {
122        let proposal_id: u64 = 1;
123
124        let token = VotingReceiptToken::new(proposal_id);
125
126        let denom = token.to_string();
127        println!("denom: {denom}");
128        let token2 = VotingReceiptToken::from_str(&denom).unwrap();
129        let denom2 = token2.to_string();
130
131        assert_eq!(denom, denom2);
132        assert_eq!(token, token2);
133    }
134}