1use penumbra_sdk_keys::Address;
4use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
5use serde::{Deserialize, Serialize};
6use serde_unit_struct::{Deserialize_unit_struct, Serialize_unit_struct};
7use serde_with::{serde_as, DisplayFromStr};
8
9use crate::{DelegationToken, FundingStream, FundingStreams, GovernanceKey, IdentityKey};
10
11mod bonding;
12mod definition;
13mod info;
14mod state;
15mod status;
16
17pub use bonding::State as BondingState;
18pub use definition::Definition;
19pub use info::Info;
20pub use state::State;
21pub use status::Status;
22
23#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
28#[serde(try_from = "pb::Validator", into = "pb::Validator")]
29pub struct Validator {
30 pub identity_key: IdentityKey,
32
33 pub governance_key: GovernanceKey,
35
36 pub consensus_key: tendermint::PublicKey,
39
40 pub name: String,
43
44 pub website: String,
47
48 pub description: String,
51
52 pub enabled: bool,
56
57 pub funding_streams: FundingStreams,
63
64 pub sequence_number: u32,
70}
71
72impl Validator {
73 pub fn token(&self) -> DelegationToken {
74 DelegationToken::new(self.identity_key.clone())
75 }
76}
77
78#[serde_as]
79#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
80pub struct ValidatorToml {
81 pub sequence_number: u32,
86
87 pub enabled: bool,
91
92 pub name: String,
94
95 pub website: String,
97
98 pub description: String,
100
101 #[serde_as(as = "DisplayFromStr")]
103 pub identity_key: IdentityKey,
104
105 #[serde_as(as = "DisplayFromStr")]
107 pub governance_key: GovernanceKey,
108
109 pub consensus_key: tendermint::PublicKey,
112
113 #[serde(rename = "funding_stream")]
119 pub funding_streams: Vec<FundingStreamToml>,
120}
121
122impl From<Validator> for ValidatorToml {
123 fn from(v: Validator) -> Self {
124 ValidatorToml {
125 identity_key: v.identity_key,
126 governance_key: v.governance_key,
127 consensus_key: v.consensus_key,
128 name: v.name,
129 website: v.website,
130 description: v.description,
131 enabled: v.enabled,
132 funding_streams: v.funding_streams.into_iter().map(Into::into).collect(),
133 sequence_number: v.sequence_number,
134 }
135 }
136}
137
138impl TryFrom<ValidatorToml> for Validator {
139 type Error = anyhow::Error;
140
141 fn try_from(v: ValidatorToml) -> anyhow::Result<Self> {
142 if v.website.len() > 70 {
145 anyhow::bail!("validator website field must be less than 70 bytes");
146 }
147
148 if v.name.len() > 140 {
150 anyhow::bail!("validator name must be less than 140 bytes");
151 }
152
153 if v.description.len() > 280 {
155 anyhow::bail!("validator description must be less than 280 bytes");
156 }
157
158 Ok(Validator {
159 identity_key: v.identity_key,
160 governance_key: v.governance_key,
161 consensus_key: v.consensus_key,
162 name: v.name,
163 website: v.website,
164 description: v.description,
165 enabled: v.enabled,
166 funding_streams: FundingStreams::try_from(
167 v.funding_streams
168 .into_iter()
169 .map(Into::into)
170 .collect::<Vec<_>>(),
171 )?,
172 sequence_number: v.sequence_number,
173 })
174 }
175}
176
177#[allow(clippy::large_enum_variant)]
179#[serde_as]
180#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
181#[serde(untagged)]
182pub enum FundingStreamToml {
183 Address {
184 #[serde(rename = "recipient")]
185 #[serde_as(as = "DisplayFromStr")]
186 address: Address,
187 rate_bps: u16,
188 },
189 CommunityPool {
190 recipient: CommunityPool,
191 rate_bps: u16,
192 },
193}
194
195#[derive(Debug, PartialEq, Eq, Clone, Deserialize_unit_struct, Serialize_unit_struct)]
197pub struct CommunityPool;
198
199impl From<FundingStream> for FundingStreamToml {
200 fn from(f: FundingStream) -> Self {
201 match f {
202 FundingStream::ToAddress { address, rate_bps } => {
203 FundingStreamToml::Address { address, rate_bps }
204 }
205 FundingStream::ToCommunityPool { rate_bps } => FundingStreamToml::CommunityPool {
206 rate_bps,
207 recipient: CommunityPool,
208 },
209 }
210 }
211}
212
213impl From<FundingStreamToml> for FundingStream {
214 fn from(f: FundingStreamToml) -> Self {
215 match f {
216 FundingStreamToml::Address { address, rate_bps } => {
217 FundingStream::ToAddress { address, rate_bps }
218 }
219 FundingStreamToml::CommunityPool { rate_bps, .. } => {
220 FundingStream::ToCommunityPool { rate_bps }
221 }
222 }
223 }
224}
225
226impl DomainType for Validator {
227 type Proto = pb::Validator;
228}
229
230impl From<Validator> for pb::Validator {
231 fn from(v: Validator) -> Self {
232 pb::Validator {
233 identity_key: Some(v.identity_key.into()),
234 governance_key: Some(v.governance_key.into()),
235 consensus_key: v.consensus_key.to_bytes(),
236 name: v.name,
237 website: v.website,
238 description: v.description,
239 enabled: v.enabled,
240 funding_streams: v.funding_streams.into_iter().map(Into::into).collect(),
241 sequence_number: v.sequence_number,
242 }
243 }
244}
245
246impl TryFrom<pb::Validator> for Validator {
247 type Error = anyhow::Error;
248 fn try_from(v: pb::Validator) -> Result<Self, Self::Error> {
249 if v.website.len() > 70 {
252 anyhow::bail!("validator website field must be less than 70 bytes");
253 }
254
255 if v.name.len() > 140 {
257 anyhow::bail!("validator name must be less than 140 bytes");
258 }
259
260 if v.description.len() > 280 {
262 anyhow::bail!("validator description must be less than 280 bytes");
263 }
264
265 Ok(Validator {
266 identity_key: v
267 .identity_key
268 .ok_or_else(|| anyhow::anyhow!("missing identity key"))?
269 .try_into()?,
270 governance_key: v
271 .governance_key
272 .ok_or_else(|| anyhow::anyhow!("missing governance key"))?
273 .try_into()?,
274 consensus_key: tendermint::PublicKey::from_raw_ed25519(&v.consensus_key)
275 .ok_or_else(|| anyhow::anyhow!("invalid ed25519 consensus pubkey"))?,
276 name: v.name,
277 website: v.website,
278 description: v.description,
279 enabled: v.enabled,
280 funding_streams: v
281 .funding_streams
282 .into_iter()
283 .map(TryInto::try_into)
284 .collect::<Result<Vec<FundingStream>, _>>()?
285 .try_into()?,
286 sequence_number: v.sequence_number,
287 })
288 }
289}