|
| 1 | +//! Program instructions |
| 2 | +
|
| 3 | +use crate::{state::AcceptanceCriteria, *}; |
| 4 | +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; |
| 5 | +use solana_program::{ |
| 6 | + info, |
| 7 | + instruction::{AccountMeta, Instruction}, |
| 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)] |
| 16 | +pub 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 "delivery" 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 | + /// `solana_program::feature`) |
| 28 | + /// |
| 29 | + /// On successful execution of the instruction, the feature proposer is expected to distribute |
| 30 | + /// the tokens in the delivery 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]` Delivery token account address from `get_delivery_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. `[]` SPL 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 | + |
| 77 | +impl Sealed for FeatureProposalInstruction {} |
| 78 | +impl Pack for FeatureProposalInstruction { |
| 79 | + const LEN: usize = 26; // 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 | + info!(&format!( |
| 90 | + "Error: failed to deserialize feature proposal instruction: {}", |
| 91 | + err |
| 92 | + )); |
| 93 | + ProgramError::InvalidInstructionData |
| 94 | + }) |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +impl 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 |
| 105 | +pub 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 delivery_token_address = get_delivery_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(delivery_token_address, false), |
| 123 | + AccountMeta::new(acceptance_token_address, false), |
| 124 | + AccountMeta::new(feature_id_address, false), |
| 125 | + AccountMeta::new_readonly(solana_program::system_program::id(), false), |
| 126 | + AccountMeta::new_readonly(spl_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 |
| 138 | +pub 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(solana_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)] |
| 156 | +mod tests { |
| 157 | + use super::*; |
| 158 | + use crate::borsh_utils; |
| 159 | + |
| 160 | + #[test] |
| 161 | + fn test_get_packed_len() { |
| 162 | + assert_eq!( |
| 163 | + FeatureProposalInstruction::get_packed_len(), |
| 164 | + borsh_utils::get_packed_len::<FeatureProposalInstruction>() |
| 165 | + ) |
| 166 | + } |
| 167 | + |
| 168 | + #[test] |
| 169 | + fn test_serialize_bytes() { |
| 170 | + assert_eq!( |
| 171 | + FeatureProposalInstruction::Tally.try_to_vec().unwrap(), |
| 172 | + vec![1] |
| 173 | + ); |
| 174 | + |
| 175 | + assert_eq!( |
| 176 | + FeatureProposalInstruction::Propose { |
| 177 | + tokens_to_mint: 42, |
| 178 | + acceptance_criteria: AcceptanceCriteria { |
| 179 | + tokens_required: 0xdeadbeefdeadbeef, |
| 180 | + deadline: None, |
| 181 | + } |
| 182 | + } |
| 183 | + .try_to_vec() |
| 184 | + .unwrap(), |
| 185 | + vec![0, 42, 0, 0, 0, 0, 0, 0, 0, 239, 190, 173, 222, 239, 190, 173, 222, 0] |
| 186 | + ); |
| 187 | + } |
| 188 | + |
| 189 | + #[test] |
| 190 | + fn test_serialize_large_slice() { |
| 191 | + let mut dst = vec![0xff; 4]; |
| 192 | + FeatureProposalInstruction::Tally.pack_into_slice(&mut dst); |
| 193 | + |
| 194 | + // Extra bytes (0xff) ignored |
| 195 | + assert_eq!(dst, vec![1, 0xff, 0xff, 0xff]); |
| 196 | + } |
| 197 | + |
| 198 | + #[test] |
| 199 | + fn state_deserialize_invalid() { |
| 200 | + assert_eq!( |
| 201 | + FeatureProposalInstruction::unpack_from_slice(&[1]), |
| 202 | + Ok(FeatureProposalInstruction::Tally), |
| 203 | + ); |
| 204 | + |
| 205 | + // Extra bytes (0xff) ignored... |
| 206 | + assert_eq!( |
| 207 | + FeatureProposalInstruction::unpack_from_slice(&[1, 0xff, 0xff, 0xff]), |
| 208 | + Ok(FeatureProposalInstruction::Tally), |
| 209 | + ); |
| 210 | + |
| 211 | + assert_eq!( |
| 212 | + FeatureProposalInstruction::unpack_from_slice(&[2]), |
| 213 | + Err(ProgramError::InvalidInstructionData), |
| 214 | + ); |
| 215 | + } |
| 216 | +} |
0 commit comments