solana_account_decoder/
parse_vote.rs

1use {
2    crate::{parse_account_data::ParseAccountError, StringAmount},
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.inflation_rewards_commission_bps / 100) as u8,
34        votes,
35        root_slot: vote_state.root_slot,
36        authorized_voters,
37        prior_voters: Vec::new(), // <-- No `prior_voters` in v4
38        epoch_credits,
39        last_timestamp: vote_state.last_timestamp,
40        inflation_rewards_commission_bps: vote_state.inflation_rewards_commission_bps,
41        inflation_rewards_collector: vote_state.inflation_rewards_collector.to_string(),
42        block_revenue_collector: vote_state.block_revenue_collector.to_string(),
43        block_revenue_commission_bps: vote_state.block_revenue_commission_bps,
44        pending_delegator_rewards: vote_state.pending_delegator_rewards.to_string(),
45        bls_pubkey_compressed: vote_state
46            .bls_pubkey_compressed
47            .map(|bytes| bs58::encode(bytes).into_string()),
48    }))
49}
50
51/// A wrapper enum for consistency across programs
52#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "camelCase", tag = "type", content = "info")]
54pub enum VoteAccountType {
55    Vote(UiVoteState),
56}
57
58/// A duplicate representation of VoteState for pretty JSON serialization
59#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
60#[serde(rename_all = "camelCase")]
61pub struct UiVoteState {
62    node_pubkey: String,
63    authorized_withdrawer: String,
64    commission: u8,
65    votes: Vec<UiLandedVote>,
66    root_slot: Option<Slot>,
67    authorized_voters: Vec<UiAuthorizedVoters>,
68    prior_voters: Vec<UiPriorVoters>,
69    epoch_credits: Vec<UiEpochCredits>,
70    last_timestamp: BlockTimestamp,
71    // Fields added with vote state v4 via SIMD-0185:
72    inflation_rewards_commission_bps: u16,
73    inflation_rewards_collector: String,
74    block_revenue_collector: String,
75    block_revenue_commission_bps: u16,
76    pending_delegator_rewards: StringAmount,
77    bls_pubkey_compressed: Option<String>,
78}
79
80#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
81#[serde(rename_all = "camelCase")]
82struct UiLockout {
83    slot: Slot,
84    confirmation_count: u32,
85}
86
87impl From<&Lockout> for UiLockout {
88    fn from(lockout: &Lockout) -> Self {
89        Self {
90            slot: lockout.slot(),
91            confirmation_count: lockout.confirmation_count(),
92        }
93    }
94}
95
96#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
97#[serde(rename_all = "camelCase")]
98struct UiLandedVote {
99    latency: u8,
100    // Previously, the `votes` field on `UiVoteState` was a vector of
101    // `UiLockout`. If we changed the element type to `UiLandedVote` without
102    // flattening, the serialized JSON would have an extra nesting level.
103    #[serde(flatten)]
104    lockout: UiLockout,
105}
106
107impl From<&LandedVote> for UiLandedVote {
108    fn from(landed_vote: &LandedVote) -> Self {
109        Self {
110            latency: landed_vote.latency,
111            lockout: UiLockout::from(&landed_vote.lockout),
112        }
113    }
114}
115
116#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "camelCase")]
118struct UiAuthorizedVoters {
119    epoch: Epoch,
120    authorized_voter: String,
121}
122
123#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
124#[serde(rename_all = "camelCase")]
125struct UiPriorVoters {
126    authorized_pubkey: String,
127    epoch_of_last_authorized_switch: Epoch,
128    target_epoch: Epoch,
129}
130
131#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
132#[serde(rename_all = "camelCase")]
133struct UiEpochCredits {
134    epoch: Epoch,
135    credits: StringAmount,
136    previous_credits: StringAmount,
137}
138
139#[cfg(test)]
140mod test {
141    use {super::*, solana_vote_interface::state::VoteStateVersions};
142
143    #[test]
144    fn test_parse_vote() {
145        let vote_pubkey = Pubkey::new_unique();
146        let vote_state = VoteStateV4::default();
147        let mut vote_account_data: Vec<u8> = vec![0; VoteStateV4::size_of()];
148        let versioned = VoteStateVersions::new_v4(vote_state.clone());
149        VoteStateV4::serialize(&versioned, &mut vote_account_data).unwrap();
150        let expected_vote_state = UiVoteState {
151            node_pubkey: Pubkey::default().to_string(),
152            authorized_withdrawer: Pubkey::default().to_string(),
153            commission: 0,
154            votes: vec![],
155            root_slot: None,
156            authorized_voters: vec![],
157            prior_voters: vec![],
158            epoch_credits: vec![],
159            last_timestamp: BlockTimestamp::default(),
160            inflation_rewards_commission_bps: vote_state.inflation_rewards_commission_bps,
161            inflation_rewards_collector: vote_state.inflation_rewards_collector.to_string(),
162            block_revenue_collector: vote_state.block_revenue_collector.to_string(),
163            block_revenue_commission_bps: vote_state.block_revenue_commission_bps,
164            pending_delegator_rewards: vote_state.pending_delegator_rewards.to_string(),
165            bls_pubkey_compressed: None,
166        };
167        assert_eq!(
168            parse_vote(&vote_account_data, &vote_pubkey).unwrap(),
169            VoteAccountType::Vote(expected_vote_state)
170        );
171
172        let bad_data = vec![0; 4];
173        assert!(parse_vote(&bad_data, &vote_pubkey).is_err());
174    }
175
176    #[test]
177    fn test_ui_landed_vote_flatten() {
178        let ui_landed_vote = UiLandedVote {
179            latency: 5,
180            lockout: UiLockout {
181                slot: 12345,
182                confirmation_count: 10,
183            },
184        };
185
186        let json = serde_json::to_value(&ui_landed_vote).unwrap();
187
188        // Verify that the lockout fields are flattened at the top level.
189        assert_eq!(json["latency"], 5);
190        assert_eq!(json["slot"], 12345);
191        assert_eq!(json["confirmationCount"], 10);
192
193        // Verify that there is no nested "lockout" field.
194        assert!(json.get("lockout").is_none());
195
196        // Now test the reverse.
197        let json_str = r#"{"latency": 5, "slot": 12345, "confirmationCount": 10}"#;
198        let deserialized: UiLandedVote = serde_json::from_str(json_str).unwrap();
199        assert_eq!(deserialized, ui_landed_vote);
200    }
201}