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