1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! Program state
use {
    borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
    solana_program::{
        clock::UnixTimestamp,
        msg,
        program_error::ProgramError,
        program_pack::{Pack, Sealed},
    },
};

/// Criteria for accepting a feature proposal
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)]
pub struct AcceptanceCriteria {
    /// The balance of the feature proposal's token account must be greater than
    /// this amount, and tallied before the deadline for the feature to be
    /// accepted.
    pub tokens_required: u64,

    /// If the required tokens are not tallied by this deadline then the
    /// proposal will expire.
    pub deadline: UnixTimestamp,
}

/// Contents of a Feature Proposal account
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)]
pub enum FeatureProposal {
    /// Default account state after creating it
    Uninitialized,
    /// Feature proposal is now pending
    Pending(AcceptanceCriteria),
    /// Feature proposal was accepted and the feature is now active
    Accepted {
        /// The balance of the feature proposal's token account at the time of
        /// activation.
        #[allow(dead_code)] // not dead code..
        tokens_upon_acceptance: u64,
    },
    /// Feature proposal was not accepted before the deadline
    Expired,
}
impl Sealed for FeatureProposal {}

impl Pack for FeatureProposal {
    const LEN: usize = 17; // see `test_get_packed_len()` for justification of "18"

    fn pack_into_slice(&self, dst: &mut [u8]) {
        let data = borsh::to_vec(&self).unwrap();
        dst[..data.len()].copy_from_slice(&data);
    }

    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
        let mut mut_src: &[u8] = src;
        Self::deserialize(&mut mut_src).map_err(|err| {
            msg!(
                "Error: failed to deserialize feature proposal account: {}",
                err
            );
            ProgramError::InvalidAccountData
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_packed_len() {
        assert_eq!(
            FeatureProposal::get_packed_len(),
            solana_program::borsh1::get_packed_len::<FeatureProposal>()
        );
    }

    #[test]
    fn test_serialize_bytes() {
        assert_eq!(borsh::to_vec(&FeatureProposal::Expired).unwrap(), vec![3]);

        assert_eq!(
            borsh::to_vec(&FeatureProposal::Pending(AcceptanceCriteria {
                tokens_required: 0xdeadbeefdeadbeef,
                deadline: -1,
            }))
            .unwrap(),
            vec![1, 239, 190, 173, 222, 239, 190, 173, 222, 255, 255, 255, 255, 255, 255, 255, 255],
        );
    }

    #[test]
    fn test_serialize_large_slice() {
        let mut dst = vec![0xff; 4];
        FeatureProposal::Expired.pack_into_slice(&mut dst);

        // Extra bytes (0xff) ignored
        assert_eq!(dst, vec![3, 0xff, 0xff, 0xff]);
    }

    #[test]
    fn state_deserialize_invalid() {
        assert_eq!(
            FeatureProposal::unpack_from_slice(&[3]),
            Ok(FeatureProposal::Expired),
        );

        // Extra bytes (0xff) ignored...
        assert_eq!(
            FeatureProposal::unpack_from_slice(&[3, 0xff, 0xff, 0xff]),
            Ok(FeatureProposal::Expired),
        );

        assert_eq!(
            FeatureProposal::unpack_from_slice(&[4]),
            Err(ProgramError::InvalidAccountData),
        );
    }
}