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#[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#[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#[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#[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#[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#[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 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 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 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 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 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#[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 pub fn compare_offsets(
365 a: &CombinedRecordWithDataInfo,
366 b: &CombinedRecordWithDataInfo,
367 ) -> Ordering {
368 a.offset.cmp(&b.offset)
369 }
370}
371
372impl IndexableAsset {
373 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 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 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 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 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 let mut combined_records = vec![];
533
534 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 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 combined_records.sort_by(CombinedRecordWithDataInfo::compare_offsets);
554
555 for (i, records) in combined_records.windows(2).enumerate() {
558 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 if let Some(record) = combined_records.last() {
584 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}