solana_account_decoder/
parse_stake.rs

1use {
2    crate::{
3        parse_account_data::{ParsableAccount, ParseAccountError},
4        StringAmount,
5    },
6    bincode::deserialize,
7    serde::{Deserialize, Serialize},
8    solana_clock::{Epoch, UnixTimestamp},
9    solana_stake_interface::state::{Authorized, Delegation, Lockup, Meta, Stake, StakeStateV2},
10};
11
12pub fn parse_stake(data: &[u8]) -> Result<StakeAccountType, ParseAccountError> {
13    let stake_state: StakeStateV2 = deserialize(data)
14        .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Stake))?;
15    let parsed_account = match stake_state {
16        StakeStateV2::Uninitialized => StakeAccountType::Uninitialized,
17        StakeStateV2::Initialized(meta) => StakeAccountType::Initialized(UiStakeAccount {
18            meta: meta.into(),
19            stake: None,
20        }),
21        StakeStateV2::Stake(meta, stake, _) => StakeAccountType::Delegated(UiStakeAccount {
22            meta: meta.into(),
23            stake: Some(stake.into()),
24        }),
25        StakeStateV2::RewardsPool => StakeAccountType::RewardsPool,
26    };
27    Ok(parsed_account)
28}
29
30#[derive(Debug, Serialize, Deserialize, PartialEq)]
31#[serde(rename_all = "camelCase", tag = "type", content = "info")]
32pub enum StakeAccountType {
33    Uninitialized,
34    Initialized(UiStakeAccount),
35    Delegated(UiStakeAccount),
36    RewardsPool,
37}
38
39#[derive(Debug, Serialize, Deserialize, PartialEq)]
40#[serde(rename_all = "camelCase")]
41pub struct UiStakeAccount {
42    pub meta: UiMeta,
43    pub stake: Option<UiStake>,
44}
45
46#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
47#[serde(rename_all = "camelCase")]
48pub struct UiMeta {
49    pub rent_exempt_reserve: StringAmount,
50    pub authorized: UiAuthorized,
51    pub lockup: UiLockup,
52}
53
54impl From<Meta> for UiMeta {
55    fn from(meta: Meta) -> Self {
56        Self {
57            rent_exempt_reserve: meta.rent_exempt_reserve.to_string(),
58            authorized: meta.authorized.into(),
59            lockup: meta.lockup.into(),
60        }
61    }
62}
63
64#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "camelCase")]
66pub struct UiLockup {
67    pub unix_timestamp: UnixTimestamp,
68    pub epoch: Epoch,
69    pub custodian: String,
70}
71
72impl From<Lockup> for UiLockup {
73    fn from(lockup: Lockup) -> Self {
74        Self {
75            unix_timestamp: lockup.unix_timestamp,
76            epoch: lockup.epoch,
77            custodian: lockup.custodian.to_string(),
78        }
79    }
80}
81
82#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
83#[serde(rename_all = "camelCase")]
84pub struct UiAuthorized {
85    pub staker: String,
86    pub withdrawer: String,
87}
88
89impl From<Authorized> for UiAuthorized {
90    fn from(authorized: Authorized) -> Self {
91        Self {
92            staker: authorized.staker.to_string(),
93            withdrawer: authorized.withdrawer.to_string(),
94        }
95    }
96}
97
98#[derive(Debug, Serialize, Deserialize, PartialEq)]
99#[serde(rename_all = "camelCase")]
100pub struct UiStake {
101    pub delegation: UiDelegation,
102    pub credits_observed: u64,
103}
104
105impl From<Stake> for UiStake {
106    fn from(stake: Stake) -> Self {
107        Self {
108            delegation: stake.delegation.into(),
109            credits_observed: stake.credits_observed,
110        }
111    }
112}
113
114#[derive(Debug, Serialize, Deserialize, PartialEq)]
115#[serde(rename_all = "camelCase")]
116pub struct UiDelegation {
117    pub voter: String,
118    pub stake: StringAmount,
119    pub activation_epoch: StringAmount,
120    pub deactivation_epoch: StringAmount,
121    #[deprecated(
122        since = "1.16.7",
123        note = "Please use `solana_stake_interface::state::warmup_cooldown_rate()` instead"
124    )]
125    pub warmup_cooldown_rate: f64,
126}
127
128impl From<Delegation> for UiDelegation {
129    fn from(delegation: Delegation) -> Self {
130        #[allow(deprecated)]
131        Self {
132            voter: delegation.voter_pubkey.to_string(),
133            stake: delegation.stake.to_string(),
134            activation_epoch: delegation.activation_epoch.to_string(),
135            deactivation_epoch: delegation.deactivation_epoch.to_string(),
136            warmup_cooldown_rate: delegation.warmup_cooldown_rate,
137        }
138    }
139}
140
141#[cfg(test)]
142mod test {
143    use {super::*, bincode::serialize, solana_stake_interface::stake_flags::StakeFlags};
144
145    #[test]
146    #[allow(deprecated)]
147    fn test_parse_stake() {
148        let stake_state = StakeStateV2::Uninitialized;
149        let stake_data = serialize(&stake_state).unwrap();
150        assert_eq!(
151            parse_stake(&stake_data).unwrap(),
152            StakeAccountType::Uninitialized
153        );
154
155        let pubkey = solana_pubkey::new_rand();
156        let custodian = solana_pubkey::new_rand();
157        let authorized = Authorized::auto(&pubkey);
158        let lockup = Lockup {
159            unix_timestamp: 0,
160            epoch: 1,
161            custodian,
162        };
163        let meta = Meta {
164            rent_exempt_reserve: 42,
165            authorized,
166            lockup,
167        };
168
169        let stake_state = StakeStateV2::Initialized(meta);
170        let stake_data = serialize(&stake_state).unwrap();
171        assert_eq!(
172            parse_stake(&stake_data).unwrap(),
173            StakeAccountType::Initialized(UiStakeAccount {
174                meta: UiMeta {
175                    rent_exempt_reserve: 42.to_string(),
176                    authorized: UiAuthorized {
177                        staker: pubkey.to_string(),
178                        withdrawer: pubkey.to_string(),
179                    },
180                    lockup: UiLockup {
181                        unix_timestamp: 0,
182                        epoch: 1,
183                        custodian: custodian.to_string(),
184                    }
185                },
186                stake: None,
187            })
188        );
189
190        let voter_pubkey = solana_pubkey::new_rand();
191        let stake = Stake {
192            delegation: Delegation {
193                voter_pubkey,
194                stake: 20,
195                activation_epoch: 2,
196                deactivation_epoch: u64::MAX,
197                warmup_cooldown_rate: 0.25,
198            },
199            credits_observed: 10,
200        };
201
202        let stake_state = StakeStateV2::Stake(meta, stake, StakeFlags::empty());
203        let stake_data = serialize(&stake_state).unwrap();
204        assert_eq!(
205            parse_stake(&stake_data).unwrap(),
206            StakeAccountType::Delegated(UiStakeAccount {
207                meta: UiMeta {
208                    rent_exempt_reserve: 42.to_string(),
209                    authorized: UiAuthorized {
210                        staker: pubkey.to_string(),
211                        withdrawer: pubkey.to_string(),
212                    },
213                    lockup: UiLockup {
214                        unix_timestamp: 0,
215                        epoch: 1,
216                        custodian: custodian.to_string(),
217                    }
218                },
219                stake: Some(UiStake {
220                    delegation: UiDelegation {
221                        voter: voter_pubkey.to_string(),
222                        stake: 20.to_string(),
223                        activation_epoch: 2.to_string(),
224                        deactivation_epoch: u64::MAX.to_string(),
225                        warmup_cooldown_rate: 0.25,
226                    },
227                    credits_observed: 10,
228                })
229            })
230        );
231
232        let stake_state = StakeStateV2::RewardsPool;
233        let stake_data = serialize(&stake_state).unwrap();
234        assert_eq!(
235            parse_stake(&stake_data).unwrap(),
236            StakeAccountType::RewardsPool
237        );
238
239        let bad_data = vec![1, 2, 3, 4];
240        assert!(parse_stake(&bad_data).is_err());
241    }
242}