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#[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 #[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 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 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 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 ExternalPluginAdapter::Oracle(_) => &ExternalPluginAdapterSchema::Binary,
280 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 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#[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 pub fn compare_offsets(
373 a: &CombinedRecordWithDataInfo,
374 b: &CombinedRecordWithDataInfo,
375 ) -> Ordering {
376 a.offset.cmp(&b.offset)
377 }
378}
379
380impl IndexableAsset {
381 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 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 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 + 32 + 4 + group.name.len()
438 + 4 + group.uri.len()
440 + 4 + (group.collections.len() * 32)
442 + 4 + (group.groups.len() * 32)
444 + 4 + (group.parent_groups.len() * 32)
446 + 4 + (group.assets.len() * 32)
448 }
449
450 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 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 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 let mut combined_records = vec![];
581
582 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 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 combined_records.sort_by(CombinedRecordWithDataInfo::compare_offsets);
602
603 for (i, records) in combined_records.windows(2).enumerate() {
606 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 if let Some(record) = combined_records.last() {
632 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}