penumbra_sdk_stake/
unbonding_token.rs1use std::str::FromStr;
2
3use regex::Regex;
4
5use penumbra_sdk_asset::asset;
6
7use crate::IdentityKey;
8
9pub 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 let base_denom = asset::REGISTRY
24 .parse_denom(&format!(
25 "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 pub fn denom(&self) -> asset::Metadata {
38 self.base_denom.clone()
39 }
40
41 pub fn default_unit(&self) -> asset::Unit {
43 self.base_denom.default_unit()
44 }
45
46 pub fn id(&self) -> asset::Id {
48 self.base_denom.id()
49 }
50
51 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 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}