mpl_core/
indexable_asset.rs

1#![warn(clippy::indexing_slicing)]
2#[cfg(feature = "anchor")]
3use anchor_lang::prelude::AnchorDeserialize;
4use base64::prelude::*;
5#[cfg(not(feature = "anchor"))]
6use borsh::BorshDeserialize;
7use num_traits::FromPrimitive;
8use solana_program::pubkey::Pubkey;
9use std::{cmp::Ordering, collections::HashMap, io::ErrorKind};
10
11use crate::{
12    accounts::{BaseAssetV1, BaseCollectionV1, PluginHeaderV1},
13    convert_external_plugin_adapter_data_to_string,
14    types::{
15        ExternalCheckResult, ExternalPluginAdapter, ExternalPluginAdapterSchema,
16        ExternalPluginAdapterType, HookableLifecycleEvent, Key, Plugin, PluginAuthority,
17        PluginType, UpdateAuthority,
18    },
19    DataBlob, ExternalCheckResultBits, ExternalRegistryRecordSafe, PluginRegistryV1Safe,
20    RegistryRecordSafe,
21};
22
23/// Schema used for indexing known plugin types.
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct IndexablePluginSchemaV1 {
27    pub index: u64,
28    pub offset: u64,
29    pub authority: PluginAuthority,
30    pub data: Plugin,
31}
32
33/// Schema used for indexing unknown plugin types, storing the plugin as raw data.
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct IndexableUnknownPluginSchemaV1 {
37    pub index: u64,
38    pub offset: u64,
39    pub authority: PluginAuthority,
40    pub r#type: u8,
41    pub data: String,
42}
43
44#[cfg(feature = "serde")]
45mod custom_serde {
46    use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
47    use serde_json::Value as JsonValue;
48
49    pub fn serialize<S>(data: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
50    where
51        S: Serializer,
52    {
53        match data {
54            Some(s) => {
55                if let Ok(json_value) = serde_json::from_str::<JsonValue>(s) {
56                    json_value.serialize(serializer)
57                } else {
58                    serializer.serialize_str(s)
59                }
60            }
61            None => serializer.serialize_none(),
62        }
63    }
64
65    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        let json_value: Option<JsonValue> = Option::deserialize(deserializer)?;
70        match json_value {
71            Some(JsonValue::String(s)) => Ok(Some(s)),
72            Some(json_value) => Ok(Some(json_value.to_string())),
73            None => Ok(None),
74        }
75    }
76}
77
78/// Schema used for indexing known external plugin types.
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct IndexableExternalPluginSchemaV1 {
82    pub index: u64,
83    pub offset: u64,
84    pub authority: PluginAuthority,
85    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
86    pub lifecycle_checks: Option<LifecycleChecks>,
87    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
88    pub unknown_lifecycle_checks: Option<Vec<(u8, Vec<IndexableCheckResult>)>>,
89    pub r#type: ExternalPluginAdapterType,
90    pub adapter_config: ExternalPluginAdapter,
91    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
92    pub data_offset: Option<u64>,
93    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
94    pub data_len: Option<u64>,
95    #[cfg_attr(
96        feature = "serde",
97        serde(skip_serializing_if = "Option::is_none", with = "custom_serde")
98    )]
99    pub data: Option<String>,
100}
101
102/// Schema used for indexing unknown external plugin types, storing the plugin as raw data.
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104#[derive(Clone, Debug, Eq, PartialEq)]
105pub struct IndexableUnknownExternalPluginSchemaV1 {
106    pub index: u64,
107    pub offset: u64,
108    pub authority: PluginAuthority,
109    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
110    pub lifecycle_checks: Option<LifecycleChecks>,
111    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
112    pub unknown_lifecycle_checks: Option<Vec<(u8, Vec<IndexableCheckResult>)>>,
113    pub r#type: u8,
114    pub unknown_adapter_config: String,
115    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
116    pub data_offset: Option<u64>,
117    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
118    pub data_len: Option<u64>,
119    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
120    pub data: Option<String>,
121}
122
123// Type used to store a plugin that was processed and is either a known or unknown plugin type.
124// Used when building an `IndexableAsset`.
125#[derive(Clone, Debug, Eq, PartialEq)]
126enum ProcessedPlugin {
127    Known((PluginType, IndexablePluginSchemaV1)),
128    Unknown(IndexableUnknownPluginSchemaV1),
129}
130
131impl ProcessedPlugin {
132    fn from_data(
133        index: u64,
134        offset: u64,
135        authority: PluginAuthority,
136        plugin_type: u8,
137        plugin_slice: &mut &[u8],
138    ) -> Result<Self, std::io::Error> {
139        let processed_plugin = if let Some(known_plugin_type) = PluginType::from_u8(plugin_type) {
140            let data = Plugin::deserialize(plugin_slice)?;
141            let indexable_plugin_schema = IndexablePluginSchemaV1 {
142                index,
143                offset,
144                authority,
145                data,
146            };
147            ProcessedPlugin::Known((known_plugin_type, indexable_plugin_schema))
148        } else {
149            let encoded: String = BASE64_STANDARD.encode(plugin_slice);
150            ProcessedPlugin::Unknown(IndexableUnknownPluginSchemaV1 {
151                index,
152                offset,
153                authority,
154                r#type: plugin_type,
155                data: encoded,
156            })
157        };
158
159        Ok(processed_plugin)
160    }
161}
162
163// Type used to store an external plugin that was processed and is either a known or unknown plugin
164// type.  Used when building an `IndexableAsset`.
165#[derive(Clone, Debug, Eq, PartialEq)]
166enum ProcessedExternalPlugin {
167    Known(IndexableExternalPluginSchemaV1),
168    Unknown(IndexableUnknownExternalPluginSchemaV1),
169}
170
171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172#[derive(Clone, Debug, Eq, PartialEq, Default)]
173pub struct LifecycleChecks {
174    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
175    pub create: Vec<IndexableCheckResult>,
176    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
177    pub update: Vec<IndexableCheckResult>,
178    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
179    pub transfer: Vec<IndexableCheckResult>,
180    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
181    pub burn: Vec<IndexableCheckResult>,
182}
183
184impl LifecycleChecks {
185    pub fn is_all_empty(&self) -> bool {
186        self.create.is_empty()
187            && self.update.is_empty()
188            && self.transfer.is_empty()
189            && self.burn.is_empty()
190    }
191}
192
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194#[derive(Clone, Debug, Eq, PartialEq)]
195pub enum IndexableCheckResult {
196    CanListen,
197    CanApprove,
198    CanReject,
199}
200
201impl From<ExternalCheckResult> for Vec<IndexableCheckResult> {
202    fn from(check_result: ExternalCheckResult) -> Self {
203        let check_result_bits = ExternalCheckResultBits::from(check_result);
204        let mut check_result_vec = vec![];
205        if check_result_bits.can_listen() {
206            check_result_vec.push(IndexableCheckResult::CanListen);
207        }
208        if check_result_bits.can_approve() {
209            check_result_vec.push(IndexableCheckResult::CanApprove);
210        }
211        if check_result_bits.can_reject() {
212            check_result_vec.push(IndexableCheckResult::CanReject);
213        }
214        check_result_vec
215    }
216}
217
218struct ExternalPluginDataInfo<'a> {
219    data_offset: u64,
220    data_len: u64,
221    data_slice: &'a [u8],
222}
223
224impl ProcessedExternalPlugin {
225    fn from_data(
226        index: u64,
227        offset: u64,
228        authority: PluginAuthority,
229        lifecycle_checks: Option<Vec<(u8, ExternalCheckResult)>>,
230        external_plugin_adapter_type: u8,
231        plugin_slice: &mut &[u8],
232        external_plugin_data_info: Option<ExternalPluginDataInfo>,
233    ) -> Result<Self, std::io::Error> {
234        // First process the lifecycle checks.
235        let mut known_lifecycle_checks = LifecycleChecks::default();
236        let mut unknown_lifecycle_checks = vec![];
237
238        if let Some(checks) = lifecycle_checks {
239            for (event, check) in checks {
240                let checks = Vec::<IndexableCheckResult>::from(check);
241                match HookableLifecycleEvent::from_u8(event) {
242                    Some(val) => match val {
243                        HookableLifecycleEvent::Create => known_lifecycle_checks.create = checks,
244                        HookableLifecycleEvent::Update => known_lifecycle_checks.update = checks,
245                        HookableLifecycleEvent::Transfer => {
246                            known_lifecycle_checks.transfer = checks
247                        }
248                        HookableLifecycleEvent::Burn => known_lifecycle_checks.burn = checks,
249                    },
250                    None => unknown_lifecycle_checks.push((event, checks)),
251                }
252            }
253        }
254
255        // Save them as known and unknown.
256        let known_lifecycle_checks =
257            (!known_lifecycle_checks.is_all_empty()).then_some(known_lifecycle_checks);
258        let unknown_lifecycle_checks =
259            (!unknown_lifecycle_checks.is_empty()).then_some(unknown_lifecycle_checks);
260
261        // Next, process the external plugin adapter and save them as known and unknown variants.
262        let processed_plugin = if let Some(r#type) =
263            ExternalPluginAdapterType::from_u8(external_plugin_adapter_type)
264        {
265            let adapter_config = ExternalPluginAdapter::deserialize(plugin_slice)?;
266            let (data_offset, data_len, data) = match external_plugin_data_info {
267                Some(data_info) => {
268                    let schema = match &adapter_config {
269                        ExternalPluginAdapter::LifecycleHook(lc_hook) => &lc_hook.schema,
270                        ExternalPluginAdapter::AppData(app_data) => &app_data.schema,
271                        ExternalPluginAdapter::LinkedLifecycleHook(l_lc_hook) => &l_lc_hook.schema,
272                        ExternalPluginAdapter::LinkedAppData(l_app_data) => &l_app_data.schema,
273                        ExternalPluginAdapter::DataSection(data_section) => &data_section.schema,
274                        // Assume binary for `Oracle`, but this should never happen.
275                        ExternalPluginAdapter::Oracle(_) => &ExternalPluginAdapterSchema::Binary,
276                    };
277
278                    (
279                        Some(data_info.data_offset),
280                        Some(data_info.data_len),
281                        Some(convert_external_plugin_adapter_data_to_string(
282                            schema,
283                            data_info.data_slice,
284                        )),
285                    )
286                }
287                None => (None, None, None),
288            };
289
290            let indexable_plugin_schema = IndexableExternalPluginSchemaV1 {
291                index,
292                offset,
293                authority,
294                lifecycle_checks: known_lifecycle_checks,
295                unknown_lifecycle_checks,
296                r#type,
297                adapter_config,
298                data_offset,
299                data_len,
300                data,
301            };
302            ProcessedExternalPlugin::Known(indexable_plugin_schema)
303        } else {
304            let encoded: String = BASE64_STANDARD.encode(plugin_slice);
305
306            let (data_offset, data_len, data) = match external_plugin_data_info {
307                Some(data_info) => (
308                    Some(data_info.data_offset),
309                    Some(data_info.data_len),
310                    // We don't know the schema so use base64.
311                    Some(BASE64_STANDARD.encode(data_info.data_slice)),
312                ),
313                None => (None, None, None),
314            };
315
316            ProcessedExternalPlugin::Unknown(IndexableUnknownExternalPluginSchemaV1 {
317                index,
318                offset,
319                authority,
320                lifecycle_checks: known_lifecycle_checks,
321                unknown_lifecycle_checks,
322                r#type: external_plugin_adapter_type,
323                unknown_adapter_config: encoded,
324                data_offset,
325                data_len,
326                data,
327            })
328        };
329
330        Ok(processed_plugin)
331    }
332}
333
334/// A type used to store both Core Assets and Core Collections for indexing.
335#[derive(Clone, Debug, Eq, PartialEq)]
336#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
337pub struct IndexableAsset {
338    pub owner: Option<Pubkey>,
339    pub update_authority: UpdateAuthority,
340    pub name: String,
341    pub uri: String,
342    pub seq: u64,
343    pub num_minted: Option<u32>,
344    pub current_size: Option<u32>,
345    pub plugins: HashMap<PluginType, IndexablePluginSchemaV1>,
346    pub unknown_plugins: Vec<IndexableUnknownPluginSchemaV1>,
347    pub external_plugins: Vec<IndexableExternalPluginSchemaV1>,
348    pub unknown_external_plugins: Vec<IndexableUnknownExternalPluginSchemaV1>,
349}
350
351enum CombinedRecord<'a> {
352    Internal(&'a RegistryRecordSafe),
353    External(&'a ExternalRegistryRecordSafe),
354}
355
356struct CombinedRecordWithDataInfo<'a> {
357    pub offset: u64,
358    pub data_offset: Option<u64>,
359    pub record: CombinedRecord<'a>,
360}
361
362impl<'a> CombinedRecordWithDataInfo<'a> {
363    // Associated function for sorting `RegistryRecordIndexable` by offset.
364    pub fn compare_offsets(
365        a: &CombinedRecordWithDataInfo,
366        b: &CombinedRecordWithDataInfo,
367    ) -> Ordering {
368        a.offset.cmp(&b.offset)
369    }
370}
371
372impl IndexableAsset {
373    /// Create a new `IndexableAsset` from a `BaseAssetV1``.  Note this uses a passed-in `seq` rather than
374    /// the one contained in `asset` to avoid errors.
375    pub fn from_asset(asset: BaseAssetV1, seq: u64) -> Self {
376        Self {
377            owner: Some(asset.owner),
378            update_authority: asset.update_authority,
379            name: asset.name,
380            uri: asset.uri,
381            seq,
382            num_minted: None,
383            current_size: None,
384            plugins: HashMap::new(),
385            unknown_plugins: vec![],
386            external_plugins: vec![],
387            unknown_external_plugins: vec![],
388        }
389    }
390
391    /// Create a new `IndexableAsset` from a `BaseCollectionV1`.
392    pub fn from_collection(collection: BaseCollectionV1) -> Self {
393        Self {
394            owner: None,
395            update_authority: UpdateAuthority::Address(collection.update_authority),
396            name: collection.name,
397            uri: collection.uri,
398            seq: 0,
399            num_minted: Some(collection.num_minted),
400            current_size: Some(collection.current_size),
401            plugins: HashMap::new(),
402            unknown_plugins: vec![],
403            external_plugins: vec![],
404            unknown_external_plugins: vec![],
405        }
406    }
407
408    // Add a processed plugin to the correct `IndexableAsset` struct member.
409    fn add_processed_plugin(&mut self, plugin: ProcessedPlugin) {
410        match plugin {
411            ProcessedPlugin::Known((plugin_type, indexable_plugin_schema)) => {
412                self.plugins.insert(plugin_type, indexable_plugin_schema);
413            }
414            ProcessedPlugin::Unknown(indexable_unknown_plugin_schema) => {
415                self.unknown_plugins.push(indexable_unknown_plugin_schema)
416            }
417        }
418    }
419
420    // Add a processed external plugin to the correct `IndexableAsset` struct member.
421    fn add_processed_external_plugin(&mut self, plugin: ProcessedExternalPlugin) {
422        match plugin {
423            ProcessedExternalPlugin::Known(indexable_plugin_schema) => {
424                self.external_plugins.push(indexable_plugin_schema);
425            }
426            ProcessedExternalPlugin::Unknown(indexable_unknown_plugin_schema) => self
427                .unknown_external_plugins
428                .push(indexable_unknown_plugin_schema),
429        }
430    }
431
432    fn slice_external_plugin_data(
433        data_offset: Option<u64>,
434        data_len: Option<u64>,
435        account: &[u8],
436    ) -> Result<Option<ExternalPluginDataInfo>, std::io::Error> {
437        if data_offset.is_some() && data_len.is_some() {
438            let data_offset = data_offset.unwrap() as usize;
439            let data_len = data_len.unwrap() as usize;
440
441            let end = data_offset
442                .checked_add(data_len)
443                .ok_or(ErrorKind::InvalidData)?;
444            let data_slice = account
445                .get(data_offset..end)
446                .ok_or(ErrorKind::InvalidData)?;
447
448            Ok(Some(ExternalPluginDataInfo {
449                data_offset: data_offset as u64,
450                data_len: data_len as u64,
451                data_slice,
452            }))
453        } else {
454            Ok(None)
455        }
456    }
457
458    fn process_combined_record(
459        index: u64,
460        combined_record: &CombinedRecord,
461        plugin_slice: &mut &[u8],
462        account: &[u8],
463        indexable_asset: &mut IndexableAsset,
464    ) -> Result<(), std::io::Error> {
465        match combined_record {
466            CombinedRecord::Internal(record) => {
467                let processed_plugin = ProcessedPlugin::from_data(
468                    index,
469                    record.offset,
470                    record.authority.clone(),
471                    record.plugin_type,
472                    plugin_slice,
473                )?;
474
475                indexable_asset.add_processed_plugin(processed_plugin);
476            }
477            CombinedRecord::External(record) => {
478                let external_plugin_data_info =
479                    Self::slice_external_plugin_data(record.data_offset, record.data_len, account)?;
480
481                let processed_plugin = ProcessedExternalPlugin::from_data(
482                    index,
483                    record.offset,
484                    record.authority.clone(),
485                    record.lifecycle_checks.clone(),
486                    record.plugin_type,
487                    plugin_slice,
488                    external_plugin_data_info,
489                )?;
490
491                indexable_asset.add_processed_external_plugin(processed_plugin);
492            }
493        }
494
495        Ok(())
496    }
497
498    /// Fetch the base `Asset`` or `Collection`` and all the plugins and store in an
499    /// `IndexableAsset`.
500    pub fn fetch(key: Key, account: &[u8]) -> Result<Self, std::io::Error> {
501        if Key::from_slice(account, 0)? != key {
502            return Err(ErrorKind::InvalidInput.into());
503        }
504
505        let (mut indexable_asset, base_size) = match key {
506            Key::AssetV1 => {
507                let asset = BaseAssetV1::from_bytes(account)?;
508                let base_size = asset.len();
509                let indexable_asset = Self::from_asset(asset, 0);
510                (indexable_asset, base_size)
511            }
512            Key::CollectionV1 => {
513                let collection = BaseCollectionV1::from_bytes(account)?;
514                let base_size = collection.len();
515                let indexable_asset = Self::from_collection(collection);
516                (indexable_asset, base_size)
517            }
518            _ => return Err(ErrorKind::InvalidInput.into()),
519        };
520
521        if base_size != account.len() {
522            let header = PluginHeaderV1::from_bytes(
523                account.get(base_size..).ok_or(ErrorKind::InvalidData)?,
524            )?;
525            let plugin_registry = PluginRegistryV1Safe::from_bytes(
526                account
527                    .get((header.plugin_registry_offset as usize)..)
528                    .ok_or(ErrorKind::InvalidData)?,
529            )?;
530
531            // Combine internal and external plugin registry records.
532            let mut combined_records = vec![];
533
534            // Add internal registry records.
535            for record in &plugin_registry.registry {
536                combined_records.push(CombinedRecordWithDataInfo {
537                    offset: record.offset,
538                    data_offset: None,
539                    record: CombinedRecord::Internal(record),
540                });
541            }
542
543            // Add external registry records.
544            for record in &plugin_registry.external_registry {
545                combined_records.push(CombinedRecordWithDataInfo {
546                    offset: record.offset,
547                    data_offset: record.data_offset,
548                    record: CombinedRecord::External(record),
549                });
550            }
551
552            // Sort combined registry records by offset.
553            combined_records.sort_by(CombinedRecordWithDataInfo::compare_offsets);
554
555            // Process combined registry records using windows of 2 so that plugin slice length can
556            // be calculated.
557            for (i, records) in combined_records.windows(2).enumerate() {
558                // For internal plugins, the end of the slice is the start of the next plugin.  For
559                // external plugins, the end of the adapter is either the start of the data (if
560                // present) or the start of the next plugin.
561                let end = records
562                    .first()
563                    .ok_or(ErrorKind::InvalidData)?
564                    .data_offset
565                    .unwrap_or(records.get(1).ok_or(ErrorKind::InvalidData)?.offset);
566                let mut plugin_slice = account
567                    .get(
568                        records.first().ok_or(ErrorKind::InvalidData)?.offset as usize
569                            ..end as usize,
570                    )
571                    .ok_or(ErrorKind::InvalidData)?;
572
573                Self::process_combined_record(
574                    i as u64,
575                    &records.first().ok_or(ErrorKind::InvalidData)?.record,
576                    &mut plugin_slice,
577                    account,
578                    &mut indexable_asset,
579                )?;
580            }
581
582            // Process the last combined registry record.
583            if let Some(record) = combined_records.last() {
584                // For the last plugin, if it is an internal plugin, the slice ends at the plugin
585                // registry offset.  If it is an external plugin, the end of the adapter is either
586                // the start of the data (if present) or the plugin registry offset.
587                let end = record.data_offset.unwrap_or(header.plugin_registry_offset);
588                let mut plugin_slice = account
589                    .get(record.offset as usize..end as usize)
590                    .ok_or(ErrorKind::InvalidData)?;
591
592                Self::process_combined_record(
593                    combined_records.len() as u64 - 1,
594                    &record.record,
595                    &mut plugin_slice,
596                    account,
597                    &mut indexable_asset,
598                )?;
599            }
600        }
601        Ok(indexable_asset)
602    }
603}