solana_account_decoder_wasm/
parse_stake.rs

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