1use serde::{Deserialize, Serialize};
2
3use penumbra_sdk_proto::{penumbra::core::component::governance::v1 as pb, DomainType};
4
5use crate::MAX_VALIDATOR_VOTE_REASON_LENGTH;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(try_from = "pb::ProposalState", into = "pb::ProposalState")]
9pub enum State {
10 Voting,
11 Withdrawn { reason: String },
12 Finished { outcome: Outcome<String> },
13 Claimed { outcome: Outcome<String> },
14}
15
16impl State {
17 pub fn is_voting(&self) -> bool {
18 matches!(self, State::Voting)
19 }
20
21 pub fn is_withdrawn(&self) -> bool {
22 matches!(self, State::Withdrawn { .. })
23 }
24
25 pub fn is_finished(&self) -> bool {
26 matches!(self, State::Finished { .. })
27 }
28
29 pub fn is_claimed(&self) -> bool {
30 matches!(self, State::Claimed { .. })
31 }
32
33 pub fn is_passed(&self) -> bool {
34 match self {
35 State::Finished { outcome } => outcome.is_passed(),
36 State::Claimed { outcome } => outcome.is_passed(),
37 _ => false,
38 }
39 }
40
41 pub fn is_failed(&self) -> bool {
42 match self {
43 State::Finished { outcome } => outcome.is_failed(),
44 State::Claimed { outcome } => outcome.is_failed(),
45 _ => false,
46 }
47 }
48
49 pub fn is_slashed(&self) -> bool {
50 match self {
51 State::Finished { outcome } => outcome.is_slashed(),
52 State::Claimed { outcome } => outcome.is_slashed(),
53 _ => false,
54 }
55 }
56}
57
58impl State {
59 pub fn withdrawn(self) -> Withdrawn<String> {
60 match self {
61 State::Voting => Withdrawn::No,
62 State::Withdrawn { reason } => Withdrawn::WithReason { reason },
63 State::Finished { outcome } => match outcome {
64 Outcome::Passed => Withdrawn::No,
65 Outcome::Failed { withdrawn } | Outcome::Slashed { withdrawn } => withdrawn,
66 },
67 State::Claimed { outcome } => match outcome {
68 Outcome::Passed => Withdrawn::No,
69 Outcome::Failed { withdrawn } | Outcome::Slashed { withdrawn } => withdrawn,
70 },
71 }
72 }
73}
74
75impl DomainType for State {
76 type Proto = pb::ProposalState;
77}
78
79impl From<State> for pb::ProposalState {
80 fn from(s: State) -> Self {
81 let state = match s {
82 State::Voting => pb::proposal_state::State::Voting(pb::proposal_state::Voting {}),
83 State::Withdrawn { reason } => {
84 pb::proposal_state::State::Withdrawn(pb::proposal_state::Withdrawn { reason })
85 }
86 State::Finished { outcome } => {
87 pb::proposal_state::State::Finished(pb::proposal_state::Finished {
88 outcome: Some(outcome.into()),
89 })
90 }
91 State::Claimed { outcome } => {
92 pb::proposal_state::State::Finished(pb::proposal_state::Finished {
93 outcome: Some(outcome.into()),
94 })
95 }
96 };
97 pb::ProposalState { state: Some(state) }
98 }
99}
100
101impl TryFrom<pb::ProposalState> for State {
102 type Error = anyhow::Error;
103
104 fn try_from(msg: pb::ProposalState) -> Result<Self, Self::Error> {
105 Ok(
106 match msg
107 .state
108 .ok_or_else(|| anyhow::anyhow!("missing proposal state"))?
109 {
110 pb::proposal_state::State::Voting(pb::proposal_state::Voting {}) => State::Voting,
111 pb::proposal_state::State::Withdrawn(pb::proposal_state::Withdrawn { reason }) => {
112 State::Withdrawn { reason }
113 }
114 pb::proposal_state::State::Finished(pb::proposal_state::Finished { outcome }) => {
115 State::Finished {
116 outcome: outcome
117 .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))?
118 .try_into()?,
119 }
120 }
121 pb::proposal_state::State::Claimed(pb::proposal_state::Claimed { outcome }) => {
122 State::Claimed {
123 outcome: outcome
124 .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))?
125 .try_into()?,
126 }
127 }
128 },
129 )
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)]
137#[serde(
138 try_from = "pb::ProposalOutcome",
139 into = "pb::ProposalOutcome",
140 bound = "W: Clone, pb::ProposalOutcome: From<Outcome<W>>, Outcome<W>: TryFrom<pb::ProposalOutcome, Error = anyhow::Error>"
141)]
142pub enum Outcome<W> {
143 Passed,
144 Failed { withdrawn: Withdrawn<W> },
145 Slashed { withdrawn: Withdrawn<W> },
146}
147
148impl<W> Outcome<W> {
149 pub fn should_be_refunded(&self) -> bool {
151 !self.is_slashed()
152 }
153
154 pub fn is_slashed(&self) -> bool {
155 matches!(self, Outcome::Slashed { .. })
156 }
157
158 pub fn is_failed(&self) -> bool {
159 matches!(self, Outcome::Failed { .. } | Outcome::Slashed { .. })
160 }
161
162 pub fn is_passed(&self) -> bool {
163 matches!(self, Outcome::Passed)
164 }
165
166 pub fn as_ref(&self) -> Outcome<&W> {
167 match self {
168 Outcome::Passed => Outcome::Passed,
169 Outcome::Failed { withdrawn } => Outcome::Failed {
170 withdrawn: withdrawn.as_ref(),
171 },
172 Outcome::Slashed { withdrawn } => Outcome::Slashed {
173 withdrawn: withdrawn.as_ref(),
174 },
175 }
176 }
177
178 pub fn map<X>(self, f: impl FnOnce(W) -> X) -> Outcome<X> {
179 match self {
180 Outcome::Passed => Outcome::Passed,
181 Outcome::Failed { withdrawn } => Outcome::Failed {
182 withdrawn: Option::from(withdrawn).map(f).into(),
183 },
184 Outcome::Slashed { withdrawn } => Outcome::Slashed {
185 withdrawn: Option::from(withdrawn).map(f).into(),
186 },
187 }
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Copy)]
195pub enum Withdrawn<W> {
196 No,
197 WithReason { reason: W },
198}
199
200impl<W> Withdrawn<W> {
201 pub fn as_ref(&self) -> Withdrawn<&W> {
202 match self {
203 Withdrawn::No => Withdrawn::No,
204 Withdrawn::WithReason { reason } => Withdrawn::WithReason { reason },
205 }
206 }
207}
208
209impl<W> From<Option<W>> for Withdrawn<W> {
210 fn from(reason: Option<W>) -> Self {
211 match reason {
212 Some(reason) => Withdrawn::WithReason { reason },
213 None => Withdrawn::No,
214 }
215 }
216}
217
218impl<W> From<Withdrawn<W>> for Option<W> {
219 fn from(withdrawn: Withdrawn<W>) -> Self {
220 match withdrawn {
221 Withdrawn::No => None,
222 Withdrawn::WithReason { reason } => Some(reason),
223 }
224 }
225}
226
227impl TryFrom<Withdrawn<String>> for Withdrawn<()> {
228 type Error = anyhow::Error;
229
230 fn try_from(withdrawn: Withdrawn<String>) -> Result<Self, Self::Error> {
231 Ok(match withdrawn {
232 Withdrawn::No => Withdrawn::No,
233 Withdrawn::WithReason { reason } => {
234 if reason.is_empty() {
235 Withdrawn::WithReason { reason: () }
236 } else {
237 anyhow::bail!("withdrawn reason is not empty")
238 }
239 }
240 })
241 }
242}
243
244impl DomainType for Outcome<String> {
245 type Proto = pb::ProposalOutcome;
246}
247
248impl From<Outcome<String>> for pb::ProposalOutcome {
249 fn from(o: Outcome<String>) -> Self {
250 let outcome = match o {
251 Outcome::Passed => {
252 pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {})
253 }
254 Outcome::Failed { withdrawn } => {
255 pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed {
256 withdrawn: match withdrawn {
257 Withdrawn::No => None,
258 Withdrawn::WithReason { reason } => {
259 Some(pb::proposal_outcome::Withdrawn { reason })
260 }
261 },
262 })
263 }
264 Outcome::Slashed { withdrawn } => {
265 pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed {
266 withdrawn: match withdrawn {
267 Withdrawn::No => None,
268 Withdrawn::WithReason { reason } => {
269 Some(pb::proposal_outcome::Withdrawn { reason })
270 }
271 },
272 })
273 }
274 };
275 pb::ProposalOutcome {
276 outcome: Some(outcome),
277 }
278 }
279}
280
281impl TryFrom<pb::ProposalOutcome> for Outcome<String> {
282 type Error = anyhow::Error;
283
284 fn try_from(msg: pb::ProposalOutcome) -> Result<Self, Self::Error> {
285 Ok(
286 match msg
287 .outcome
288 .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))?
289 {
290 pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) => {
291 Outcome::Passed
292 }
293 pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed {
294 withdrawn,
295 }) => Outcome::Failed {
296 withdrawn: if let Some(pb::proposal_outcome::Withdrawn { reason }) = withdrawn {
297 if reason.len() > MAX_VALIDATOR_VOTE_REASON_LENGTH {
299 anyhow::bail!("withdrawn reason must be smaller than 1kb")
300 }
301
302 Withdrawn::WithReason { reason }
303 } else {
304 Withdrawn::No
305 },
306 },
307 pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed {
308 withdrawn,
309 }) => Outcome::Slashed {
310 withdrawn: if let Some(pb::proposal_outcome::Withdrawn { reason }) = withdrawn {
311 if reason.len() > MAX_VALIDATOR_VOTE_REASON_LENGTH {
313 anyhow::bail!("withdrawn reason must be smaller than 1kb")
314 }
315 Withdrawn::WithReason { reason }
316 } else {
317 Withdrawn::No
318 },
319 },
320 },
321 )
322 }
323}
324
325impl DomainType for Outcome<()> {
326 type Proto = pb::ProposalOutcome;
327}
328
329impl From<Outcome<()>> for pb::ProposalOutcome {
330 fn from(o: Outcome<()>) -> Self {
331 let outcome = match o {
332 Outcome::Passed => {
333 pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {})
334 }
335 Outcome::Failed { withdrawn } => {
336 pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed {
337 withdrawn: <Option<()>>::from(withdrawn).map(|()| {
338 pb::proposal_outcome::Withdrawn {
339 reason: "".to_string(),
340 }
341 }),
342 })
343 }
344 Outcome::Slashed { withdrawn } => {
345 pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed {
346 withdrawn: <Option<()>>::from(withdrawn).map(|()| {
347 pb::proposal_outcome::Withdrawn {
348 reason: "".to_string(),
349 }
350 }),
351 })
352 }
353 };
354 pb::ProposalOutcome {
355 outcome: Some(outcome),
356 }
357 }
358}
359
360impl TryFrom<pb::ProposalOutcome> for Outcome<()> {
361 type Error = anyhow::Error;
362
363 fn try_from(msg: pb::ProposalOutcome) -> Result<Self, Self::Error> {
364 Ok(
365 match msg
366 .outcome
367 .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))?
368 {
369 pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) => {
370 Outcome::Passed
371 }
372 pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed {
373 withdrawn,
374 }) => {
375 if withdrawn.is_some() {
377 let reason = &withdrawn.as_ref().expect("reason is some").reason;
378 if reason.len() > MAX_VALIDATOR_VOTE_REASON_LENGTH {
379 anyhow::bail!("withdrawn reason must be smaller than 1kb");
380 }
381 }
382 Outcome::Failed {
383 withdrawn: <Withdrawn<String>>::from(withdrawn.map(|w| w.reason))
384 .try_into()?,
385 }
386 }
387 pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed {
388 withdrawn,
389 }) => {
390 if withdrawn.is_some() {
392 let reason = &withdrawn.as_ref().expect("reason is some").reason;
393 if reason.len() > MAX_VALIDATOR_VOTE_REASON_LENGTH {
394 anyhow::bail!("withdrawn reason must be smaller than 1kb");
395 }
396 }
397
398 Outcome::Slashed {
399 withdrawn: <Withdrawn<String>>::from(withdrawn.map(|w| w.reason))
400 .try_into()?,
401 }
402 }
403 },
404 )
405 }
406}