Skip to main content

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    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
183    pub execute: Vec<IndexableCheckResult>,
184}
185
186impl LifecycleChecks {
187    pub fn is_all_empty(&self) -> bool {
188        self.create.is_empty()
189            && self.update.is_empty()
190            && self.transfer.is_empty()
191            && self.burn.is_empty()
192            && self.execute.is_empty()
193    }
194}
195
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub enum IndexableCheckResult {
199    CanListen,
200    CanApprove,
201    CanReject,
202}
203
204impl From<ExternalCheckResult> for Vec<IndexableCheckResult> {
205    fn from(check_result: ExternalCheckResult) -> Self {
206        let check_result_bits = ExternalCheckResultBits::from(check_result);
207        let mut check_result_vec = vec![];
208        if check_result_bits.can_listen() {
209            check_result_vec.push(IndexableCheckResult::CanListen);
210        }
211        if check_result_bits.can_approve() {
212            check_result_vec.push(IndexableCheckResult::CanApprove);
213        }
214        if check_result_bits.can_reject() {
215            check_result_vec.push(IndexableCheckResult::CanReject);
216        }
217        check_result_vec
218    }
219}
220
221struct ExternalPluginDataInfo<'a> {
222    data_offset: u64,
223    data_len: u64,
224    data_slice: &'a [u8],
225}
226
227impl ProcessedExternalPlugin {
228    fn from_data(
229        index: u64,
230        offset: u64,
231        authority: PluginAuthority,
232        lifecycle_checks: Option<Vec<(u8, ExternalCheckResult)>>,
233        external_plugin_adapter_type: u8,
234        plugin_slice: &mut &[u8],
235        external_plugin_data_info: Option<ExternalPluginDataInfo>,
236    ) -> Result<Self, std::io::Error> {
237        // First process the lifecycle checks.
238        let mut known_lifecycle_checks = LifecycleChecks::default();
239        let mut unknown_lifecycle_checks = vec![];
240
241        if let Some(checks) = lifecycle_checks {
242            for (event, check) in checks {
243                let checks = Vec::<IndexableCheckResult>::from(check);
244                match HookableLifecycleEvent::from_u8(event) {
245                    Some(val) => match val {
246                        HookableLifecycleEvent::Create => known_lifecycle_checks.create = checks,
247                        HookableLifecycleEvent::Update => known_lifecycle_checks.update = checks,
248                        HookableLifecycleEvent::Transfer => {
249                            known_lifecycle_checks.transfer = checks
250                        }
251                        HookableLifecycleEvent::Burn => known_lifecycle_checks.burn = checks,
252                        HookableLifecycleEvent::Execute => known_lifecycle_checks.execute = checks,
253                    },
254                    None => unknown_lifecycle_checks.push((event, checks)),
255                }
256            }
257        }
258
259        // Save them as known and unknown.
260        let known_lifecycle_checks =
261            (!known_lifecycle_checks.is_all_empty()).then_some(known_lifecycle_checks);
262        let unknown_lifecycle_checks =
263            (!unknown_lifecycle_checks.is_empty()).then_some(unknown_lifecycle_checks);
264
265        // Next, process the external plugin adapter and save them as known and unknown variants.
266        let processed_plugin = if let Some(r#type) =
267            ExternalPluginAdapterType::from_u8(external_plugin_adapter_type)
268        {
269            let adapter_config = ExternalPluginAdapter::deserialize(plugin_slice)?;
270            let (data_offset, data_len, data) = match external_plugin_data_info {
271                Some(data_info) => {
272                    let schema = match &adapter_config {
273                        ExternalPluginAdapter::LifecycleHook(lc_hook) => &lc_hook.schema,
274                        ExternalPluginAdapter::AppData(app_data) => &app_data.schema,
275                        ExternalPluginAdapter::LinkedLifecycleHook(l_lc_hook) => &l_lc_hook.schema,
276                        ExternalPluginAdapter::LinkedAppData(l_app_data) => &l_app_data.schema,
277                        ExternalPluginAdapter::DataSection(data_section) => &data_section.schema,
278                        // Assume binary for `Oracle`, but this should never happen.
279                        ExternalPluginAdapter::Oracle(_) => &ExternalPluginAdapterSchema::Binary,
280                        // AgentIdentity has no data section.
281                        ExternalPluginAdapter::AgentIdentity(_) => {
282                            &ExternalPluginAdapterSchema::Binary
283                        }
284                    };
285
286                    (
287                        Some(data_info.data_offset),
288                        Some(data_info.data_len),
289                        Some(convert_external_plugin_adapter_data_to_string(
290                            schema,
291                            data_info.data_slice,
292                        )),
293                    )
294                }
295                None => (None, None, None),
296            };
297
298            let indexable_plugin_schema = IndexableExternalPluginSchemaV1 {
299                index,
300                offset,
301                authority,
302                lifecycle_checks: known_lifecycle_checks,
303                unknown_lifecycle_checks,
304                r#type,
305                adapter_config,
306                data_offset,
307                data_len,
308                data,
309            };
310            ProcessedExternalPlugin::Known(indexable_plugin_schema)
311        } else {
312            let encoded: String = BASE64_STANDARD.encode(plugin_slice);
313
314            let (data_offset, data_len, data) = match external_plugin_data_info {
315                Some(data_info) => (
316                    Some(data_info.data_offset),
317                    Some(data_info.data_len),
318                    // We don't know the schema so use base64.
319                    Some(BASE64_STANDARD.encode(data_info.data_slice)),
320                ),
321                None => (None, None, None),
322            };
323
324            ProcessedExternalPlugin::Unknown(IndexableUnknownExternalPluginSchemaV1 {
325                index,
326                offset,
327                authority,
328                lifecycle_checks: known_lifecycle_checks,
329                unknown_lifecycle_checks,
330                r#type: external_plugin_adapter_type,
331                unknown_adapter_config: encoded,
332                data_offset,
333                data_len,
334                data,
335            })
336        };
337
338        Ok(processed_plugin)
339    }
340}
341
342/// A type used to store both Core Assets and Core Collections for indexing.
343#[derive(Clone, Debug, Eq, PartialEq)]
344#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
345pub struct IndexableAsset {
346    pub owner: Option<Pubkey>,
347    pub update_authority: UpdateAuthority,
348    pub name: String,
349    pub uri: String,
350    pub seq: u64,
351    pub num_minted: Option<u32>,
352    pub current_size: Option<u32>,
353    pub plugins: HashMap<PluginType, IndexablePluginSchemaV1>,
354    pub unknown_plugins: Vec<IndexableUnknownPluginSchemaV1>,
355    pub external_plugins: Vec<IndexableExternalPluginSchemaV1>,
356    pub unknown_external_plugins: Vec<IndexableUnknownExternalPluginSchemaV1>,
357}
358
359enum CombinedRecord<'a> {
360    Internal(&'a RegistryRecordSafe),
361    External(&'a ExternalRegistryRecordSafe),
362}
363
364struct CombinedRecordWithDataInfo<'a> {
365    pub offset: u64,
366    pub data_offset: Option<u64>,
367    pub record: CombinedRecord<'a>,
368}
369
370impl<'a> CombinedRecordWithDataInfo<'a> {
371    // Associated function for sorting `RegistryRecordIndexable` by offset.
372    pub fn compare_offsets(
373        a: &CombinedRecordWithDataInfo,
374        b: &CombinedRecordWithDataInfo,
375    ) -> Ordering {
376        a.offset.cmp(&b.offset)
377    }
378}
379
380impl IndexableAsset {
381    /// Create a new `IndexableAsset` from a `BaseAssetV1``.  Note this uses a passed-in `seq` rather than
382    /// the one contained in `asset` to avoid errors.
383    pub fn from_asset(asset: BaseAssetV1, seq: u64) -> Self {
384        Self {
385            owner: Some(asset.owner),
386            update_authority: asset.update_authority,
387            name: asset.name,
388            uri: asset.uri,
389            seq,
390            num_minted: None,
391            current_size: None,
392            plugins: HashMap::new(),
393            unknown_plugins: vec![],
394            external_plugins: vec![],
395            unknown_external_plugins: vec![],
396        }
397    }
398
399    /// Create a new `IndexableAsset` from a `BaseCollectionV1`.
400    pub fn from_collection(collection: BaseCollectionV1) -> Self {
401        Self {
402            owner: None,
403            update_authority: UpdateAuthority::Address(collection.update_authority),
404            name: collection.name,
405            uri: collection.uri,
406            seq: 0,
407            num_minted: Some(collection.num_minted),
408            current_size: Some(collection.current_size),
409            plugins: HashMap::new(),
410            unknown_plugins: vec![],
411            external_plugins: vec![],
412            unknown_external_plugins: vec![],
413        }
414    }
415
416    // Add a processed plugin to the correct `IndexableAsset` struct member.
417    fn add_processed_plugin(&mut self, plugin: ProcessedPlugin) {
418        match plugin {
419            ProcessedPlugin::Known((plugin_type, indexable_plugin_schema)) => {
420                self.plugins.insert(plugin_type, indexable_plugin_schema);
421            }
422            ProcessedPlugin::Unknown(indexable_unknown_plugin_schema) => {
423                self.unknown_plugins.push(indexable_unknown_plugin_schema)
424            }
425        }
426    }
427
428    // Add a processed external plugin to the correct `IndexableAsset` struct member.
429    fn add_processed_external_plugin(&mut self, plugin: ProcessedExternalPlugin) {
430        match plugin {
431            ProcessedExternalPlugin::Known(indexable_plugin_schema) => {
432                self.external_plugins.push(indexable_plugin_schema);
433            }
434            ProcessedExternalPlugin::Unknown(indexable_unknown_plugin_schema) => self
435                .unknown_external_plugins
436                .push(indexable_unknown_plugin_schema),
437        }
438    }
439
440    fn slice_external_plugin_data(
441        data_offset: Option<u64>,
442        data_len: Option<u64>,
443        account: &[u8],
444    ) -> Result<Option<ExternalPluginDataInfo>, std::io::Error> {
445        if data_offset.is_some() && data_len.is_some() {
446            let data_offset = data_offset.unwrap() as usize;
447            let data_len = data_len.unwrap() as usize;
448
449            let end = data_offset
450                .checked_add(data_len)
451                .ok_or(ErrorKind::InvalidData)?;
452            let data_slice = account
453                .get(data_offset..end)
454                .ok_or(ErrorKind::InvalidData)?;
455
456            Ok(Some(ExternalPluginDataInfo {
457                data_offset: data_offset as u64,
458                data_len: data_len as u64,
459                data_slice,
460            }))
461        } else {
462            Ok(None)
463        }
464    }
465
466    fn process_combined_record(
467        index: u64,
468        combined_record: &CombinedRecord,
469        plugin_slice: &mut &[u8],
470        account: &[u8],
471        indexable_asset: &mut IndexableAsset,
472    ) -> Result<(), std::io::Error> {
473        match combined_record {
474            CombinedRecord::Internal(record) => {
475                let processed_plugin = ProcessedPlugin::from_data(
476                    index,
477                    record.offset,
478                    record.authority.clone(),
479                    record.plugin_type,
480                    plugin_slice,
481                )?;
482
483                indexable_asset.add_processed_plugin(processed_plugin);
484            }
485            CombinedRecord::External(record) => {
486                let external_plugin_data_info =
487                    Self::slice_external_plugin_data(record.data_offset, record.data_len, account)?;
488
489                let processed_plugin = ProcessedExternalPlugin::from_data(
490                    index,
491                    record.offset,
492                    record.authority.clone(),
493                    record.lifecycle_checks.clone(),
494                    record.plugin_type,
495                    plugin_slice,
496                    external_plugin_data_info,
497                )?;
498
499                indexable_asset.add_processed_external_plugin(processed_plugin);
500            }
501        }
502
503        Ok(())
504    }
505
506    /// Fetch the base `Asset`` or `Collection`` and all the plugins and store in an
507    /// `IndexableAsset`.
508    pub fn fetch(key: Key, account: &[u8]) -> Result<Self, std::io::Error> {
509        if Key::from_slice(account, 0)? != key {
510            return Err(ErrorKind::InvalidInput.into());
511        }
512
513        let (mut indexable_asset, base_size) = match key {
514            Key::AssetV1 => {
515                let asset = BaseAssetV1::from_bytes(account)?;
516                let base_size = asset.len();
517                let indexable_asset = Self::from_asset(asset, 0);
518                (indexable_asset, base_size)
519            }
520            Key::CollectionV1 => {
521                let collection = BaseCollectionV1::from_bytes(account)?;
522                let base_size = collection.len();
523                let indexable_asset = Self::from_collection(collection);
524                (indexable_asset, base_size)
525            }
526            _ => return Err(ErrorKind::InvalidInput.into()),
527        };
528
529        if base_size != account.len() {
530            let header = PluginHeaderV1::from_bytes(
531                account.get(base_size..).ok_or(ErrorKind::InvalidData)?,
532            )?;
533            let plugin_registry = PluginRegistryV1Safe::from_bytes(
534                account
535                    .get((header.plugin_registry_offset as usize)..)
536                    .ok_or(ErrorKind::InvalidData)?,
537            )?;
538
539            // Combine internal and external plugin registry records.
540            let mut combined_records = vec![];
541
542            // Add internal registry records.
543            for record in &plugin_registry.registry {
544                combined_records.push(CombinedRecordWithDataInfo {
545                    offset: record.offset,
546                    data_offset: None,
547                    record: CombinedRecord::Internal(record),
548                });
549            }
550
551            // Add external registry records.
552            for record in &plugin_registry.external_registry {
553                combined_records.push(CombinedRecordWithDataInfo {
554                    offset: record.offset,
555                    data_offset: record.data_offset,
556                    record: CombinedRecord::External(record),
557                });
558            }
559
560            // Sort combined registry records by offset.
561            combined_records.sort_by(CombinedRecordWithDataInfo::compare_offsets);
562
563            // Process combined registry records using windows of 2 so that plugin slice length can
564            // be calculated.
565            for (i, records) in combined_records.windows(2).enumerate() {
566                // For internal plugins, the end of the slice is the start of the next plugin.  For
567                // external plugins, the end of the adapter is either the start of the data (if
568                // present) or the start of the next plugin.
569                let end = records
570                    .first()
571                    .ok_or(ErrorKind::InvalidData)?
572                    .data_offset
573                    .unwrap_or(records.get(1).ok_or(ErrorKind::InvalidData)?.offset);
574                let mut plugin_slice = account
575                    .get(
576                        records.first().ok_or(ErrorKind::InvalidData)?.offset as usize
577                            ..end as usize,
578                    )
579                    .ok_or(ErrorKind::InvalidData)?;
580
581                Self::process_combined_record(
582                    i as u64,
583                    &records.first().ok_or(ErrorKind::InvalidData)?.record,
584                    &mut plugin_slice,
585                    account,
586                    &mut indexable_asset,
587                )?;
588            }
589
590            // Process the last combined registry record.
591            if let Some(record) = combined_records.last() {
592                // For the last plugin, if it is an internal plugin, the slice ends at the plugin
593                // registry offset.  If it is an external plugin, the end of the adapter is either
594                // the start of the data (if present) or the plugin registry offset.
595                let end = record.data_offset.unwrap_or(header.plugin_registry_offset);
596                let mut plugin_slice = account
597                    .get(record.offset as usize..end as usize)
598                    .ok_or(ErrorKind::InvalidData)?;
599
600                Self::process_combined_record(
601                    combined_records.len() as u64 - 1,
602                    &record.record,
603                    &mut plugin_slice,
604                    account,
605                    &mut indexable_asset,
606                )?;
607            }
608        }
609        Ok(indexable_asset)
610    }
611}