diff --git a/idls/nifty_asset_interface.json b/idls/nifty_asset_interface.json index 15f4432d..a8a86cba 100644 --- a/idls/nifty_asset_interface.json +++ b/idls/nifty_asset_interface.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "0.2.1", "name": "nifty_asset_interface", "instructions": [ { @@ -109,6 +109,15 @@ "Asset account of the group" ] }, + { + "name": "groupAuthority", + "isMut": false, + "isSigner": true, + "isOptional": true, + "docs": [ + "The delegate authority for minting assets into a group" + ] + }, { "name": "payer", "isMut": true, diff --git a/programs/asset/interface/src/generated/instructions/create.rs b/programs/asset/interface/src/generated/instructions/create.rs index 8cf15991..5000267f 100644 --- a/programs/asset/interface/src/generated/instructions/create.rs +++ b/programs/asset/interface/src/generated/instructions/create.rs @@ -20,6 +20,8 @@ pub struct Create { pub owner: solana_program::pubkey::Pubkey, /// Asset account of the group pub group: Option, + /// The delegate authority for minting assets into a group + pub group_authority: Option, /// The account paying for the storage fees pub payer: Option, /// The system program @@ -39,7 +41,7 @@ impl Create { args: CreateInstructionArgs, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { - let mut accounts = Vec::with_capacity(6 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( self.asset, true, )); @@ -58,6 +60,17 @@ impl Create { false, )); } + if let Some(group_authority) = self.group_authority { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + group_authority, + true, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::INTERFACE_ID, + false, + )); + } if let Some(payer) = self.payer { accounts.push(solana_program::instruction::AccountMeta::new(payer, true)); } else { @@ -118,14 +131,16 @@ pub struct CreateInstructionArgs { /// 1. `[signer]` authority /// 2. `[]` owner /// 3. `[writable, optional]` group -/// 4. `[writable, signer, optional]` payer -/// 5. `[optional]` system_program +/// 4. `[signer, optional]` group_authority +/// 5. `[writable, signer, optional]` payer +/// 6. `[optional]` system_program #[derive(Default)] pub struct CreateBuilder { asset: Option, authority: Option<(solana_program::pubkey::Pubkey, bool)>, owner: Option, group: Option, + group_authority: Option, payer: Option, system_program: Option, name: Option, @@ -169,6 +184,16 @@ impl CreateBuilder { self } /// `[optional account]` + /// The delegate authority for minting assets into a group + #[inline(always)] + pub fn group_authority( + &mut self, + group_authority: Option, + ) -> &mut Self { + self.group_authority = group_authority; + self + } + /// `[optional account]` /// The account paying for the storage fees #[inline(always)] pub fn payer(&mut self, payer: Option) -> &mut Self { @@ -233,6 +258,7 @@ impl CreateBuilder { authority: self.authority.expect("authority is not set"), owner: self.owner.expect("owner is not set"), group: self.group, + group_authority: self.group_authority, payer: self.payer, system_program: self.system_program, }; @@ -257,6 +283,8 @@ pub struct CreateCpiAccounts<'a, 'b> { pub owner: &'b solana_program::account_info::AccountInfo<'a>, /// Asset account of the group pub group: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The delegate authority for minting assets into a group + pub group_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The account paying for the storage fees pub payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The system program @@ -275,6 +303,8 @@ pub struct CreateCpi<'a, 'b> { pub owner: &'b solana_program::account_info::AccountInfo<'a>, /// Asset account of the group pub group: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The delegate authority for minting assets into a group + pub group_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The account paying for the storage fees pub payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The system program @@ -295,6 +325,7 @@ impl<'a, 'b> CreateCpi<'a, 'b> { authority: accounts.authority, owner: accounts.owner, group: accounts.group, + group_authority: accounts.group_authority, payer: accounts.payer, system_program: accounts.system_program, __args: args, @@ -333,7 +364,7 @@ impl<'a, 'b> CreateCpi<'a, 'b> { bool, )], ) -> solana_program::entrypoint::ProgramResult { - let mut accounts = Vec::with_capacity(6 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( *self.asset.key, true, @@ -356,6 +387,17 @@ impl<'a, 'b> CreateCpi<'a, 'b> { false, )); } + if let Some(group_authority) = self.group_authority { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *group_authority.key, + true, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::INTERFACE_ID, + false, + )); + } if let Some(payer) = self.payer { accounts.push(solana_program::instruction::AccountMeta::new( *payer.key, true, @@ -393,7 +435,7 @@ impl<'a, 'b> CreateCpi<'a, 'b> { accounts, data, }; - let mut account_infos = Vec::with_capacity(6 + 1 + remaining_accounts.len()); + let mut account_infos = Vec::with_capacity(7 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.asset.clone()); account_infos.push(self.authority.0.clone()); @@ -401,6 +443,9 @@ impl<'a, 'b> CreateCpi<'a, 'b> { if let Some(group) = self.group { account_infos.push(group.clone()); } + if let Some(group_authority) = self.group_authority { + account_infos.push(group_authority.clone()); + } if let Some(payer) = self.payer { account_infos.push(payer.clone()); } @@ -427,8 +472,9 @@ impl<'a, 'b> CreateCpi<'a, 'b> { /// 1. `[signer]` authority /// 2. `[]` owner /// 3. `[writable, optional]` group -/// 4. `[writable, signer, optional]` payer -/// 5. `[optional]` system_program +/// 4. `[signer, optional]` group_authority +/// 5. `[writable, signer, optional]` payer +/// 6. `[optional]` system_program pub struct CreateCpiBuilder<'a, 'b> { instruction: Box>, } @@ -441,6 +487,7 @@ impl<'a, 'b> CreateCpiBuilder<'a, 'b> { authority: None, owner: None, group: None, + group_authority: None, payer: None, system_program: None, name: None, @@ -484,6 +531,16 @@ impl<'a, 'b> CreateCpiBuilder<'a, 'b> { self } /// `[optional account]` + /// The delegate authority for minting assets into a group + #[inline(always)] + pub fn group_authority( + &mut self, + group_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.group_authority = group_authority; + self + } + /// `[optional account]` /// The account paying for the storage fees #[inline(always)] pub fn payer( @@ -588,6 +645,8 @@ impl<'a, 'b> CreateCpiBuilder<'a, 'b> { group: self.instruction.group, + group_authority: self.instruction.group_authority, + payer: self.instruction.payer, system_program: self.instruction.system_program, @@ -606,6 +665,7 @@ struct CreateCpiBuilderInstruction<'a, 'b> { authority: Option<(&'b solana_program::account_info::AccountInfo<'a>, bool)>, owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, group: Option<&'b solana_program::account_info::AccountInfo<'a>>, + group_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, name: Option, diff --git a/programs/asset/interface/src/interface.rs b/programs/asset/interface/src/interface.rs index 3c8d25f1..a939c424 100644 --- a/programs/asset/interface/src/interface.rs +++ b/programs/asset/interface/src/interface.rs @@ -159,6 +159,9 @@ pub struct MetadataInput { /// Indicates whether the asset is mutable or not. pub mutable: bool, + + /// Extensions to be added to the asset. + pub extensions: Option>, } #[repr(C)] diff --git a/programs/asset/program/src/processor/group.rs b/programs/asset/program/src/processor/group.rs index 3109ce7a..39e78c71 100644 --- a/programs/asset/program/src/processor/group.rs +++ b/programs/asset/program/src/processor/group.rs @@ -1,5 +1,5 @@ use nifty_asset_types::{ - extensions::GroupingMut, + extensions::{Extension, GroupingMut}, podded::{pod::PodOption, ZeroCopy}, state::{Asset, Discriminator}, }; @@ -64,8 +64,8 @@ pub fn process_group(program_id: &Pubkey, ctx: Context) -> Progra "asset" ); - let group = Asset::load_mut(&mut group_data); - let group_authority = group.authority; + let (group, extensions) = group_data.split_at_mut(Asset::LEN); + let group = Asset::load_mut(group); // authority of the group must match the asset require!( @@ -74,7 +74,7 @@ pub fn process_group(program_id: &Pubkey, ctx: Context) -> Progra "Group and asset authority mismatch" ); - let grouping = if let Some(grouping) = Asset::get_mut::(&mut group_data) { + let grouping = if let Some(grouping) = Extension::get_mut::(extensions) { grouping } else { return err!( @@ -84,18 +84,18 @@ pub fn process_group(program_id: &Pubkey, ctx: Context) -> Progra }; // if the signing authority doesn't match the group authority - if *ctx.accounts.authority.key != group_authority { + if *ctx.accounts.authority.key != group.authority { // then the authority must match the grouping delegate if let Some(delegate) = grouping.delegate.value() { require!( - *delegate == ctx.accounts.authority.key.into(), + **delegate == *ctx.accounts.authority.key, AssetError::InvalidAuthority, - "group authority delegate mismatch" + "Group authority or delegate mismatch" ); } else { return err!( AssetError::InvalidAuthority, - "missing group authority delegate" + "Invalid group authority delegate" ); }; } diff --git a/programs/asset/program/src/processor/ungroup.rs b/programs/asset/program/src/processor/ungroup.rs index 22a3aad6..b1192398 100644 --- a/programs/asset/program/src/processor/ungroup.rs +++ b/programs/asset/program/src/processor/ungroup.rs @@ -1,5 +1,5 @@ use nifty_asset_types::{ - extensions::GroupingMut, + extensions::{Extension, GroupingMut}, podded::{pod::PodOption, ZeroCopy}, state::{Asset, Discriminator}, }; @@ -56,9 +56,10 @@ pub fn process_ungroup(program_id: &Pubkey, ctx: Context) -> Pr "asset" ); - let group = Asset::load_mut(&mut group_data); let asset = Asset::load_mut(&mut asset_data); - let group_authority = group.authority; + + let (group, extensions) = group_data.split_at_mut(Asset::LEN); + let group = Asset::load_mut(group); // asset must be in the group require!( @@ -67,7 +68,7 @@ pub fn process_ungroup(program_id: &Pubkey, ctx: Context) -> Pr "asset group mismatch" ); - let grouping = if let Some(grouping) = Asset::get_mut::(&mut group_data) { + let grouping = if let Some(grouping) = Extension::get_mut::(extensions) { grouping } else { return err!( @@ -77,18 +78,18 @@ pub fn process_ungroup(program_id: &Pubkey, ctx: Context) -> Pr }; // if the signing authority doesn't match the group authority - if *ctx.accounts.authority.key != group_authority { + if *ctx.accounts.authority.key != group.authority { // then the authority must match the grouping delegate if let Some(delegate) = grouping.delegate.value() { require!( - *delegate == ctx.accounts.authority.key.into(), + **delegate == *ctx.accounts.authority.key, AssetError::InvalidAuthority, - "group authority delegate mismatch" + "Group authority or delegate mismatch" ); } else { return err!( AssetError::InvalidAuthority, - "missing group authority delegate" + "Invalid group authority delegate" ); }; } diff --git a/programs/asset/types/src/extensions/grouping.rs b/programs/asset/types/src/extensions/grouping.rs index b4679477..505dcefd 100644 --- a/programs/asset/types/src/extensions/grouping.rs +++ b/programs/asset/types/src/extensions/grouping.rs @@ -157,7 +157,7 @@ impl GroupingBuilder { } /// Add a new attribute to the extension. - pub fn set_max_size(&mut self, max_size: Option) -> &mut Self { + pub fn set(&mut self, max_size: Option, delegate: Option) -> &mut Self { // setting the data replaces any existing data self.0.clear(); @@ -165,22 +165,12 @@ impl GroupingBuilder { self.0 .extend_from_slice(&u64::to_le_bytes(max_size.unwrap_or(0))); - self.0.extend_from_slice(&Pubkey::default().to_bytes()); - - self - } - - /// Add a delegate. - pub fn set_delegate(&mut self, delegate: PodOption) -> &mut Self { - let offset = std::mem::size_of::() * 2 as usize; - - let slice: &mut [u8] = &mut self.0[offset..offset + std::mem::size_of::()]; - slice.copy_from_slice( - &delegate - .value() - .unwrap_or(&NullablePubkey::default()) - .to_bytes(), - ); + let delegate = if let Some(delegate) = delegate { + delegate.to_bytes() + } else { + Pubkey::default().to_bytes() + }; + self.0.extend_from_slice(&delegate); self } @@ -206,7 +196,6 @@ impl Deref for GroupingBuilder { #[cfg(test)] mod tests { - use podded::pod::PodOption; use solana_program::sysvar; use crate::{ @@ -218,7 +207,7 @@ mod tests { fn test_set_max_size() { // max_size set let mut builder = GroupingBuilder::default(); - builder.set_max_size(Some(10)); + builder.set(Some(10), None); let grouping = builder.build(); assert_eq!(*grouping.size, 0); @@ -241,7 +230,7 @@ mod tests { fn test_set_delegate() { // set delegate to a pubkey let mut builder = GroupingBuilder::default(); - builder.set_delegate(PodOption::new(NullablePubkey::new(sysvar::ID))); + builder.set(None, Some(sysvar::ID)); let grouping = builder.build(); assert!(grouping.delegate.value().is_some()); @@ -251,7 +240,7 @@ mod tests { ); // set delegate to None - builder.set_delegate(PodOption::new(NullablePubkey::default())); + builder.set(None, None); let grouping = builder.build(); assert!(grouping.delegate.value().is_none()); diff --git a/programs/asset/types/src/extensions/mod.rs b/programs/asset/types/src/extensions/mod.rs index a3cc51b9..f7716824 100644 --- a/programs/asset/types/src/extensions/mod.rs +++ b/programs/asset/types/src/extensions/mod.rs @@ -132,6 +132,30 @@ impl Extension { None } + + /// Returns a mutable reference to the extension data of a given type. + /// + /// This function expects a slice of bytes of extension data only and it will return the first + /// extension of the given type; if the extension type is not found, `None` is returned. + pub fn get_mut<'a, T: ExtensionDataMut<'a>>(data: &'a mut [u8]) -> Option { + let mut cursor = 0; + + while (cursor + Extension::LEN) <= data.len() { + let extension = Extension::load(&data[cursor..cursor + Extension::LEN]); + + match extension.try_extension_type() { + Ok(t) if t == T::TYPE => { + let start = cursor + Extension::LEN; + let end = start + extension.length() as usize; + return Some(T::from_bytes_mut(&mut data[start..end])); + } + Ok(ExtensionType::None) => return None, + _ => cursor = extension.boundary() as usize - Asset::LEN, + } + } + + None + } } impl Debug for Extension {