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, GroupV1, 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 Core Assets, Collections, and Groups 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    /// Create a new `IndexableAsset` from a `GroupV1`.
417    pub fn from_group(group: GroupV1) -> Self {
418        Self {
419            owner: Some(group.update_authority),
420            update_authority: UpdateAuthority::Address(group.update_authority),
421            name: group.name,
422            uri: group.uri,
423            seq: 0,
424            num_minted: None,
425            current_size: None,
426            plugins: HashMap::new(),
427            unknown_plugins: vec![],
428            external_plugins: vec![],
429            unknown_external_plugins: vec![],
430        }
431    }
432
433    fn group_len(group: &GroupV1) -> usize {
434        1 // Key
435            + 32 // Update authority
436            + 4 // Name length
437            + group.name.len()
438            + 4 // URI length
439            + group.uri.len()
440            + 4 // collections length
441            + (group.collections.len() * 32)
442            + 4 // groups length
443            + (group.groups.len() * 32)
444            + 4 // parent_groups length
445            + (group.parent_groups.len() * 32)
446            + 4 // assets length
447            + (group.assets.len() * 32)
448    }
449
450    // Add a processed plugin to the correct `IndexableAsset` struct member.
451    fn add_processed_plugin(&mut self, plugin: ProcessedPlugin) {
452        match plugin {
453            ProcessedPlugin::Known((plugin_type, indexable_plugin_schema)) => {
454                self.plugins.insert(plugin_type, indexable_plugin_schema);
455            }
456            ProcessedPlugin::Unknown(indexable_unknown_plugin_schema) => {
457                self.unknown_plugins.push(indexable_unknown_plugin_schema)
458            }
459        }
460    }
461
462    // Add a processed external plugin to the correct `IndexableAsset` struct member.
463    fn add_processed_external_plugin(&mut self, plugin: ProcessedExternalPlugin) {
464        match plugin {
465            ProcessedExternalPlugin::Known(indexable_plugin_schema) => {
466                self.external_plugins.push(indexable_plugin_schema);
467            }
468            ProcessedExternalPlugin::Unknown(indexable_unknown_plugin_schema) => self
469                .unknown_external_plugins
470                .push(indexable_unknown_plugin_schema),
471        }
472    }
473
474    fn slice_external_plugin_data(
475        data_offset: Option<u64>,
476        data_len: Option<u64>,
477        account: &[u8],
478    ) -> Result<Option<ExternalPluginDataInfo<'_>>, std::io::Error> {
479        if data_offset.is_some() && data_len.is_some() {
480            let data_offset = data_offset.unwrap() as usize;
481            let data_len = data_len.unwrap() as usize;
482
483            let end = data_offset
484                .checked_add(data_len)
485                .ok_or(ErrorKind::InvalidData)?;
486            let data_slice = account
487                .get(data_offset..end)
488                .ok_or(ErrorKind::InvalidData)?;
489
490            Ok(Some(ExternalPluginDataInfo {
491                data_offset: data_offset as u64,
492                data_len: data_len as u64,
493                data_slice,
494            }))
495        } else {
496            Ok(None)
497        }
498    }
499
500    fn process_combined_record(
501        index: u64,
502        combined_record: &CombinedRecord,
503        plugin_slice: &mut &[u8],
504        account: &[u8],
505        indexable_asset: &mut IndexableAsset,
506    ) -> Result<(), std::io::Error> {
507        match combined_record {
508            CombinedRecord::Internal(record) => {
509                let processed_plugin = ProcessedPlugin::from_data(
510                    index,
511                    record.offset,
512                    record.authority.clone(),
513                    record.plugin_type,
514                    plugin_slice,
515                )?;
516
517                indexable_asset.add_processed_plugin(processed_plugin);
518            }
519            CombinedRecord::External(record) => {
520                let external_plugin_data_info =
521                    Self::slice_external_plugin_data(record.data_offset, record.data_len, account)?;
522
523                let processed_plugin = ProcessedExternalPlugin::from_data(
524                    index,
525                    record.offset,
526                    record.authority.clone(),
527                    record.lifecycle_checks.clone(),
528                    record.plugin_type,
529                    plugin_slice,
530                    external_plugin_data_info,
531                )?;
532
533                indexable_asset.add_processed_external_plugin(processed_plugin);
534            }
535        }
536
537        Ok(())
538    }
539
540    /// Fetch the base `Asset`, `Collection`, or `Group` and all the plugins and store in an
541    /// `IndexableAsset`.
542    pub fn fetch(key: Key, account: &[u8]) -> Result<Self, std::io::Error> {
543        if Key::from_slice(account, 0)? != key {
544            return Err(ErrorKind::InvalidInput.into());
545        }
546
547        let (mut indexable_asset, base_size) = match key {
548            Key::AssetV1 => {
549                let asset = BaseAssetV1::from_bytes(account)?;
550                let base_size = asset.len();
551                let indexable_asset = Self::from_asset(asset, 0);
552                (indexable_asset, base_size)
553            }
554            Key::CollectionV1 => {
555                let collection = BaseCollectionV1::from_bytes(account)?;
556                let base_size = collection.len();
557                let indexable_asset = Self::from_collection(collection);
558                (indexable_asset, base_size)
559            }
560            Key::GroupV1 => {
561                let group = GroupV1::from_bytes(account)?;
562                let base_size = Self::group_len(&group);
563                let indexable_asset = Self::from_group(group);
564                (indexable_asset, base_size)
565            }
566            _ => return Err(ErrorKind::InvalidInput.into()),
567        };
568
569        if base_size != account.len() {
570            let header = PluginHeaderV1::from_bytes(
571                account.get(base_size..).ok_or(ErrorKind::InvalidData)?,
572            )?;
573            let plugin_registry = PluginRegistryV1Safe::from_bytes(
574                account
575                    .get((header.plugin_registry_offset as usize)..)
576                    .ok_or(ErrorKind::InvalidData)?,
577            )?;
578
579            // Combine internal and external plugin registry records.
580            let mut combined_records = vec![];
581
582            // Add internal registry records.
583            for record in &plugin_registry.registry {
584                combined_records.push(CombinedRecordWithDataInfo {
585                    offset: record.offset,
586                    data_offset: None,
587                    record: CombinedRecord::Internal(record),
588                });
589            }
590
591            // Add external registry records.
592            for record in &plugin_registry.external_registry {
593                combined_records.push(CombinedRecordWithDataInfo {
594                    offset: record.offset,
595                    data_offset: record.data_offset,
596                    record: CombinedRecord::External(record),
597                });
598            }
599
600            // Sort combined registry records by offset.
601            combined_records.sort_by(CombinedRecordWithDataInfo::compare_offsets);
602
603            // Process combined registry records using windows of 2 so that plugin slice length can
604            // be calculated.
605            for (i, records) in combined_records.windows(2).enumerate() {
606                // For internal plugins, the end of the slice is the start of the next plugin.  For
607                // external plugins, the end of the adapter is either the start of the data (if
608                // present) or the start of the next plugin.
609                let end = records
610                    .first()
611                    .ok_or(ErrorKind::InvalidData)?
612                    .data_offset
613                    .unwrap_or(records.get(1).ok_or(ErrorKind::InvalidData)?.offset);
614                let mut plugin_slice = account
615                    .get(
616                        records.first().ok_or(ErrorKind::InvalidData)?.offset as usize
617                            ..end as usize,
618                    )
619                    .ok_or(ErrorKind::InvalidData)?;
620
621                Self::process_combined_record(
622                    i as u64,
623                    &records.first().ok_or(ErrorKind::InvalidData)?.record,
624                    &mut plugin_slice,
625                    account,
626                    &mut indexable_asset,
627                )?;
628            }
629
630            // Process the last combined registry record.
631            if let Some(record) = combined_records.last() {
632                // For the last plugin, if it is an internal plugin, the slice ends at the plugin
633                // registry offset.  If it is an external plugin, the end of the adapter is either
634                // the start of the data (if present) or the plugin registry offset.
635                let end = record.data_offset.unwrap_or(header.plugin_registry_offset);
636                let mut plugin_slice = account
637                    .get(record.offset as usize..end as usize)
638                    .ok_or(ErrorKind::InvalidData)?;
639
640                Self::process_combined_record(
641                    combined_records.len() as u64 - 1,
642                    &record.record,
643                    &mut plugin_slice,
644                    account,
645                    &mut indexable_asset,
646                )?;
647            }
648        }
649        Ok(indexable_asset)
650    }
651}