gpl_feature_proposal/
instruction.rs

1//! Program instructions
2
3use crate::{state::AcceptanceCriteria, *};
4use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
5use gemachain_program::{
6    instruction::{AccountMeta, Instruction},
7    msg,
8    program_error::ProgramError,
9    program_pack::{Pack, Sealed},
10    pubkey::Pubkey,
11    sysvar,
12};
13
14/// Instructions supported by the Feature Proposal program
15#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)]
16pub enum FeatureProposalInstruction {
17    /// Propose a new feature.
18    ///
19    /// This instruction will create a variety of accounts to support the feature proposal, all
20    /// funded by account 0:
21    /// * A new token mint with a supply of `tokens_to_mint`, owned by the program and never
22    ///   modified again
23    /// * A new "distributor" token account that holds the total supply, owned by account 0.
24    /// * A new "acceptance" token account that holds 0 tokens, owned by the program.  Tokens
25    ///   transfers to this address are irrevocable and permanent.
26    /// * A new feature id account that has been funded and allocated (as described in
27    ///  `gemachain_program::feature`)
28    ///
29    /// On successful execution of the instruction, the feature proposer is expected to distribute
30    /// the tokens in the distributor token account out to all participating parties.
31    ///
32    /// Based on the provided acceptance criteria, if `AcceptanceCriteria::tokens_required`
33    /// tokens are transferred into the acceptance token account before
34    /// `AcceptanceCriteria::deadline` then the proposal is eligible to be accepted.
35    ///
36    /// The `FeatureProposalInstruction::Tally` instruction must be executed, by any party, to
37    /// complete the feature acceptance process.
38    ///
39    /// Accounts expected by this instruction:
40    ///
41    /// 0. `[writeable,signer]` Funding account (must be a system account)
42    /// 1. `[writeable,signer]` Unallocated feature proposal account to create
43    /// 2. `[writeable]` Token mint address from `get_mint_address`
44    /// 3. `[writeable]` Distributor token account address from `get_distributor_token_address`
45    /// 4. `[writeable]` Acceptance token account address from `get_acceptance_token_address`
46    /// 5. `[writeable]` Feature id account address from `get_feature_id_address`
47    /// 6. `[]` System program
48    /// 7. `[]` GPL Token program
49    /// 8. `[]` Rent sysvar
50    ///
51    Propose {
52        /// Total number of tokens to mint for this proposal
53        #[allow(dead_code)] // not dead code..
54        tokens_to_mint: u64,
55
56        /// Criteria for how this proposal may be activated
57        #[allow(dead_code)] // not dead code..
58        acceptance_criteria: AcceptanceCriteria,
59    },
60
61    /// `Tally` is a permission-less instruction to check the acceptance criteria for the feature
62    /// proposal, which may result in:
63    /// * No action
64    /// * Feature proposal acceptance
65    /// * Feature proposal expiration
66    ///
67    /// Accounts expected by this instruction:
68    ///
69    /// 0. `[writeable]` Feature proposal account
70    /// 1. `[]` Acceptance token account address from `get_acceptance_token_address`
71    /// 2. `[writeable]` Derived feature id account address from `get_feature_id_address`
72    /// 3. `[]` System program
73    /// 4. `[]` Clock sysvar
74    Tally,
75}
76
77impl Sealed for FeatureProposalInstruction {}
78impl Pack for FeatureProposalInstruction {
79    const LEN: usize = 25; // see `test_get_packed_len()` for justification of "18"
80
81    fn pack_into_slice(&self, dst: &mut [u8]) {
82        let data = self.pack_into_vec();
83        dst[..data.len()].copy_from_slice(&data);
84    }
85
86    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
87        let mut mut_src: &[u8] = src;
88        Self::deserialize(&mut mut_src).map_err(|err| {
89            msg!(
90                "Error: failed to deserialize feature proposal instruction: {}",
91                err
92            );
93            ProgramError::InvalidInstructionData
94        })
95    }
96}
97
98impl FeatureProposalInstruction {
99    fn pack_into_vec(&self) -> Vec<u8> {
100        self.try_to_vec().expect("try_to_vec")
101    }
102}
103
104/// Create a `FeatureProposalInstruction::Propose` instruction
105pub fn propose(
106    funding_address: &Pubkey,
107    feature_proposal_address: &Pubkey,
108    tokens_to_mint: u64,
109    acceptance_criteria: AcceptanceCriteria,
110) -> Instruction {
111    let mint_address = get_mint_address(feature_proposal_address);
112    let distributor_token_address = get_distributor_token_address(feature_proposal_address);
113    let acceptance_token_address = get_acceptance_token_address(feature_proposal_address);
114    let feature_id_address = get_feature_id_address(feature_proposal_address);
115
116    Instruction {
117        program_id: id(),
118        accounts: vec![
119            AccountMeta::new(*funding_address, true),
120            AccountMeta::new(*feature_proposal_address, true),
121            AccountMeta::new(mint_address, false),
122            AccountMeta::new(distributor_token_address, false),
123            AccountMeta::new(acceptance_token_address, false),
124            AccountMeta::new(feature_id_address, false),
125            AccountMeta::new_readonly(gemachain_program::system_program::id(), false),
126            AccountMeta::new_readonly(gpl_token::id(), false),
127            AccountMeta::new_readonly(sysvar::rent::id(), false),
128        ],
129        data: FeatureProposalInstruction::Propose {
130            tokens_to_mint,
131            acceptance_criteria,
132        }
133        .pack_into_vec(),
134    }
135}
136
137/// Create a `FeatureProposalInstruction::Tally` instruction
138pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
139    let acceptance_token_address = get_acceptance_token_address(feature_proposal_address);
140    let feature_id_address = get_feature_id_address(feature_proposal_address);
141
142    Instruction {
143        program_id: id(),
144        accounts: vec![
145            AccountMeta::new(*feature_proposal_address, false),
146            AccountMeta::new_readonly(acceptance_token_address, false),
147            AccountMeta::new(feature_id_address, false),
148            AccountMeta::new_readonly(gemachain_program::system_program::id(), false),
149            AccountMeta::new_readonly(sysvar::clock::id(), false),
150        ],
151        data: FeatureProposalInstruction::Tally.pack_into_vec(),
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_get_packed_len() {
161        assert_eq!(
162            FeatureProposalInstruction::get_packed_len(),
163            gemachain_program::borsh::get_packed_len::<FeatureProposalInstruction>()
164        )
165    }
166
167    #[test]
168    fn test_serialize_bytes() {
169        assert_eq!(
170            FeatureProposalInstruction::Tally.try_to_vec().unwrap(),
171            vec![1]
172        );
173
174        assert_eq!(
175            FeatureProposalInstruction::Propose {
176                tokens_to_mint: 42,
177                acceptance_criteria: AcceptanceCriteria {
178                    tokens_required: 0xdeadbeefdeadbeef,
179                    deadline: -1,
180                }
181            }
182            .try_to_vec()
183            .unwrap(),
184            vec![
185                0, 42, 0, 0, 0, 0, 0, 0, 0, 239, 190, 173, 222, 239, 190, 173, 222, 255, 255, 255,
186                255, 255, 255, 255, 255
187            ]
188        );
189    }
190
191    #[test]
192    fn test_serialize_large_slice() {
193        let mut dst = vec![0xff; 4];
194        FeatureProposalInstruction::Tally.pack_into_slice(&mut dst);
195
196        // Extra bytes (0xff) ignored
197        assert_eq!(dst, vec![1, 0xff, 0xff, 0xff]);
198    }
199
200    #[test]
201    fn state_deserialize_invalid() {
202        assert_eq!(
203            FeatureProposalInstruction::unpack_from_slice(&[1]),
204            Ok(FeatureProposalInstruction::Tally),
205        );
206
207        // Extra bytes (0xff) ignored...
208        assert_eq!(
209            FeatureProposalInstruction::unpack_from_slice(&[1, 0xff, 0xff, 0xff]),
210            Ok(FeatureProposalInstruction::Tally),
211        );
212
213        assert_eq!(
214            FeatureProposalInstruction::unpack_from_slice(&[2]),
215            Err(ProgramError::InvalidInstructionData),
216        );
217    }
218}