1use {
2 crate::{StringAmount, parse_account_data::ParseAccountError},
3 serde::{Deserialize, Serialize},
4 solana_clock::{Epoch, Slot},
5 solana_pubkey::Pubkey,
6 solana_vote_interface::state::{BlockTimestamp, LandedVote, Lockout, VoteStateV4},
7};
8
9pub fn parse_vote(data: &[u8], vote_pubkey: &Pubkey) -> Result<VoteAccountType, ParseAccountError> {
10 let vote_state =
11 VoteStateV4::deserialize(data, vote_pubkey).map_err(ParseAccountError::from)?;
12 let epoch_credits = vote_state
13 .epoch_credits
14 .iter()
15 .map(|(epoch, credits, previous_credits)| UiEpochCredits {
16 epoch: *epoch,
17 credits: credits.to_string(),
18 previous_credits: previous_credits.to_string(),
19 })
20 .collect();
21 let votes = vote_state.votes.iter().map(UiLandedVote::from).collect();
22 let authorized_voters = vote_state
23 .authorized_voters
24 .iter()
25 .map(|(epoch, authorized_voter)| UiAuthorizedVoters {
26 epoch: *epoch,
27 authorized_voter: authorized_voter.to_string(),
28 })
29 .collect();
30 Ok(VoteAccountType::Vote(UiVoteState {
31 node_pubkey: vote_state.node_pubkey.to_string(),
32 authorized_withdrawer: vote_state.authorized_withdrawer.to_string(),
33 commission: vote_state
34 .inflation_rewards_commission_bps
35 .div_ceil(100)
36 .min(u8::MAX as u16) as u8,
37 votes,
38 root_slot: vote_state.root_slot,
39 authorized_voters,
40 prior_voters: Vec::new(), epoch_credits,
42 last_timestamp: vote_state.last_timestamp,
43 inflation_rewards_commission_bps: vote_state.inflation_rewards_commission_bps,
44 inflation_rewards_collector: vote_state.inflation_rewards_collector.to_string(),
45 block_revenue_collector: vote_state.block_revenue_collector.to_string(),
46 block_revenue_commission_bps: vote_state.block_revenue_commission_bps,
47 pending_delegator_rewards: vote_state.pending_delegator_rewards.to_string(),
48 bls_pubkey_compressed: vote_state
49 .bls_pubkey_compressed
50 .map(|bytes| bs58::encode(bytes).into_string()),
51 }))
52}
53
54#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "camelCase", tag = "type", content = "info")]
57pub enum VoteAccountType {
58 Vote(UiVoteState),
59}
60
61#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
63#[serde(rename_all = "camelCase")]
64pub struct UiVoteState {
65 node_pubkey: String,
66 authorized_withdrawer: String,
67 commission: u8,
68 votes: Vec<UiLandedVote>,
69 root_slot: Option<Slot>,
70 authorized_voters: Vec<UiAuthorizedVoters>,
71 prior_voters: Vec<UiPriorVoters>,
72 epoch_credits: Vec<UiEpochCredits>,
73 last_timestamp: BlockTimestamp,
74 inflation_rewards_commission_bps: u16,
76 inflation_rewards_collector: String,
77 block_revenue_collector: String,
78 block_revenue_commission_bps: u16,
79 pending_delegator_rewards: StringAmount,
80 bls_pubkey_compressed: Option<String>,
81}
82
83#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(rename_all = "camelCase")]
85struct UiLockout {
86 slot: Slot,
87 confirmation_count: u32,
88}
89
90impl From<&Lockout> for UiLockout {
91 fn from(lockout: &Lockout) -> Self {
92 Self {
93 slot: lockout.slot(),
94 confirmation_count: lockout.confirmation_count(),
95 }
96 }
97}
98
99#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
100#[serde(rename_all = "camelCase")]
101struct UiLandedVote {
102 latency: u8,
103 #[serde(flatten)]
107 lockout: UiLockout,
108}
109
110impl From<&LandedVote> for UiLandedVote {
111 fn from(landed_vote: &LandedVote) -> Self {
112 Self {
113 latency: landed_vote.latency,
114 lockout: UiLockout::from(&landed_vote.lockout),
115 }
116 }
117}
118
119#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
120#[serde(rename_all = "camelCase")]
121struct UiAuthorizedVoters {
122 epoch: Epoch,
123 authorized_voter: String,
124}
125
126#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
127#[serde(rename_all = "camelCase")]
128struct UiPriorVoters {
129 authorized_pubkey: String,
130 epoch_of_last_authorized_switch: Epoch,
131 target_epoch: Epoch,
132}
133
134#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
135#[serde(rename_all = "camelCase")]
136struct UiEpochCredits {
137 epoch: Epoch,
138 credits: StringAmount,
139 previous_credits: StringAmount,
140}
141
142#[cfg(test)]
143mod test {
144 use {super::*, solana_vote_interface::state::VoteStateVersions};
145
146 #[test]
147 fn test_parse_vote() {
148 let vote_pubkey = Pubkey::new_unique();
149 let vote_state = VoteStateV4::default();
150 let mut vote_account_data: Vec<u8> = vec![0; VoteStateV4::size_of()];
151 let versioned = VoteStateVersions::new_v4(vote_state.clone());
152 VoteStateV4::serialize(&versioned, &mut vote_account_data).unwrap();
153 let expected_vote_state = UiVoteState {
154 node_pubkey: Pubkey::default().to_string(),
155 authorized_withdrawer: Pubkey::default().to_string(),
156 commission: 0,
157 votes: vec![],
158 root_slot: None,
159 authorized_voters: vec![],
160 prior_voters: vec![],
161 epoch_credits: vec![],
162 last_timestamp: BlockTimestamp::default(),
163 inflation_rewards_commission_bps: vote_state.inflation_rewards_commission_bps,
164 inflation_rewards_collector: vote_state.inflation_rewards_collector.to_string(),
165 block_revenue_collector: vote_state.block_revenue_collector.to_string(),
166 block_revenue_commission_bps: vote_state.block_revenue_commission_bps,
167 pending_delegator_rewards: vote_state.pending_delegator_rewards.to_string(),
168 bls_pubkey_compressed: None,
169 };
170 assert_eq!(
171 parse_vote(&vote_account_data, &vote_pubkey).unwrap(),
172 VoteAccountType::Vote(expected_vote_state)
173 );
174
175 let bad_data = vec![0; 4];
176 assert!(parse_vote(&bad_data, &vote_pubkey).is_err());
177 }
178
179 #[test]
180 fn test_ui_landed_vote_flatten() {
181 let ui_landed_vote = UiLandedVote {
182 latency: 5,
183 lockout: UiLockout {
184 slot: 12345,
185 confirmation_count: 10,
186 },
187 };
188
189 let json = serde_json::to_value(&ui_landed_vote).unwrap();
190
191 assert_eq!(json["latency"], 5);
193 assert_eq!(json["slot"], 12345);
194 assert_eq!(json["confirmationCount"], 10);
195
196 assert!(json.get("lockout").is_none());
198
199 let json_str = r#"{"latency": 5, "slot": 12345, "confirmationCount": 10}"#;
201 let deserialized: UiLandedVote = serde_json::from_str(json_str).unwrap();
202 assert_eq!(deserialized, ui_landed_vote);
203 }
204}