penumbra_sdk_stake/
unbonding_token.rs

1use std::str::FromStr;
2
3use regex::Regex;
4
5use penumbra_sdk_asset::asset;
6
7use crate::IdentityKey;
8
9/// Unbonding tokens represent staking tokens that are currently unbonding and
10/// subject to slashing.
11///
12/// Unbonding tokens are parameterized by the validator identity, and the epoch at
13/// which unbonding began.
14pub struct UnbondingToken {
15    validator_identity: IdentityKey,
16    unbonding_start_height: u64,
17    base_denom: asset::Metadata,
18}
19
20impl UnbondingToken {
21    pub fn new(validator_identity: IdentityKey, unbonding_start_height: u64) -> Self {
22        // This format string needs to be in sync with the asset registry
23        let base_denom = asset::REGISTRY
24            .parse_denom(&format!(
25                // "uu" is not a typo, these are micro-unbonding tokens
26                "uunbonding_start_at_{unbonding_start_height}_{validator_identity}"
27            ))
28            .expect("base denom format is valid");
29        UnbondingToken {
30            validator_identity,
31            base_denom,
32            unbonding_start_height,
33        }
34    }
35
36    /// Get the base denomination for this delegation token.
37    pub fn denom(&self) -> asset::Metadata {
38        self.base_denom.clone()
39    }
40
41    /// Get the default display denomination for this delegation token.
42    pub fn default_unit(&self) -> asset::Unit {
43        self.base_denom.default_unit()
44    }
45
46    /// Get the asset ID for this delegation token.
47    pub fn id(&self) -> asset::Id {
48        self.base_denom.id()
49    }
50
51    /// Get the identity key of the validator this delegation token is associated with.
52    pub fn validator(&self) -> IdentityKey {
53        self.validator_identity.clone()
54    }
55
56    pub fn unbonding_start_height(&self) -> u64 {
57        self.unbonding_start_height
58    }
59}
60
61impl TryFrom<asset::Metadata> for UnbondingToken {
62    type Error = anyhow::Error;
63
64    fn try_from(base_denom: asset::Metadata) -> Result<Self, Self::Error> {
65        let base_string = base_denom.to_string();
66
67        // Note: this regex must be in sync with both asset::REGISTRY
68        // and VALIDATOR_IDENTITY_BECH32_PREFIX
69        // The data capture group is used by asset::REGISTRY
70        let captures =
71            Regex::new("^uunbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$")
72                .expect("regex is valid")
73                .captures(base_string.as_ref())
74                .ok_or_else(|| {
75                    anyhow::anyhow!(
76                        "base denom {} is not an unbonding token",
77                        base_denom.to_string()
78                    )
79                })?;
80
81        let validator_identity = captures
82            .name("validator")
83            .expect("validator is a named capture")
84            .as_str()
85            .parse()?;
86
87        let unbonding_start_height = captures
88            .name("start")
89            .expect("start is a named capture")
90            .as_str()
91            .parse()?;
92
93        Ok(Self {
94            base_denom,
95            validator_identity,
96            unbonding_start_height,
97        })
98    }
99}
100
101impl FromStr for UnbondingToken {
102    type Err = anyhow::Error;
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        asset::REGISTRY
105            .parse_denom(s)
106            .ok_or_else(|| anyhow::anyhow!("could not parse {} as base denomination", s))?
107            .try_into()
108    }
109}
110
111impl std::fmt::Display for UnbondingToken {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        self.base_denom.fmt(f)
114    }
115}
116
117impl std::fmt::Debug for UnbondingToken {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        self.base_denom.fmt(f)
120    }
121}
122
123impl PartialEq for UnbondingToken {
124    fn eq(&self, other: &Self) -> bool {
125        self.base_denom.eq(&other.base_denom)
126    }
127}
128
129impl Eq for UnbondingToken {}
130
131impl std::hash::Hash for UnbondingToken {
132    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
133        self.base_denom.hash(state)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use decaf377_rdsa::{SigningKey, VerificationKey};
140
141    use super::*;
142
143    #[test]
144    fn unbonding_token_denomination_round_trip() {
145        use rand_core::OsRng;
146
147        let vk = VerificationKey::from(SigningKey::new(OsRng));
148        let ik = IdentityKey(vk.into());
149        let start = 782;
150
151        let token = UnbondingToken::new(ik, start);
152
153        let denom = token.to_string();
154        println!("denom: {denom}");
155        let token2 = UnbondingToken::from_str(&denom).unwrap();
156        let denom2 = token2.to_string();
157
158        assert_eq!(denom, denom2);
159        assert_eq!(token, token2);
160    }
161}