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}