solana_account_decoder/
parse_stake.rs

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