mpl_core/hooked/
mod.rs

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