mpl_token_auth_rules/state/mod.rs
1//! All structures and related functions representing a Rule Set on-chain.
2//!
3//! Key types include the main `RuleSetV1` type which keeps the the map of operations to `Rules`,
4//! `RuleSetV2` type which keep a list of `RuleV2`, as well as `RuleSetHeader` and `RuleSetRevisionMapV1`
5//! types used to manage data within the `RuleSet` PDA.
6//!
7//! Each time a `RuleSet` is updated, a new revision is added to the PDA, and previous revisions
8//! never deleted. The revision map is needed so that during `RuleSet` validation the desired
9//! revision can be selected by the user.
10//!
11//! Because the `RuleSet`s and the revision map are variable size, a fixed size header is stored
12//! at the beginning of the `RuleSet` PDA that allows new `RuleSets` and updated revision maps
13//! to be added to the PDA without moving the previous revision `RuleSets` and without losing the
14//! revision map's location.
15//!
16//! Also note there is a 1-byte version preceding each `RuleSetV1` revision (for `RuleSetV2` the
17//! first byte of the serialized rule set is the version) and the revision map.
18//!
19//! Approximate RuleSet PDA data layout
20//! ```text
21//! | Header | RuleSetV1 version | RuleSetV1 Revision 0 | RuleSetV1 version | RuleSetV1 Revision 1 | RuleSetV2 Revision 2 | ... | RuleSetRevisionMap version | RuleSetRevisionMap |
22//! |---------|-------------------|----------------------|-------------------|----------------------|----------------------|-----|----------------------------|--------------------|
23//! | 9 bytes | 1 byte | variable bytes | 1 byte | variable bytes | variable bytes | ... | 1 byte | variable bytes |
24//!
25//! When `RuleSetV2` revisions are added, there migh be the need to add padding bytes to align the data
26//! to 8 bytes.
27//! ```
28use borsh::{BorshDeserialize, BorshSerialize};
29use num_derive::FromPrimitive;
30use num_traits::FromPrimitive;
31use solana_program::{
32 account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
33};
34
35mod frequency;
36mod rule_set;
37mod rules;
38mod v2;
39
40pub use frequency::*;
41pub use rule_set::*;
42pub use rules::*;
43pub use v2::*;
44
45use crate::{error::RuleSetError, utils::assert_owned_by};
46
47/// The maximum size that can be allocated at one time for a PDA.
48pub const CHUNK_SIZE: usize = 10_000;
49
50#[repr(C)]
51#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy, FromPrimitive)]
52/// The key at the beginning of the serialized account that identifies the account type.
53/// NOTE: This is not used for the `RuleSet` account, which uses msgpack instead of Borsh for SerDes.
54pub enum Key {
55 /// An uninitialized account, which has all bytes set to zero by default.
56 Uninitialized,
57 /// An account containing a RuleSet.
58 RuleSet,
59 /// An account containing frequency state.
60 Frequency,
61}
62
63/// A trait implementing generic functions required by all accounts on Solana.
64pub trait SolanaAccount: BorshSerialize + BorshDeserialize {
65 /// Get the `Key` for this `Account`. This key is to be stored in the first byte of the
66 /// `Account` data.
67 fn key() -> Key;
68
69 /// BorshDeserialize the `AccountInfo` into the Rust data structure.
70 fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
71 let data = account
72 .data
73 .try_borrow()
74 .map_err(|_| ProgramError::AccountBorrowFailed)?;
75
76 if !Self::is_correct_account_type_and_size(&data, Self::key()) {
77 return Err(RuleSetError::DataTypeMismatch.into());
78 }
79
80 let data = Self::try_from_slice(&data)?;
81
82 // Check that this account is owned by this program.
83 assert_owned_by(account, &crate::ID)?;
84
85 Ok(data)
86 }
87
88 /// BorshSerialize the Rust data structure into the `Account` data.
89 fn to_account_data(&self, account: &AccountInfo) -> ProgramResult {
90 let mut data = account.try_borrow_mut_data()?;
91 self.serialize(&mut *data).map_err(Into::into)
92 }
93}
94
95trait PrivateSolanaAccountMethods: SolanaAccount {
96 const KEY_BYTE: usize = 0;
97
98 // Check the `Key` byte and the data size to determine if this data represents the correct
99 // account types.
100 fn is_correct_account_type_and_size(data: &[u8], data_type: Key) -> bool {
101 let key: Option<Key> = Key::from_u8(data[Self::KEY_BYTE]);
102 match key {
103 Some(key) => key == data_type,
104 None => false,
105 }
106 }
107}
108
109impl<T: SolanaAccount> PrivateSolanaAccountMethods for T {}