Skip to main content

solana_account_decoder/
parse_vote.rs

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(), // <-- No `prior_voters` in v4
41        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/// A wrapper enum for consistency across programs
55#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "camelCase", tag = "type", content = "info")]
57pub enum VoteAccountType {
58    Vote(UiVoteState),
59}
60
61/// A duplicate representation of VoteState for pretty JSON serialization
62#[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    // Fields added with vote state v4 via SIMD-0185:
75    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    // Previously, the `votes` field on `UiVoteState` was a vector of
104    // `UiLockout`. If we changed the element type to `UiLandedVote` without
105    // flattening, the serialized JSON would have an extra nesting level.
106    #[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        // Verify that the lockout fields are flattened at the top level.
192        assert_eq!(json["latency"], 5);
193        assert_eq!(json["slot"], 12345);
194        assert_eq!(json["confirmationCount"], 10);
195
196        // Verify that there is no nested "lockout" field.
197        assert!(json.get("lockout").is_none());
198
199        // Now test the reverse.
200        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}