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 #[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 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 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 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 let mut combined_records = vec![];
541
542 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 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 combined_records.sort_by(CombinedRecordWithDataInfo::compare_offsets);
562
563 for (i, records) in combined_records.windows(2).enumerate() {
566 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 if let Some(record) = combined_records.last() {
592 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}