Skip to main content

solana_account_decoder/
parse_stake.rs

1use {
2    crate::{
3        StringAmount,
4        parse_account_data::{ParsableAccount, ParseAccountError},
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    #[deprecated(
50        since = "4.1.0",
51        note = "Stake account rent must be calculated via the `Rent` sysvar. This value will \
52                cease to be correct once lamports-per-byte is adjusted."
53    )]
54    pub rent_exempt_reserve: StringAmount,
55    pub authorized: UiAuthorized,
56    pub lockup: UiLockup,
57}
58
59impl From<Meta> for UiMeta {
60    fn from(meta: Meta) -> Self {
61        Self {
62            #[expect(deprecated)]
63            rent_exempt_reserve: meta.rent_exempt_reserve.to_string(),
64            authorized: meta.authorized.into(),
65            lockup: meta.lockup.into(),
66        }
67    }
68}
69
70#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
71#[serde(rename_all = "camelCase")]
72pub struct UiLockup {
73    pub unix_timestamp: UnixTimestamp,
74    pub epoch: Epoch,
75    pub custodian: String,
76}
77
78impl From<Lockup> for UiLockup {
79    fn from(lockup: Lockup) -> Self {
80        Self {
81            unix_timestamp: lockup.unix_timestamp,
82            epoch: lockup.epoch,
83            custodian: lockup.custodian.to_string(),
84        }
85    }
86}
87
88#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "camelCase")]
90pub struct UiAuthorized {
91    pub staker: String,
92    pub withdrawer: String,
93}
94
95impl From<Authorized> for UiAuthorized {
96    fn from(authorized: Authorized) -> Self {
97        Self {
98            staker: authorized.staker.to_string(),
99            withdrawer: authorized.withdrawer.to_string(),
100        }
101    }
102}
103
104#[derive(Debug, Serialize, Deserialize, PartialEq)]
105#[serde(rename_all = "camelCase")]
106pub struct UiStake {
107    pub delegation: UiDelegation,
108    pub credits_observed: u64,
109}
110
111impl From<Stake> for UiStake {
112    fn from(stake: Stake) -> Self {
113        Self {
114            delegation: stake.delegation.into(),
115            credits_observed: stake.credits_observed,
116        }
117    }
118}
119
120#[derive(Debug, Serialize, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub struct UiDelegation {
123    pub voter: String,
124    pub stake: StringAmount,
125    pub activation_epoch: StringAmount,
126    pub deactivation_epoch: StringAmount,
127}
128
129impl From<Delegation> for UiDelegation {
130    fn from(delegation: Delegation) -> Self {
131        #[allow(deprecated)]
132        Self {
133            voter: delegation.voter_pubkey.to_string(),
134            stake: delegation.stake.to_string(),
135            activation_epoch: delegation.activation_epoch.to_string(),
136            deactivation_epoch: delegation.deactivation_epoch.to_string(),
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                _reserved: [0; 8],
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                    },
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}