mpl_core/hooked/
mod.rs

1pub mod plugin;
2pub use plugin::*;
3
4pub mod advanced_types;
5pub use advanced_types::*;
6
7pub mod asset;
8pub use asset::*;
9
10pub mod collection;
11pub use collection::*;
12
13#[cfg(feature = "anchor")]
14use anchor_lang::prelude::{
15    AnchorDeserialize as CrateDeserialize, AnchorSerialize as CrateSerialize,
16};
17use base64::prelude::*;
18#[cfg(not(feature = "anchor"))]
19use borsh::{BorshDeserialize as CrateDeserialize, BorshSerialize as CrateSerialize};
20use modular_bitfield::{bitfield, specifiers::B29};
21use num_traits::FromPrimitive;
22use std::{cmp::Ordering, mem::size_of};
23
24use crate::{
25    accounts::{BaseAssetV1, BaseCollectionV1, PluginHeaderV1, PluginRegistryV1},
26    errors::MplCoreError,
27    types::{
28        ExternalCheckResult, ExternalPluginAdapterKey, ExternalPluginAdapterSchema,
29        ExternalPluginAdapterType, Key, Plugin, PluginType, RegistryRecord, UpdateAuthority,
30    },
31};
32use solana_program::account_info::AccountInfo;
33
34impl From<&Plugin> for PluginType {
35    fn from(plugin: &Plugin) -> Self {
36        match plugin {
37            Plugin::AddBlocker(_) => PluginType::AddBlocker,
38            Plugin::ImmutableMetadata(_) => PluginType::ImmutableMetadata,
39            Plugin::Royalties(_) => PluginType::Royalties,
40            Plugin::FreezeDelegate(_) => PluginType::FreezeDelegate,
41            Plugin::BurnDelegate(_) => PluginType::BurnDelegate,
42            Plugin::TransferDelegate(_) => PluginType::TransferDelegate,
43            Plugin::UpdateDelegate(_) => PluginType::UpdateDelegate,
44            Plugin::PermanentFreezeDelegate(_) => PluginType::PermanentFreezeDelegate,
45            Plugin::Attributes(_) => PluginType::Attributes,
46            Plugin::PermanentTransferDelegate(_) => PluginType::PermanentTransferDelegate,
47            Plugin::PermanentBurnDelegate(_) => PluginType::PermanentBurnDelegate,
48            Plugin::Edition(_) => PluginType::Edition,
49            Plugin::MasterEdition(_) => PluginType::MasterEdition,
50            Plugin::VerifiedCreators(_) => PluginType::VerifiedCreators,
51            Plugin::Autograph(_) => PluginType::Autograph,
52            Plugin::BubblegumV2(_) => PluginType::BubblegumV2,
53            Plugin::FreezeExecute(_) => PluginType::FreezeExecute,
54        }
55    }
56}
57
58impl BaseAssetV1 {
59    /// The base length of the asset account with an empty name and uri and no seq.
60    const BASE_LEN: usize = 1 // Key
61                            + 32 // Owner
62                            + 1 // Update Authority discriminator
63                            + 4 // Name length
64                            + 4 // URI length
65                            + 1; // Seq option
66}
67
68impl BaseCollectionV1 {
69    /// The base length of the collection account with an empty name and uri.
70    const BASE_LEN: usize = 1 // Key
71                            + 32 // Update Authority
72                            + 4 // Name Length
73                            + 4 // URI Length
74                            + 4 // num_minted
75                            + 4; // current_size
76}
77
78/// Anchor implementations that enable using `Account<BaseAssetV1>` and `Account<BaseCollectionV1>`
79/// in Anchor programs.
80#[cfg(feature = "anchor")]
81mod anchor_impl {
82    use super::*;
83    use anchor_lang::{
84        prelude::{Owner, Pubkey},
85        AccountDeserialize, AccountSerialize, Discriminator,
86    };
87
88    impl AccountDeserialize for BaseAssetV1 {
89        fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
90            let base_asset = Self::from_bytes(buf)?;
91            Ok(base_asset)
92        }
93    }
94
95    // Not used as an Anchor program using Account<BaseAssetV1> would not have permission to
96    // reserialize the account as it's owned by mpl-core.
97    impl AccountSerialize for BaseAssetV1 {}
98
99    // Not used but needed for Anchor.
100    impl Discriminator for BaseAssetV1 {
101        const DISCRIMINATOR: [u8; 8] = [0; 8];
102    }
103
104    impl Owner for BaseAssetV1 {
105        fn owner() -> Pubkey {
106            crate::ID
107        }
108    }
109
110    impl AccountDeserialize for BaseCollectionV1 {
111        fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
112            let base_asset = Self::from_bytes(buf)?;
113            Ok(base_asset)
114        }
115    }
116
117    // Not used as an Anchor program using Account<BaseCollectionV1> would not have permission to
118    // reserialize the account as it's owned by mpl-core.
119    impl AccountSerialize for BaseCollectionV1 {}
120
121    // Not used but needed for Anchor.
122    impl Discriminator for BaseCollectionV1 {
123        const DISCRIMINATOR: [u8; 8] = [0; 8];
124    }
125
126    impl Owner for BaseCollectionV1 {
127        fn owner() -> Pubkey {
128            crate::ID
129        }
130    }
131}
132
133impl DataBlob for BaseAssetV1 {
134    fn len(&self) -> usize {
135        let mut size = BaseAssetV1::BASE_LEN + self.name.len() + self.uri.len();
136
137        if let UpdateAuthority::Address(_) | UpdateAuthority::Collection(_) = self.update_authority
138        {
139            size += 32;
140        }
141
142        if self.seq.is_some() {
143            size += size_of::<u64>();
144        }
145        size
146    }
147}
148
149impl SolanaAccount for BaseAssetV1 {
150    fn key() -> Key {
151        Key::AssetV1
152    }
153}
154
155impl DataBlob for BaseCollectionV1 {
156    fn len(&self) -> usize {
157        Self::BASE_LEN + self.name.len() + self.uri.len()
158    }
159}
160
161impl SolanaAccount for BaseCollectionV1 {
162    fn key() -> Key {
163        Key::CollectionV1
164    }
165}
166
167impl SolanaAccount for PluginRegistryV1 {
168    fn key() -> Key {
169        Key::PluginRegistryV1
170    }
171}
172
173impl SolanaAccount for PluginHeaderV1 {
174    fn key() -> Key {
175        Key::PluginHeaderV1
176    }
177}
178
179impl Key {
180    /// Load the one byte key from a slice of data at the given offset.
181    pub fn from_slice(data: &[u8], offset: usize) -> Result<Self, std::io::Error> {
182        let key_byte = *data.get(offset).ok_or_else(|| {
183            std::io::Error::new(
184                std::io::ErrorKind::Other,
185                MplCoreError::DeserializationError.to_string(),
186            )
187        })?;
188
189        Self::from_u8(key_byte).ok_or_else(|| {
190            std::io::Error::new(
191                std::io::ErrorKind::Other,
192                MplCoreError::DeserializationError.to_string(),
193            )
194        })
195    }
196}
197
198/// Load the one byte key from the account data at the given offset.
199pub fn load_key(account: &AccountInfo, offset: usize) -> Result<Key, std::io::Error> {
200    let data = account.data.borrow();
201    Key::from_slice(&data, offset)
202}
203
204/// A trait for generic blobs of data that have size.
205#[allow(clippy::len_without_is_empty)]
206pub trait DataBlob: CrateSerialize + CrateDeserialize {
207    /// Get the current length of the data blob.
208    fn len(&self) -> usize;
209}
210
211/// A trait for Solana accounts.
212pub trait SolanaAccount: CrateSerialize + CrateDeserialize {
213    /// Get the discriminator key for the account.
214    fn key() -> Key;
215
216    /// Load the account from the given account info starting at the offset.
217    fn load(account: &AccountInfo, offset: usize) -> Result<Self, std::io::Error> {
218        let key = load_key(account, offset)?;
219
220        if key != Self::key() {
221            return Err(std::io::Error::new(
222                std::io::ErrorKind::Other,
223                MplCoreError::DeserializationError.to_string(),
224            ));
225        }
226
227        let mut bytes: &[u8] = &(*account.data).borrow()[offset..];
228        Self::deserialize(&mut bytes)
229    }
230
231    /// Save the account to the given account info starting at the offset.
232    fn save(&self, account: &AccountInfo, offset: usize) -> Result<(), std::io::Error> {
233        borsh::to_writer(&mut account.data.borrow_mut()[offset..], self)
234    }
235}
236
237impl RegistryRecord {
238    /// Associated function for sorting `RegistryRecords` by offset.
239    pub fn compare_offsets(a: &RegistryRecord, b: &RegistryRecord) -> Ordering {
240        a.offset.cmp(&b.offset)
241    }
242}
243
244/// Bitfield representation of lifecycle permissions for external plugin adapter, third party plugins.
245#[bitfield(bits = 32)]
246#[derive(Eq, PartialEq, Copy, Clone, Debug, Default)]
247pub struct ExternalCheckResultBits {
248    pub can_listen: bool,
249    pub can_approve: bool,
250    pub can_reject: bool,
251    pub empty_bits: B29,
252}
253
254impl From<ExternalCheckResult> for ExternalCheckResultBits {
255    fn from(check_result: ExternalCheckResult) -> Self {
256        ExternalCheckResultBits::from_bytes(check_result.flags.to_le_bytes())
257    }
258}
259
260impl From<ExternalCheckResultBits> for ExternalCheckResult {
261    fn from(bits: ExternalCheckResultBits) -> Self {
262        ExternalCheckResult {
263            flags: u32::from_le_bytes(bits.into_bytes()),
264        }
265    }
266}
267
268impl From<&ExternalPluginAdapterKey> for ExternalPluginAdapterType {
269    fn from(key: &ExternalPluginAdapterKey) -> Self {
270        match key {
271            ExternalPluginAdapterKey::LifecycleHook(_) => ExternalPluginAdapterType::LifecycleHook,
272            ExternalPluginAdapterKey::LinkedLifecycleHook(_) => {
273                ExternalPluginAdapterType::LinkedLifecycleHook
274            }
275            ExternalPluginAdapterKey::Oracle(_) => ExternalPluginAdapterType::Oracle,
276            ExternalPluginAdapterKey::AppData(_) => ExternalPluginAdapterType::AppData,
277            ExternalPluginAdapterKey::LinkedAppData(_) => ExternalPluginAdapterType::LinkedAppData,
278            ExternalPluginAdapterKey::DataSection(_) => ExternalPluginAdapterType::DataSection,
279        }
280    }
281}
282
283/// Use `ExternalPluginAdapterSchema` to convert data to string.  If schema is binary or there is
284/// an error, then use Base64 encoding.
285pub fn convert_external_plugin_adapter_data_to_string(
286    schema: &ExternalPluginAdapterSchema,
287    data_slice: &[u8],
288) -> String {
289    match schema {
290        ExternalPluginAdapterSchema::Binary => {
291            // Encode the binary data as a base64 string.
292            BASE64_STANDARD.encode(data_slice)
293        }
294        ExternalPluginAdapterSchema::Json => {
295            // Convert the byte slice to a UTF-8 string, replacing invalid characterse.
296            String::from_utf8_lossy(data_slice).to_string()
297        }
298        ExternalPluginAdapterSchema::MsgPack => {
299            // Attempt to decode `MsgPack` to serde_json::Value and serialize to JSON string.
300            match rmp_serde::decode::from_slice::<serde_json::Value>(data_slice) {
301                Ok(json_val) => serde_json::to_string(&json_val)
302                    .unwrap_or_else(|_| BASE64_STANDARD.encode(data_slice)),
303                Err(_) => {
304                    // Failed to decode `MsgPack`, fallback to base64.
305                    BASE64_STANDARD.encode(data_slice)
306                }
307            }
308        }
309    }
310}