diff --git a/clients/rust/src/hooked/advanced_types.rs b/clients/rust/src/hooked/advanced_types.rs index b76b3993..8f3c520b 100644 --- a/clients/rust/src/hooked/advanced_types.rs +++ b/clients/rust/src/hooked/advanced_types.rs @@ -1,10 +1,13 @@ +use borsh::BorshDeserialize; use solana_program::pubkey::Pubkey; +use std::{cmp::Ordering, io::ErrorKind}; use crate::{ accounts::{BaseAssetV1, BaseCollectionV1, PluginHeaderV1}, types::{ - Attributes, BurnDelegate, FreezeDelegate, PermanentBurnDelegate, PermanentFreezeDelegate, - PermanentTransferDelegate, PluginAuthority, Royalties, TransferDelegate, UpdateDelegate, + Attributes, BurnDelegate, FreezeDelegate, Key, PermanentBurnDelegate, + PermanentFreezeDelegate, PermanentTransferDelegate, PluginAuthority, Royalties, + TransferDelegate, UpdateDelegate, }, }; @@ -140,5 +143,59 @@ pub struct Asset { pub struct Collection { pub base: BaseCollectionV1, pub plugin_list: PluginsList, - pub plugin_header: PluginHeaderV1, + pub plugin_header: Option, +} + +/// Registry record that can be used when the plugin type is not known (i.e. a `PluginType` that +/// is too new for this client to know about). +pub struct RegistryRecordSafe { + pub plugin_type: u8, + pub authority: PluginAuthority, + pub offset: u64, +} + +impl RegistryRecordSafe { + /// Associated function for sorting `RegistryRecordIndexable` by offset. + pub fn compare_offsets(a: &RegistryRecordSafe, b: &RegistryRecordSafe) -> Ordering { + a.offset.cmp(&b.offset) + } +} + +/// Plugin registry that an account can safely be deserialized into even if some plugins are +/// not known. Note this skips over external plugins for now, and will be updated when those +/// are defined. +pub struct PluginRegistryV1Safe { + pub _key: Key, + pub registry: Vec, +} + +impl PluginRegistryV1Safe { + #[inline(always)] + pub fn from_bytes(data: &[u8]) -> Result { + let mut data: &[u8] = data; + let key = Key::deserialize(&mut data)?; + if key != Key::PluginRegistryV1 { + return Err(ErrorKind::InvalidInput.into()); + } + + let registry_size = u32::deserialize(&mut data)?; + + let mut registry = vec![]; + for _ in 0..registry_size { + let plugin_type = u8::deserialize(&mut data)?; + let authority = PluginAuthority::deserialize(&mut data)?; + let offset = u64::deserialize(&mut data)?; + + registry.push(RegistryRecordSafe { + plugin_type, + authority, + offset, + }); + } + + Ok(Self { + _key: key, + registry, + }) + } } diff --git a/clients/rust/src/hooked/asset.rs b/clients/rust/src/hooked/asset.rs index 31da0a76..17d695b9 100644 --- a/clients/rust/src/hooked/asset.rs +++ b/clients/rust/src/hooked/asset.rs @@ -1,26 +1,20 @@ -//! This code was AUTOGENERATED using the kinobi library. -//! Please DO NOT EDIT THIS FILE, instead use visitors -//! to add features, then rerun kinobi to update it. -//! -//! [https://github.com/metaplex-foundation/kinobi] -//! - use borsh::BorshSerialize; use crate::{ - accounts::{BaseAssetV1, PluginHeaderV1, PluginRegistryV1}, - registry_records_to_plugin_list, Asset, + accounts::{BaseAssetV1, PluginHeaderV1}, + registry_records_to_plugin_list, Asset, PluginRegistryV1Safe, }; impl Asset { - pub fn deserialize_asset(data: &[u8]) -> Result { + pub fn deserialize(data: &[u8]) -> Result { let base = BaseAssetV1::from_bytes(data)?; let base_data = base.try_to_vec()?; let (plugin_header, plugin_list) = if base_data.len() != data.len() { let plugin_header = PluginHeaderV1::from_bytes(&data[base_data.len()..])?; - let plugin_registry = PluginRegistryV1::from_bytes( + let plugin_registry = PluginRegistryV1Safe::from_bytes( &data[plugin_header.plugin_registry_offset as usize..], )?; + let plugin_list = registry_records_to_plugin_list(&plugin_registry.registry, data)?; (Some(plugin_header), Some(plugin_list)) @@ -37,7 +31,7 @@ impl Asset { #[inline(always)] pub fn from_bytes(data: &[u8]) -> Result { - Self::deserialize_asset(data) + Self::deserialize(data) } } @@ -48,6 +42,6 @@ impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for Asset { account_info: &solana_program::account_info::AccountInfo<'a>, ) -> Result { let data: &[u8] = &(*account_info.data).borrow(); - Self::deserialize_asset(data) + Self::deserialize(data) } } diff --git a/clients/rust/src/hooked/collection.rs b/clients/rust/src/hooked/collection.rs new file mode 100644 index 00000000..5aa9e51a --- /dev/null +++ b/clients/rust/src/hooked/collection.rs @@ -0,0 +1,47 @@ +use borsh::BorshSerialize; + +use crate::{ + accounts::{BaseCollectionV1, PluginHeaderV1}, + registry_records_to_plugin_list, Collection, PluginRegistryV1Safe, +}; + +impl Collection { + pub fn deserialize(data: &[u8]) -> Result { + let base = BaseCollectionV1::from_bytes(data)?; + let base_data = base.try_to_vec()?; + let (plugin_header, plugin_list) = if base_data.len() != data.len() { + let plugin_header = PluginHeaderV1::from_bytes(&data[base_data.len()..])?; + let plugin_registry = PluginRegistryV1Safe::from_bytes( + &data[plugin_header.plugin_registry_offset as usize..], + )?; + + let plugin_list = registry_records_to_plugin_list(&plugin_registry.registry, data)?; + + (Some(plugin_header), Some(plugin_list)) + } else { + (None, None) + }; + + Ok(Self { + base, + plugin_list: plugin_list.unwrap_or_default(), + plugin_header, + }) + } + + #[inline(always)] + pub fn from_bytes(data: &[u8]) -> Result { + Self::deserialize(data) + } +} + +impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for Collection { + type Error = std::io::Error; + + fn try_from( + account_info: &solana_program::account_info::AccountInfo<'a>, + ) -> Result { + let data: &[u8] = &(*account_info.data).borrow(); + Self::deserialize(data) + } +} diff --git a/clients/rust/src/hooked/mod.rs b/clients/rust/src/hooked/mod.rs index 02f9c9f1..703c7da7 100644 --- a/clients/rust/src/hooked/mod.rs +++ b/clients/rust/src/hooked/mod.rs @@ -7,8 +7,10 @@ pub use advanced_types::*; pub mod asset; pub use asset::*; -use borsh::{BorshDeserialize, BorshSerialize}; +pub mod collection; +pub use collection::*; +use borsh::{BorshDeserialize, BorshSerialize}; use std::{cmp::Ordering, mem::size_of}; use crate::{ @@ -18,6 +20,24 @@ use crate::{ }; use solana_program::account_info::AccountInfo; +impl PluginType { + // Needed to determine if a plugin is a known or unknown type. + pub fn from_u8(n: u8) -> Option { + match n { + 0 => Some(PluginType::Royalties), + 1 => Some(PluginType::FreezeDelegate), + 2 => Some(PluginType::BurnDelegate), + 3 => Some(PluginType::TransferDelegate), + 4 => Some(PluginType::UpdateDelegate), + 5 => Some(PluginType::PermanentFreezeDelegate), + 6 => Some(PluginType::Attributes), + 7 => Some(PluginType::PermanentTransferDelegate), + 8 => Some(PluginType::PermanentBurnDelegate), + _ => None, + } + } +} + impl From<&Plugin> for PluginType { fn from(plugin: &Plugin) -> Self { match plugin { diff --git a/clients/rust/src/hooked/plugin.rs b/clients/rust/src/hooked/plugin.rs index 6a1aa188..9658b859 100644 --- a/clients/rust/src/hooked/plugin.rs +++ b/clients/rust/src/hooked/plugin.rs @@ -2,13 +2,13 @@ use borsh::BorshDeserialize; use solana_program::account_info::AccountInfo; use crate::{ - accounts::{BaseAssetV1, PluginHeaderV1, PluginRegistryV1}, + accounts::{BaseAssetV1, PluginHeaderV1}, errors::MplCoreError, types::{Plugin, PluginAuthority, PluginType, RegistryRecord}, AttributesPlugin, BaseAuthority, BasePlugin, BurnDelegatePlugin, DataBlob, FreezeDelegatePlugin, PermanentBurnDelegatePlugin, PermanentFreezeDelegatePlugin, - PermanentTransferDelegatePlugin, PluginsList, RoyaltiesPlugin, SolanaAccount, - TransferDelegatePlugin, UpdateDelegatePlugin, + PermanentTransferDelegatePlugin, PluginRegistryV1Safe, PluginsList, RegistryRecordSafe, + RoyaltiesPlugin, SolanaAccount, TransferDelegatePlugin, UpdateDelegatePlugin, }; /// Fetch the plugin from the registry. @@ -25,14 +25,22 @@ pub fn fetch_plugin( )); } - let header = PluginHeaderV1::load(account, asset.get_size())?; - let PluginRegistryV1 { registry, .. } = - PluginRegistryV1::load(account, header.plugin_registry_offset as usize)?; + let header = PluginHeaderV1::from_bytes(&(*account.data).borrow()[asset.get_size()..])?; + let plugin_registry = PluginRegistryV1Safe::from_bytes( + &(*account.data).borrow()[header.plugin_registry_offset as usize..], + )?; // Find the plugin in the registry. - let registry_record = registry + let registry_record = plugin_registry + .registry .iter() - .find(|record| record.plugin_type == plugin_type) + .find(|record| { + if let Some(plugin) = PluginType::from_u8(record.plugin_type) { + plugin == plugin_type + } else { + false + } + }) .ok_or(std::io::Error::new( std::io::ErrorKind::Other, MplCoreError::PluginNotFound.to_string(), @@ -66,95 +74,117 @@ pub fn fetch_plugin( )) } -/// Fetch the plugin registry. -pub fn fetch_plugins(account: &[u8]) -> Result, std::io::Error> { - let asset = BaseAssetV1::from_bytes(account)?; +/// Fetch the plugin registry, dropping any unknown plugins (i.e. `PluginType`s that are too new +/// for this client to know about). +pub fn fetch_plugins(account_data: &[u8]) -> Result, std::io::Error> { + let asset = BaseAssetV1::from_bytes(account_data)?; - let header = PluginHeaderV1::from_bytes(&account[asset.get_size()..])?; - let PluginRegistryV1 { registry, .. } = - PluginRegistryV1::from_bytes(&account[(header.plugin_registry_offset as usize)..])?; + let header = PluginHeaderV1::from_bytes(&account_data[asset.get_size()..])?; + let plugin_registry = PluginRegistryV1Safe::from_bytes( + &account_data[(header.plugin_registry_offset as usize)..], + )?; - Ok(registry) + let filtered_plugin_registry = plugin_registry + .registry + .iter() + .filter_map(|record| { + PluginType::from_u8(record.plugin_type).map(|plugin_type| RegistryRecord { + plugin_type, + authority: record.authority.clone(), + offset: record.offset, + }) + }) + .collect(); + + Ok(filtered_plugin_registry) } -/// Create plugin header and registry if it doesn't exist -pub fn list_plugins(account: &[u8]) -> Result, std::io::Error> { - let asset = BaseAssetV1::from_bytes(account)?; - - let header = PluginHeaderV1::from_bytes(&account[asset.get_size()..])?; - let PluginRegistryV1 { registry, .. } = - PluginRegistryV1::from_bytes(&account[(header.plugin_registry_offset as usize)..])?; +/// List all plugins in an account, dropping any unknown plugins (i.e. `PluginType`s that are too +/// new for this client to know about). Note this also does not support external plugins for now, +/// and will be updated when those are defined. +pub fn list_plugins(account_data: &[u8]) -> Result, std::io::Error> { + let asset = BaseAssetV1::from_bytes(account_data)?; + let header = PluginHeaderV1::from_bytes(&account_data[asset.get_size()..])?; + let plugin_registry = PluginRegistryV1Safe::from_bytes( + &account_data[(header.plugin_registry_offset as usize)..], + )?; - Ok(registry + Ok(plugin_registry + .registry .iter() - .map(|registry_record| registry_record.plugin_type.clone()) + .filter_map(|registry_record| PluginType::from_u8(registry_record.plugin_type)) .collect()) } -pub fn registry_records_to_plugin_list( - registry_records: &[RegistryRecord], +// Convert a slice of `RegistryRecordSafe` into the `PluginsList` type, dropping any unknown +// plugins (i.e. `PluginType`s that are too new for this client to know about). Note this also does +// not support external plugins for now, and will be updated when those are defined. +pub(crate) fn registry_records_to_plugin_list( + registry_records: &[RegistryRecordSafe], account_data: &[u8], ) -> Result { let result = registry_records .iter() .try_fold(PluginsList::default(), |mut acc, record| { - let authority: BaseAuthority = record.authority.clone().into(); - let base = BasePlugin { - authority, - offset: Some(record.offset), - }; - let plugin = Plugin::deserialize(&mut &account_data[record.offset as usize..])?; - - match plugin { - Plugin::Royalties(royalties) => { - acc.royalties = Some(RoyaltiesPlugin { base, royalties }); - } - Plugin::FreezeDelegate(freeze_delegate) => { - acc.freeze_delegate = Some(FreezeDelegatePlugin { - base, - freeze_delegate, - }); - } - Plugin::BurnDelegate(burn_delegate) => { - acc.burn_delegate = Some(BurnDelegatePlugin { - base, - burn_delegate, - }); - } - Plugin::TransferDelegate(transfer_delegate) => { - acc.transfer_delegate = Some(TransferDelegatePlugin { - base, - transfer_delegate, - }); - } - Plugin::UpdateDelegate(update_delegate) => { - acc.update_delegate = Some(UpdateDelegatePlugin { - base, - update_delegate, - }); - } - Plugin::PermanentFreezeDelegate(permanent_freeze_delegate) => { - acc.permanent_freeze_delegate = Some(PermanentFreezeDelegatePlugin { - base, - permanent_freeze_delegate, - }); - } - Plugin::Attributes(attributes) => { - acc.attributes = Some(AttributesPlugin { base, attributes }); - } - Plugin::PermanentTransferDelegate(permanent_transfer_delegate) => { - acc.permanent_transfer_delegate = Some(PermanentTransferDelegatePlugin { - base, - permanent_transfer_delegate, - }) - } - Plugin::PermanentBurnDelegate(permanent_burn_delegate) => { - acc.permanent_burn_delegate = Some(PermanentBurnDelegatePlugin { - base, - permanent_burn_delegate, - }) + if PluginType::from_u8(record.plugin_type).is_some() { + let authority: BaseAuthority = record.authority.clone().into(); + let base = BasePlugin { + authority, + offset: Some(record.offset), + }; + let plugin = Plugin::deserialize(&mut &account_data[record.offset as usize..])?; + + match plugin { + Plugin::Royalties(royalties) => { + acc.royalties = Some(RoyaltiesPlugin { base, royalties }); + } + Plugin::FreezeDelegate(freeze_delegate) => { + acc.freeze_delegate = Some(FreezeDelegatePlugin { + base, + freeze_delegate, + }); + } + Plugin::BurnDelegate(burn_delegate) => { + acc.burn_delegate = Some(BurnDelegatePlugin { + base, + burn_delegate, + }); + } + Plugin::TransferDelegate(transfer_delegate) => { + acc.transfer_delegate = Some(TransferDelegatePlugin { + base, + transfer_delegate, + }); + } + Plugin::UpdateDelegate(update_delegate) => { + acc.update_delegate = Some(UpdateDelegatePlugin { + base, + update_delegate, + }); + } + Plugin::PermanentFreezeDelegate(permanent_freeze_delegate) => { + acc.permanent_freeze_delegate = Some(PermanentFreezeDelegatePlugin { + base, + permanent_freeze_delegate, + }); + } + Plugin::Attributes(attributes) => { + acc.attributes = Some(AttributesPlugin { base, attributes }); + } + Plugin::PermanentTransferDelegate(permanent_transfer_delegate) => { + acc.permanent_transfer_delegate = Some(PermanentTransferDelegatePlugin { + base, + permanent_transfer_delegate, + }) + } + Plugin::PermanentBurnDelegate(permanent_burn_delegate) => { + acc.permanent_burn_delegate = Some(PermanentBurnDelegatePlugin { + base, + permanent_burn_delegate, + }) + } } - }; + } Ok(acc) }); diff --git a/clients/rust/src/indexable_asset.rs b/clients/rust/src/indexable_asset.rs index e48517fc..b458aa23 100644 --- a/clients/rust/src/indexable_asset.rs +++ b/clients/rust/src/indexable_asset.rs @@ -1,33 +1,14 @@ use base64::prelude::*; use borsh::BorshDeserialize; use solana_program::pubkey::Pubkey; -use std::{cmp::Ordering, collections::HashMap, io::ErrorKind}; +use std::{collections::HashMap, io::ErrorKind}; use crate::{ accounts::{BaseAssetV1, BaseCollectionV1, PluginHeaderV1}, types::{Key, Plugin, PluginAuthority, PluginType, UpdateAuthority}, - DataBlob, + DataBlob, PluginRegistryV1Safe, RegistryRecordSafe, }; -impl PluginType { - // Needed to determine if a plugin is a known or unknown type. - // TODO: Derive this using Kinobi. - pub fn from_u8(n: u8) -> Option { - match n { - 0 => Some(PluginType::Royalties), - 1 => Some(PluginType::FreezeDelegate), - 2 => Some(PluginType::BurnDelegate), - 3 => Some(PluginType::TransferDelegate), - 4 => Some(PluginType::UpdateDelegate), - 5 => Some(PluginType::PermanentFreezeDelegate), - 6 => Some(PluginType::Attributes), - 7 => Some(PluginType::PermanentTransferDelegate), - 8 => Some(PluginType::PermanentBurnDelegate), - _ => None, - } - } -} - /// Schema used for indexing known plugin types. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, Eq, PartialEq)] @@ -89,57 +70,6 @@ impl ProcessedPlugin { } } -// Registry record that can be used when some plugins are not known. -struct RegistryRecordSafe { - pub plugin_type: u8, - pub authority: PluginAuthority, - pub offset: u64, -} - -impl RegistryRecordSafe { - /// Associated function for sorting `RegistryRecordIndexable` by offset. - pub fn compare_offsets(a: &RegistryRecordSafe, b: &RegistryRecordSafe) -> Ordering { - a.offset.cmp(&b.offset) - } -} - -// Plugin registry that can safely be deserialized even if some plugins are not known. -struct PluginRegistryV1Safe { - pub _key: Key, - pub registry: Vec, -} - -impl PluginRegistryV1Safe { - #[inline(always)] - pub fn from_bytes(data: &[u8]) -> Result { - let mut data: &[u8] = data; - let key = Key::deserialize(&mut data)?; - if key != Key::PluginRegistryV1 { - return Err(ErrorKind::InvalidInput.into()); - } - - let registry_size = u32::deserialize(&mut data)?; - - let mut registry = vec![]; - for _ in 0..registry_size { - let plugin_type = u8::deserialize(&mut data)?; - let authority = PluginAuthority::deserialize(&mut data)?; - let offset = u64::deserialize(&mut data)?; - - registry.push(RegistryRecordSafe { - plugin_type, - authority, - offset, - }); - } - - Ok(Self { - _key: key, - registry, - }) - } -} - /// A type used to store both Core Assets and Core Collections for indexing. #[derive(Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/clients/rust/tests/create_collection.rs b/clients/rust/tests/create_collection.rs new file mode 100644 index 00000000..755eceba --- /dev/null +++ b/clients/rust/tests/create_collection.rs @@ -0,0 +1,234 @@ +#![cfg(feature = "test-sbf")] +pub mod setup; +use mpl_core::types::{Creator, Plugin, PluginAuthority, PluginAuthorityPair, Royalties, RuleSet}; +pub use setup::*; + +use solana_program_test::tokio; +use solana_sdk::{native_token::LAMPORTS_PER_SOL, signature::Keypair, signer::Signer}; + +#[tokio::test] +async fn test_create_collection() { + let mut context = program_test().start_with_context().await; + + let collection = Keypair::new(); + create_collection( + &mut context, + CreateCollectionHelperArgs { + collection: &collection, + update_authority: None, + payer: None, + name: None, + uri: None, + plugins: vec![], + }, + ) + .await + .unwrap(); + + let update_authority = context.payer.pubkey(); + assert_collection( + &mut context, + AssertCollectionHelperArgs { + collection: collection.pubkey(), + update_authority, + name: None, + uri: None, + num_minted: 0, + current_size: 0, + plugins: vec![], + }, + ) + .await; +} + +#[tokio::test] +async fn create_collection_with_different_payer() { + let mut context = program_test().start_with_context().await; + + let collection = Keypair::new(); + let payer = Keypair::new(); + airdrop(&mut context, &payer.pubkey(), LAMPORTS_PER_SOL) + .await + .unwrap(); + create_collection( + &mut context, + CreateCollectionHelperArgs { + collection: &collection, + update_authority: None, + payer: Some(&payer), + name: None, + uri: None, + plugins: vec![], + }, + ) + .await + .unwrap(); + + assert_collection( + &mut context, + AssertCollectionHelperArgs { + collection: collection.pubkey(), + update_authority: payer.pubkey(), + name: None, + uri: None, + num_minted: 0, + current_size: 0, + plugins: vec![], + }, + ) + .await; +} + +#[tokio::test] +async fn create_collection_with_plugins() { + let mut context = program_test().start_with_context().await; + + let collection = Keypair::new(); + let update_authority = context.payer.pubkey(); + create_collection( + &mut context, + CreateCollectionHelperArgs { + collection: &collection, + update_authority: None, + payer: None, + name: None, + uri: None, + plugins: vec![PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: update_authority, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }], + }, + ) + .await + .unwrap(); + + assert_collection( + &mut context, + AssertCollectionHelperArgs { + collection: collection.pubkey(), + update_authority, + name: None, + uri: None, + num_minted: 0, + current_size: 0, + plugins: vec![PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: update_authority, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }], + }, + ) + .await; +} + +#[tokio::test] +async fn create_collection_with_different_update_authority() { + let mut context = program_test().start_with_context().await; + + let collection = Keypair::new(); + let payer = Keypair::new(); + airdrop(&mut context, &payer.pubkey(), LAMPORTS_PER_SOL) + .await + .unwrap(); + let update_authority = Keypair::new(); + create_collection( + &mut context, + CreateCollectionHelperArgs { + collection: &collection, + update_authority: Some(update_authority.pubkey()), + payer: Some(&payer), + name: None, + uri: None, + plugins: vec![], + }, + ) + .await + .unwrap(); + + assert_collection( + &mut context, + AssertCollectionHelperArgs { + collection: collection.pubkey(), + update_authority: update_authority.pubkey(), + name: None, + uri: None, + num_minted: 0, + current_size: 0, + plugins: vec![], + }, + ) + .await; +} + +#[tokio::test] +async fn create_collection_with_plugins_with_different_plugin_authority() { + let mut context = program_test().start_with_context().await; + + let collection = Keypair::new(); + let update_authority = context.payer.pubkey(); + let royalties_authority = Keypair::new(); + create_collection( + &mut context, + CreateCollectionHelperArgs { + collection: &collection, + update_authority: Some(update_authority), + payer: None, + name: None, + uri: None, + plugins: vec![PluginAuthorityPair { + authority: Some(PluginAuthority::Address { + address: royalties_authority.pubkey(), + }), + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: update_authority, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }], + }, + ) + .await + .unwrap(); + + assert_collection( + &mut context, + AssertCollectionHelperArgs { + collection: collection.pubkey(), + update_authority, + name: None, + uri: None, + num_minted: 0, + current_size: 0, + plugins: vec![PluginAuthorityPair { + authority: Some(PluginAuthority::Address { + address: royalties_authority.pubkey(), + }), + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: update_authority, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }], + }, + ) + .await; +} diff --git a/clients/rust/tests/plugins.rs b/clients/rust/tests/plugins.rs new file mode 100644 index 00000000..65510768 --- /dev/null +++ b/clients/rust/tests/plugins.rs @@ -0,0 +1,319 @@ +#![cfg(feature = "test-sbf")] +pub mod setup; +use mpl_core::{ + accounts::{BaseAssetV1, PluginHeaderV1}, + fetch_plugin, fetch_plugins, list_plugins, + types::{ + Creator, FreezeDelegate, Plugin, PluginAuthority, PluginAuthorityPair, PluginType, + RegistryRecord, Royalties, RuleSet, UpdateAuthority, + }, + DataBlob, +}; +pub use setup::*; + +use solana_program::account_info::AccountInfo; +use solana_program_test::tokio; +use solana_sdk::{signature::Keypair, signer::Signer}; +use std::mem::size_of; + +#[tokio::test] +async fn test_fetch_plugin() { + let mut context = program_test().start_with_context().await; + + let asset = Keypair::new(); + let creator = context.payer.pubkey(); + create_asset( + &mut context, + CreateAssetHelperArgs { + owner: None, + payer: None, + asset: &asset, + data_state: None, + name: None, + uri: None, + authority: None, + update_authority: None, + collection: None, + plugins: vec![ + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }), + authority: None, + }, + PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: creator, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }, + ], + }, + ) + .await + .unwrap(); + + let owner = context.payer.pubkey(); + let update_authority = context.payer.pubkey(); + assert_asset( + &mut context, + AssertAssetHelperArgs { + asset: asset.pubkey(), + owner, + update_authority: Some(UpdateAuthority::Address(update_authority)), + name: None, + uri: None, + plugins: vec![ + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }), + authority: Some(PluginAuthority::Owner), + }, + PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: creator, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }, + ], + }, + ) + .await; + + let mut asset_account = context + .banks_client + .get_account(asset.pubkey()) + .await + .expect("get_account") + .expect("asset account not found"); + + let asset_pubkey = asset.pubkey(); + let mut lamports = 1_000_000_000; + let account_info = AccountInfo::new( + &asset_pubkey, + false, + false, + &mut lamports, + &mut asset_account.data, + &asset_account.owner, + false, + 1_000_000_000, + ); + + let plugin = + fetch_plugin::(&account_info, PluginType::FreezeDelegate) + .unwrap(); + + let expected_plugin_offset = BaseAssetV1::from_bytes(&asset_account.data) + .unwrap() + .get_size() + + PluginHeaderV1::LEN; + + let expected = ( + PluginAuthority::Owner, + FreezeDelegate { frozen: false }, + expected_plugin_offset, + ); + assert_eq!(plugin, expected); +} + +#[tokio::test] +async fn test_fetch_plugins() { + let mut context = program_test().start_with_context().await; + + let asset = Keypair::new(); + let creator = context.payer.pubkey(); + create_asset( + &mut context, + CreateAssetHelperArgs { + owner: None, + payer: None, + asset: &asset, + data_state: None, + name: None, + uri: None, + authority: None, + update_authority: None, + collection: None, + plugins: vec![ + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }), + authority: None, + }, + PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: creator, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }, + ], + }, + ) + .await + .unwrap(); + + let owner = context.payer.pubkey(); + let update_authority = context.payer.pubkey(); + assert_asset( + &mut context, + AssertAssetHelperArgs { + asset: asset.pubkey(), + owner, + update_authority: Some(UpdateAuthority::Address(update_authority)), + name: None, + uri: None, + plugins: vec![ + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }), + authority: Some(PluginAuthority::Owner), + }, + PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: creator, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }, + ], + }, + ) + .await; + + let asset_account = context + .banks_client + .get_account(asset.pubkey()) + .await + .expect("get_account") + .expect("asset account not found"); + + let plugins = fetch_plugins(&asset_account.data).unwrap(); + + let expected_first_plugin_offset = BaseAssetV1::from_bytes(&asset_account.data) + .unwrap() + .get_size() + + PluginHeaderV1::LEN; + + let first_expected_registry_record = RegistryRecord { + plugin_type: PluginType::FreezeDelegate, + authority: PluginAuthority::Owner, + offset: expected_first_plugin_offset as u64, + }; + + let expected_second_plugin_offset = + expected_first_plugin_offset + size_of::() + size_of::(); + + let second_expected_registry_record = RegistryRecord { + plugin_type: PluginType::Royalties, + authority: PluginAuthority::UpdateAuthority, + offset: expected_second_plugin_offset as u64, + }; + + assert_eq!( + plugins, + vec![ + first_expected_registry_record, + second_expected_registry_record + ] + ) +} + +#[tokio::test] +async fn test_list_plugins() { + let mut context = program_test().start_with_context().await; + + let asset = Keypair::new(); + let creator = context.payer.pubkey(); + create_asset( + &mut context, + CreateAssetHelperArgs { + owner: None, + payer: None, + asset: &asset, + data_state: None, + name: None, + uri: None, + authority: None, + update_authority: None, + collection: None, + plugins: vec![ + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }), + authority: None, + }, + PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: creator, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }, + ], + }, + ) + .await + .unwrap(); + + let owner = context.payer.pubkey(); + let update_authority = context.payer.pubkey(); + assert_asset( + &mut context, + AssertAssetHelperArgs { + asset: asset.pubkey(), + owner, + update_authority: Some(UpdateAuthority::Address(update_authority)), + name: None, + uri: None, + plugins: vec![ + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }), + authority: Some(PluginAuthority::Owner), + }, + PluginAuthorityPair { + authority: None, + plugin: Plugin::Royalties(Royalties { + basis_points: 500, + creators: vec![Creator { + address: creator, + percentage: 100, + }], + rule_set: RuleSet::ProgramDenyList(vec![]), + }), + }, + ], + }, + ) + .await; + + let asset_account = context + .banks_client + .get_account(asset.pubkey()) + .await + .expect("get_account") + .expect("asset account not found"); + + let plugins = list_plugins(&asset_account.data).unwrap(); + assert_eq!( + plugins, + vec![PluginType::FreezeDelegate, PluginType::Royalties] + ) +} diff --git a/clients/rust/tests/setup/mod.rs b/clients/rust/tests/setup/mod.rs index e416a48b..40b6fb04 100644 --- a/clients/rust/tests/setup/mod.rs +++ b/clients/rust/tests/setup/mod.rs @@ -1,7 +1,7 @@ use mpl_core::{ - instructions::CreateV1Builder, + instructions::{CreateCollectionV1Builder, CreateV1Builder}, types::{DataState, Key, Plugin, PluginAuthorityPair, UpdateAuthority}, - Asset, + Asset, Collection, }; use solana_program_test::{BanksClientError, ProgramTest, ProgramTestContext}; use solana_sdk::{ @@ -15,6 +15,8 @@ pub fn program_test() -> ProgramTest { const DEFAULT_ASSET_NAME: &str = "Test Asset"; const DEFAULT_ASSET_URI: &str = "https://example.com/asset"; +const DEFAULT_COLLECTION_NAME: &str = "Test Collection"; +const DEFAULT_COLLECTION_URI: &str = "https://example.com/collection"; #[derive(Debug)] pub struct CreateAssetHelperArgs<'a> { @@ -85,6 +87,10 @@ pub async fn assert_asset(context: &mut ProgramTestContext, input: AssertAssetHe let asset = Asset::from_bytes(&asset_account.data).unwrap(); assert_eq!(asset.base.key, Key::AssetV1); + assert_eq!(asset.base.owner, input.owner); + if let Some(update_authority) = input.update_authority { + assert_eq!(asset.base.update_authority, update_authority); + } assert_eq!( asset.base.name, input.name.unwrap_or(DEFAULT_ASSET_NAME.to_owned()) @@ -93,10 +99,6 @@ pub async fn assert_asset(context: &mut ProgramTestContext, input: AssertAssetHe asset.base.uri, input.uri.unwrap_or(DEFAULT_ASSET_URI.to_owned()) ); - assert_eq!(asset.base.owner, input.owner); - if let Some(update_authority) = input.update_authority { - assert_eq!(asset.base.update_authority, update_authority); - } for plugin in input.plugins { match plugin { @@ -124,6 +126,111 @@ pub async fn assert_asset(context: &mut ProgramTestContext, input: AssertAssetHe } } } + +#[derive(Debug)] +pub struct CreateCollectionHelperArgs<'a> { + pub collection: &'a Keypair, + pub update_authority: Option, + pub payer: Option<&'a Keypair>, + pub name: Option, + pub uri: Option, + // TODO use PluginList type here + pub plugins: Vec, +} + +pub async fn create_collection<'a>( + context: &mut ProgramTestContext, + input: CreateCollectionHelperArgs<'a>, +) -> Result<(), BanksClientError> { + let payer = input.payer.unwrap_or(&context.payer); + let create_ix = CreateCollectionV1Builder::new() + .collection(input.collection.pubkey()) + .update_authority(input.update_authority) + .payer(payer.pubkey()) + .system_program(system_program::ID) + .name(input.name.unwrap_or(DEFAULT_COLLECTION_NAME.to_owned())) + .uri(input.uri.unwrap_or(DEFAULT_COLLECTION_URI.to_owned())) + .plugins(input.plugins) + .instruction(); + + let mut signers = vec![input.collection, &context.payer]; + if let Some(payer) = input.payer { + signers.push(payer); + } + + let tx = Transaction::new_signed_with_payer( + &[create_ix], + Some(&context.payer.pubkey()), + signers.as_slice(), + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await +} + +pub struct AssertCollectionHelperArgs { + pub collection: Pubkey, + pub update_authority: Pubkey, + pub name: Option, + pub uri: Option, + pub num_minted: u32, + pub current_size: u32, + // TODO use PluginList type here + pub plugins: Vec, +} + +pub async fn assert_collection( + context: &mut ProgramTestContext, + input: AssertCollectionHelperArgs, +) { + let collection_account = context + .banks_client + .get_account(input.collection) + .await + .expect("get_account") + .expect("collection account not found"); + + let collection = Collection::from_bytes(&collection_account.data).unwrap(); + assert_eq!(collection.base.key, Key::CollectionV1); + assert_eq!(collection.base.update_authority, input.update_authority); + assert_eq!( + collection.base.name, + input.name.unwrap_or(DEFAULT_COLLECTION_NAME.to_owned()) + ); + assert_eq!( + collection.base.uri, + input.uri.unwrap_or(DEFAULT_COLLECTION_URI.to_owned()) + ); + assert_eq!(collection.base.num_minted, input.num_minted); + assert_eq!(collection.base.current_size, input.current_size); + + for plugin in input.plugins { + match plugin { + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(freeze), + authority, + } => { + let plugin = collection.plugin_list.freeze_delegate.clone().unwrap(); + if let Some(authority) = authority { + assert_eq!(plugin.base.authority, authority.into()); + } + assert_eq!(plugin.freeze_delegate, freeze); + } + PluginAuthorityPair { + plugin: Plugin::Royalties(royalties), + authority, + } => { + let plugin = collection.plugin_list.royalties.clone().unwrap(); + if let Some(authority) = authority { + assert_eq!(plugin.base.authority, authority.into()); + } + assert_eq!(plugin.royalties, royalties); + } + _ => panic!("unsupported plugin type"), + } + } +} + pub async fn airdrop( context: &mut ProgramTestContext, receiver: &Pubkey, diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 1b47b629..bc62e119 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -181,7 +181,7 @@ pub fn fetch_plugins(account: &AccountInfo) -> Result, Progr Ok(registry) } -/// Create plugin header and registry if it doesn't exist +/// List all plugins in an account. pub fn list_plugins(account: &AccountInfo) -> Result, ProgramError> { let asset = AssetV1::load(account, 0)?;