From 5f7b7aa96c63f27d80582585207d5204c3f5097c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 30 Mar 2023 00:16:26 -0700 Subject: [PATCH 01/40] Additional Metadata Delegates * Add support in Delegate and Revoke for Authority, Data, CollectionItem, and ProgrammableConfigItem Metadata delegates. * Remove Update Metadata delegate. --- .../program/src/instruction/delegate.rs | 48 +++++++++++++------ .../program/src/instruction/metadata.rs | 4 +- .../src/processor/delegate/delegate.rs | 26 ++++++++-- .../program/src/processor/delegate/revoke.rs | 13 +++-- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 07bad02a46..667e088b6a 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -11,25 +11,42 @@ use solana_program::{ use super::InstructionBuilder; use crate::{instruction::MetadataInstruction, processor::AuthorizationData}; +/// Delegate args can specify Metadata delegates and Token delegates. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum DelegateArgs { + AuthorityV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + DataV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, CollectionV1 { /// Required authorization data to validate the request. authorization_data: Option, }, - SaleV1 { - amount: u64, + CollectionItemV1 { /// Required authorization data to validate the request. authorization_data: Option, }, - TransferV1 { + ProgrammableConfigV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + ProgrammableConfigItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + SaleV1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option, }, - UpdateV1 { + TransferV1 { + amount: u64, /// Required authorization data to validate the request. authorization_data: Option, }, @@ -53,25 +70,24 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - ProgrammableConfigV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, } #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum RevokeArgs { + AuthorityV1, + DataV1, CollectionV1, + CollectionItemV1, + ProgrammableConfigV1, + ProgrammableConfigItemV1, SaleV1, TransferV1, - UpdateV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, - ProgrammableConfigV1, MigrationV1, } @@ -80,20 +96,24 @@ pub enum RevokeArgs { #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum MetadataDelegateRole { Authority, - Collection, + Data, Use, - Update, + Collection, + CollectionItem, ProgrammableConfig, + ProgrammableConfigItem, } impl fmt::Display for MetadataDelegateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self { Self::Authority => "authority_delegate".to_string(), - Self::Collection => "collection_delegate".to_string(), + Self::Data => "data_delegate".to_string(), Self::Use => "use_delegate".to_string(), - Self::Update => "update_delegate".to_string(), + Self::Collection => "collection_delegate".to_string(), + Self::CollectionItem => "collection_item_delegate".to_string(), Self::ProgrammableConfig => "programmable_config_delegate".to_string(), + Self::ProgrammableConfigItem => "prog_config_item_delegate".to_string(), }; write!(f, "{message}") diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 1d80a5083e..894812c0b8 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -72,8 +72,8 @@ pub enum TransferArgs { /// Struct representing the values to be updated for an `update` instructions. /// /// Values that are set to 'None' are not changed; any value set to `Some(_)` will -/// have its value updated. There are properties that have three valid states, which -/// allow the value to remaing the same, to be cleared or to set a new value. +/// have its value updated. There are properties that have three valid states, and +/// use a "toggle" type that allows the value to be set, cleared, or remain the same. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index c6e632a0b4..1e555c137a 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -36,17 +36,21 @@ impl Display for DelegateScenario { let message = match self { Self::Metadata(role) => match role { MetadataDelegateRole::Authority => "Authority".to_string(), - MetadataDelegateRole::Collection => "Collection".to_string(), + MetadataDelegateRole::Data => "Data".to_string(), MetadataDelegateRole::Use => "Use".to_string(), - MetadataDelegateRole::Update => "Update".to_string(), + MetadataDelegateRole::Collection => "Collection".to_string(), + MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), MetadataDelegateRole::ProgrammableConfig => "ProgrammableConfig".to_string(), + MetadataDelegateRole::ProgrammableConfigItem => { + "ProgrammableConfigItem".to_string() + } }, Self::Token(role) => match role { TokenDelegateRole::Sale => "Sale".to_string(), TokenDelegateRole::Transfer => "Transfer".to_string(), - TokenDelegateRole::LockedTransfer => "LockedTransfer".to_string(), TokenDelegateRole::Utility => "Utility".to_string(), TokenDelegateRole::Staking => "Staking".to_string(), + TokenDelegateRole::LockedTransfer => "LockedTransfer".to_string(), _ => panic!("Invalid delegate role"), }, }; @@ -115,15 +119,27 @@ pub fn delegate<'a>( // checks if it is a MetadataDelegate creation let delegate_args = match &args { + DelegateArgs::AuthorityV1 { authorization_data } => { + Some((MetadataDelegateRole::Authority, authorization_data)) + } + DelegateArgs::DataV1 { authorization_data } => { + Some((MetadataDelegateRole::Data, authorization_data)) + } + DelegateArgs::CollectionV1 { authorization_data } => { Some((MetadataDelegateRole::Collection, authorization_data)) } - DelegateArgs::UpdateV1 { authorization_data } => { - Some((MetadataDelegateRole::Update, authorization_data)) + DelegateArgs::CollectionItemV1 { authorization_data } => { + Some((MetadataDelegateRole::CollectionItem, authorization_data)) } DelegateArgs::ProgrammableConfigV1 { authorization_data } => { Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) } + DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => Some(( + MetadataDelegateRole::ProgrammableConfigItem, + authorization_data, + )), + // we don't need to fail if did not find a match at this point _ => None, }; diff --git a/token-metadata/program/src/processor/delegate/revoke.rs b/token-metadata/program/src/processor/delegate/revoke.rs index 827e39925f..585a50ea29 100644 --- a/token-metadata/program/src/processor/delegate/revoke.rs +++ b/token-metadata/program/src/processor/delegate/revoke.rs @@ -33,16 +33,16 @@ pub fn revoke<'a>( RevokeArgs::SaleV1 => Some(TokenDelegateRole::Sale), // Transfer RevokeArgs::TransferV1 => Some(TokenDelegateRole::Transfer), - // LockedTransfer - RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), // Utility RevokeArgs::UtilityV1 => Some(TokenDelegateRole::Utility), // Staking RevokeArgs::StakingV1 => Some(TokenDelegateRole::Staking), - // Migration - RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // Standard RevokeArgs::StandardV1 => Some(TokenDelegateRole::Standard), + // LockedTransfer + RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), + // Migration + RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // we don't need to fail if did not find a match at this point _ => None, }; @@ -54,9 +54,12 @@ pub fn revoke<'a>( // checks if it is a MetadataDelegate creation let metadata_delegate = match &args { + RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), + RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), - RevokeArgs::UpdateV1 => Some(MetadataDelegateRole::Update), + RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), + RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point _ => None, }; From 12ce7d72d3290f82f104983f1cd58f35399a505c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Mon, 3 Apr 2023 09:00:48 -0700 Subject: [PATCH 02/40] Changes to Update to support new and changed delegates * Modify authority check to separate out item and collection-level delegates. * Add V2 Update args struct to allow user to specify token standard. * Check that new delegates are only changing metadata for which they are meant to have access. * Modify Update handler to update metadata fields based on the delegate type. --- .../program/src/instruction/metadata.rs | 33 ++- .../program/src/processor/metadata/update.rs | 221 +++++++++++++++--- .../src/processor/verification/collection.rs | 5 +- token-metadata/program/src/state/metadata.rs | 82 +++++-- .../program/src/state/programmable.rs | 13 +- token-metadata/program/src/utils/mod.rs | 5 + 6 files changed, 298 insertions(+), 61 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 894812c0b8..e6f057d5ea 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -15,7 +15,7 @@ use crate::{ processor::AuthorizationData, state::{ AssetData, Collection, CollectionDetails, Creator, Data, DataV2, MigrationType, - PrintSupply, Uses, + PrintSupply, TokenStandard, Uses, }, }; @@ -100,20 +100,45 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + V2 { + /// The new update authority. + new_update_authority: Option, + /// The metadata details. + data: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + is_mutable: Option, + /// Collection information. + collection: CollectionToggle, + /// Additional details of the collection. + collection_details: CollectionDetailsToggle, + /// Uses information. + uses: UsesToggle, + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + /// Token standard. + token_standard: Option, + /// Required authorization data to validate the request. + authorization_data: Option, + }, } impl Default for UpdateArgs { fn default() -> Self { - Self::V1 { - authorization_data: None, + Self::V2 { new_update_authority: None, data: None, primary_sale_happened: None, is_mutable: None, collection: CollectionToggle::None, - uses: UsesToggle::None, collection_details: CollectionDetailsToggle::None, + uses: UsesToggle::None, rule_set: RuleSetToggle::None, + token_standard: None, + authorization_data: None, } } } diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 29ce52e35e..859f08a553 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -16,7 +16,7 @@ use crate::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, ProgrammableConfig, TokenMetadataAccount, TokenStandard, }, - utils::{assert_derivation, check_token_standard}, + utils::{assert_derivation, check_token_standard, mint_decimals_is_zero}, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -45,6 +45,7 @@ pub fn update<'a>( match args { UpdateArgs::V1 { .. } => update_v1(program_id, context, args), + UpdateArgs::V2 { .. } => update_v1(program_id, context, args), } } @@ -63,7 +64,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro if let Some(edition) = ctx.accounts.edition_info { assert_owned_by(edition, program_id)?; - // checks that we got the correct master account + // checks that we got the correct edition account assert_derivation( program_id, edition, @@ -114,12 +115,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro return Err(MetadataError::MintMismatch.into()); } - let token_standard = if let Some(token_standard) = metadata.token_standard { - token_standard - } else { - check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? - }; - let (token_pubkey, token) = if let Some(token_info) = ctx.accounts.token_info { ( Some(token_info.key), @@ -152,7 +147,18 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro token: token_pubkey, token_account: token.as_ref(), metadata_delegate_record_info: ctx.accounts.delegate_record_info, - metadata_delegate_roles: vec![MetadataDelegateRole::ProgrammableConfig], + metadata_delegate_roles: vec![ + MetadataDelegateRole::Authority, + MetadataDelegateRole::Data, + MetadataDelegateRole::Collection, + MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::ProgrammableConfigItem, + ], + collection_metadata_delegate_roles: vec![ + MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, + ], precedence: &[ AuthorityType::Metadata, AuthorityType::MetadataDelegate, @@ -161,6 +167,34 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ..Default::default() })?; + // Find existing token standard from metadata or infer it. + let existing_or_inferred_token_standard = if let Some(token_standard) = metadata.token_standard + { + token_standard + } else { + check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? + }; + + // Check if caller passed in a desired token standard. + let desired_token_standard = match args { + UpdateArgs::V1 { .. } => None, + UpdateArgs::V2 { token_standard, .. } => token_standard, + }; + + // If there is a desired token standard, use it if it passes the check. If there is not a + // desired token standard, use the existing or inferred token standard. + let token_standard = match desired_token_standard { + Some(desired_token_standard) => { + check_desired_token_standard( + mint_decimals_is_zero(ctx.accounts.mint_info)?, + existing_or_inferred_token_standard, + desired_token_standard, + )?; + desired_token_standard + } + None => existing_or_inferred_token_standard, + }; + // For pNFTs, we need to validate the authorization rules. if matches!(token_standard, TokenStandard::ProgrammableNonFungible) { // If the metadata account has a current rule set, we validate that @@ -174,6 +208,8 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } } + // Validate that authority has permission to update the fields that have been specified in the + // update args. validate_update(&args, &authority_type, metadata_delegate_role)?; // If we reach here without errors we have validated that the authority is allowed to @@ -199,58 +235,171 @@ fn validate_update( metadata_delegate_role: Option, ) -> ProgramResult { // validate the authority type - match authority_type { AuthorityType::Metadata => { - // metadata authority is the paramount (upadte) authority + // metadata authority is the paramount (update) authority msg!("Auth type: Metadata"); } - AuthorityType::MetadataDelegate => { - // support for delegate update - msg!("Auth type: Delegate"); - } AuthorityType::Holder => { // support for holder update msg!("Auth type: Holder"); return Err(MetadataError::FeatureNotSupported.into()); } + AuthorityType::MetadataDelegate => { + // support for delegate update + msg!("Auth type: Delegate"); + } _ => { return Err(MetadataError::InvalidAuthorityType.into()); } } - let UpdateArgs::V1 { + // Destructure args. + let ( + new_update_authority, data, primary_sale_happened, is_mutable, collection, - uses, - new_update_authority, collection_details, - .. - } = args; + uses, + rule_set, + token_standard, + ) = match args { + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } => ( + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + &None, + ), + UpdateArgs::V2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + .. + } => ( + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + ), + }; // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to - match metadata_delegate_role { - Some(MetadataDelegateRole::ProgrammableConfig) => { - // can only update the programmable config - if data.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection.is_some() - || uses.is_some() - || new_update_authority.is_some() - || collection_details.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); + if let Some(metadata_delegate_role) = metadata_delegate_role { + match metadata_delegate_role { + MetadataDelegateRole::Authority => { + // Fields allowed for `Authority`: + // `new_update_authority` + // `primary_sale_happened` + // `is_mutable` + // `token_standard` + if data.is_some() + || collection.is_some() + || collection_details.is_some() + || uses.is_some() + || rule_set.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } } + MetadataDelegateRole::Data => { + // Fields allowed for `Data`: + // `data` + if new_update_authority.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection.is_some() + || collection_details.is_some() + || uses.is_some() + || rule_set.is_some() + || token_standard.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } + + MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { + // Fields allowed for `Collection` and `CollectionItem`: + // `collection` + if new_update_authority.is_some() + || data.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection_details.is_some() + || uses.is_some() + || rule_set.is_some() + || token_standard.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } + MetadataDelegateRole::ProgrammableConfig + | MetadataDelegateRole::ProgrammableConfigItem => { + // Fields allowed for `ProgrammableConfig` and `ProgrammableConfigItem`: + // `rule_set` + if new_update_authority.is_some() + || data.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection.is_some() + || collection_details.is_some() + || uses.is_some() + || token_standard.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } + _ => return Err(MetadataError::InvalidAuthorityType.into()), } - Some(_) => { - return Err(MetadataError::InvalidAuthorityType.into()); - } - None => { /* no delegate role to check */ } } Ok(()) } + +fn check_desired_token_standard( + mint_decimals_is_zero: bool, + existing_or_inferred_token_standard: TokenStandard, + desired_token_standard: TokenStandard, +) -> ProgramResult { + // This code only allows switching between Fungible and FungibleAsset, and only when + // mint decimals is zero. + if !mint_decimals_is_zero { + return Err(MetadataError::InvalidTokenStandard.into()); + } + + match existing_or_inferred_token_standard { + TokenStandard::Fungible | TokenStandard::FungibleAsset => match desired_token_standard { + TokenStandard::Fungible | TokenStandard::FungibleAsset => Ok(()), + _ => Err(MetadataError::InvalidTokenStandard.into()), + }, + _ => Err(MetadataError::InvalidTokenStandard.into()), + } +} diff --git a/token-metadata/program/src/processor/verification/collection.rs b/token-metadata/program/src/processor/verification/collection.rs index a517e2f319..aea56548cd 100644 --- a/token-metadata/program/src/processor/verification/collection.rs +++ b/token-metadata/program/src/processor/verification/collection.rs @@ -164,7 +164,10 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context // or an update delegate for the item. This call fails if no valid authority is present. auth_request.mint = &metadata.mint; auth_request.update_authority = &metadata.update_authority; - auth_request.metadata_delegate_roles = vec![MetadataDelegateRole::Update]; + auth_request.metadata_delegate_roles = vec![ + MetadataDelegateRole::Collection, + MetadataDelegateRole::CollectionItem, + ]; AuthorityType::get_authority_type(auth_request) } else { // If the parent is not burned, we need to ensure the collection metadata account is owned diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index 32b33e71de..a43e427bff 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -91,17 +91,49 @@ impl Metadata { authority_type: AuthorityType, delegate_role: Option, ) -> ProgramResult { - let UpdateArgs::V1 { + // Destructure args. + let ( + new_update_authority, data, primary_sale_happened, is_mutable, collection, + collection_details, uses, - new_update_authority, rule_set, - collection_details, - .. - } = args; + ) = match args { + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } + | UpdateArgs::V2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } => ( + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + ), + }; // updates the token standard only if the current value is None let token_standard = match self.token_standard { @@ -116,7 +148,9 @@ impl Metadata { } }; - if matches!(authority_type, AuthorityType::Metadata) { + if matches!(authority_type, AuthorityType::Metadata) + || matches!(delegate_role, Some(MetadataDelegateRole::Data)) + { if let Some(data) = data { if !self.is_mutable { return Err(MetadataError::DataIsImmutable.into()); @@ -131,7 +165,14 @@ impl Metadata { )?; self.data = data; } + } + if matches!(authority_type, AuthorityType::Metadata) + || matches!( + delegate_role, + Some(MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem) + ) + { // if the Collection data is 'Set', only allow updating if it is unverified // or if it exactly matches the existing collection info; if the Collection data // is 'Clear', then only set to 'None' if it is unverified. @@ -153,7 +194,9 @@ impl Metadata { } CollectionToggle::None => { /* nothing to do */ } } + } + if matches!(authority_type, AuthorityType::Metadata) { if uses.is_some() { let uses_option = uses.to_option(); // If already None leave it as None. @@ -161,6 +204,19 @@ impl Metadata { self.uses = uses_option; } + if let CollectionDetailsToggle::Set(collection_details) = collection_details { + // only unsized collections can have the size set, and only once. + if self.collection_details.is_some() { + return Err(MetadataError::SizedCollection.into()); + } + + self.collection_details = Some(collection_details); + } + } + + if matches!(authority_type, AuthorityType::Metadata) + || matches!(delegate_role, Some(MetadataDelegateRole::Authority)) + { if let Some(authority) = new_update_authority { self.update_authority = authority; } @@ -182,21 +238,15 @@ impl Metadata { return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); } } - - if let CollectionDetailsToggle::Set(collection_details) = collection_details { - // only unsized collections can have the size set, and only once. - if self.collection_details.is_some() { - return Err(MetadataError::SizedCollection.into()); - } - - self.collection_details = Some(collection_details); - } } if matches!(authority_type, AuthorityType::Metadata) || matches!( delegate_role, - Some(MetadataDelegateRole::ProgrammableConfig) + Some( + MetadataDelegateRole::ProgrammableConfig + | MetadataDelegateRole::ProgrammableConfigItem + ) ) { // if the rule_set data is either 'Set' or 'Clear', only allow updating if the diff --git a/token-metadata/program/src/state/programmable.rs b/token-metadata/program/src/state/programmable.rs index 1a5f55a1b7..91023135b3 100644 --- a/token-metadata/program/src/state/programmable.rs +++ b/token-metadata/program/src/state/programmable.rs @@ -219,6 +219,8 @@ pub struct AuthorityRequest<'a, 'b> { pub metadata_delegate_record_info: Option<&'a AccountInfo<'a>>, /// Expected `MetadataDelegateRole` for the request. pub metadata_delegate_roles: Vec, + /// Expected collection-level `MetadataDelegateRole` for the request. + pub collection_metadata_delegate_roles: Vec, /// `TokenRecord` account. pub token_record_info: Option<&'a AccountInfo<'a>>, /// Expected `TokenDelegateRole` for the request. @@ -242,6 +244,7 @@ impl<'a, 'b> Default for AuthorityRequest<'a, 'b> { token_account: None, metadata_delegate_record_info: None, metadata_delegate_roles: Vec::with_capacity(0), + collection_metadata_delegate_roles: Vec::with_capacity(0), token_record_info: None, token_delegate_roles: Vec::with_capacity(0), } @@ -347,12 +350,14 @@ impl AuthorityType { }); } } + } - // looking up the delegate on the collection mint (this is for - // collection-level delegates) - if let Some(mint) = request.collection_mint { + // looking up the delegate on the collection mint (this is for + // collection-level delegates) + if let Some(collection_mint) = request.collection_mint { + for role in &request.collection_metadata_delegate_roles { let (pda_key, _) = find_metadata_delegate_record_account( - mint, + collection_mint, *role, request.update_authority, request.authority, diff --git a/token-metadata/program/src/utils/mod.rs b/token-metadata/program/src/utils/mod.rs index ffb1b6c403..ed05ae1eb7 100644 --- a/token-metadata/program/src/utils/mod.rs +++ b/token-metadata/program/src/utils/mod.rs @@ -69,6 +69,11 @@ pub fn check_token_standard( } } +pub fn mint_decimals_is_zero(mint_info: &AccountInfo) -> Result { + let mint_decimals = get_mint_decimals(mint_info)?; + Ok(mint_decimals == 0) +} + pub fn is_master_edition( edition_account_info: &AccountInfo, mint_decimals: u8, From 8d3e5f467fc8d78b52446d2ef2a43f0c1f545372 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:08:58 -0700 Subject: [PATCH 03/40] Update tests to support new version of UpdateArgs * Also add macro to help destructure UpdateArgs fields. --- .../program/src/instruction/metadata.rs | 10 ++ .../program/src/processor/metadata/update.rs | 90 ++++++---------- token-metadata/program/src/state/metadata.rs | 45 +++----- token-metadata/program/tests/unverify.rs | 39 +++---- token-metadata/program/tests/update.rs | 51 ++++----- .../program/tests/utils/digital_asset.rs | 102 ++++++++++++++---- token-metadata/program/tests/verify.rs | 12 +-- 7 files changed, 177 insertions(+), 172 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index e6f057d5ea..f44d4cb2c5 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -143,6 +143,16 @@ impl Default for UpdateArgs { } } +#[macro_export] +macro_rules! get_update_args_fields { + ($args:expr, $($field:ident),+) => { + match $args { + UpdateArgs::V1 { $($field,)+ .. } => ($($field,)+), + UpdateArgs::V2 { $($field,)+ .. } => ($($field,)+), + } + }; +} + //-- Toggle implementations #[repr(C)] diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 859f08a553..fe59412f04 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -10,6 +10,7 @@ use spl_token::state::Account; use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, + get_update_args_fields, instruction::{Context, MetadataDelegateRole, Update, UpdateArgs}, pda::{EDITION, PREFIX}, state::{ @@ -167,6 +168,21 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ..Default::default() })?; + // Check if caller passed in a desired token standard. + let desired_token_standard = match args { + UpdateArgs::V1 { .. } => None, + UpdateArgs::V2 { token_standard, .. } => token_standard, + }; + + // Validate that authority has permission to update the fields that have been specified in the + // update args. + validate_update( + &args, + &authority_type, + metadata_delegate_role, + desired_token_standard, + )?; + // Find existing token standard from metadata or infer it. let existing_or_inferred_token_standard = if let Some(token_standard) = metadata.token_standard { @@ -175,12 +191,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? }; - // Check if caller passed in a desired token standard. - let desired_token_standard = match args { - UpdateArgs::V1 { .. } => None, - UpdateArgs::V2 { token_standard, .. } => token_standard, - }; - // If there is a desired token standard, use it if it passes the check. If there is not a // desired token standard, use the existing or inferred token standard. let token_standard = match desired_token_standard { @@ -208,10 +218,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } } - // Validate that authority has permission to update the fields that have been specified in the - // update args. - validate_update(&args, &authority_type, metadata_delegate_role)?; - // If we reach here without errors we have validated that the authority is allowed to // perform an update. metadata.update_v1( @@ -233,6 +239,7 @@ fn validate_update( args: &UpdateArgs, authority_type: &AuthorityType, metadata_delegate_role: Option, + desired_token_standard: Option, ) -> ProgramResult { // validate the authority type match authority_type { @@ -264,52 +271,17 @@ fn validate_update( collection_details, uses, rule_set, - token_standard, - ) = match args { - UpdateArgs::V1 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } => ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - &None, - ), - UpdateArgs::V2 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - token_standard, - .. - } => ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - token_standard, - ), - }; + ) = get_update_args_fields!( + args, + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set + ); // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to @@ -340,7 +312,7 @@ fn validate_update( || collection_details.is_some() || uses.is_some() || rule_set.is_some() - || token_standard.is_some() + || desired_token_standard.is_some() { return Err(MetadataError::InvalidUpdateArgs.into()); } @@ -356,7 +328,7 @@ fn validate_update( || collection_details.is_some() || uses.is_some() || rule_set.is_some() - || token_standard.is_some() + || desired_token_standard.is_some() { return Err(MetadataError::InvalidUpdateArgs.into()); } @@ -372,7 +344,7 @@ fn validate_update( || collection.is_some() || collection_details.is_some() || uses.is_some() - || token_standard.is_some() + || desired_token_standard.is_some() { return Err(MetadataError::InvalidUpdateArgs.into()); } diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index a43e427bff..a357998bea 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -4,6 +4,7 @@ use crate::{ collection::assert_collection_update_is_valid, metadata::assert_data_valid, uses::assert_valid_use, }, + get_update_args_fields, instruction::{ CollectionDetailsToggle, CollectionToggle, MetadataDelegateRole, RuleSetToggle, UpdateArgs, }, @@ -101,39 +102,17 @@ impl Metadata { collection_details, uses, rule_set, - ) = match args { - UpdateArgs::V1 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } - | UpdateArgs::V2 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } => ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - ), - }; + ) = get_update_args_fields!( + args, + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set + ); // updates the token standard only if the current value is None let token_standard = match self.token_standard { diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index 8f191146b9..d2d5460f1b 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -4,6 +4,7 @@ pub mod utils; use mpl_token_metadata::{ error::MetadataError, + get_update_args_fields, instruction::{BurnArgs, DelegateArgs, MetadataDelegateRole, UpdateArgs, VerificationArgs}, pda::{find_metadata_delegate_record_account, find_token_record_account}, state::{Collection, CollectionDetails, Creator, TokenStandard}, @@ -1262,12 +1263,12 @@ mod unverify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_unverify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_unverify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify( AssetToDelegate::CollectionParent, @@ -1310,12 +1311,12 @@ mod unverify_collection { } #[tokio::test] - async fn items_update_delegate_cannot_unverify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn items_collection_item_delegate_cannot_unverify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify( AssetToDelegate::Item, @@ -1909,11 +1910,8 @@ mod unverify_collection { .unwrap(); let mut args = UpdateArgs::default(); - let UpdateArgs::V1 { - new_update_authority, - .. - } = &mut args; - *new_update_authority = Some(new_collection_update_authority.pubkey()); + let new_update_authority = get_update_args_fields!(&mut args, new_update_authority); + *new_update_authority.0 = Some(new_collection_update_authority.pubkey()); let payer = context.payer.dirty_clone(); test_items @@ -1983,11 +1981,8 @@ mod unverify_collection { let new_collection_update_authority = Keypair::new(); let mut args = UpdateArgs::default(); - let UpdateArgs::V1 { - new_update_authority, - .. - } = &mut args; - *new_update_authority = Some(new_collection_update_authority.pubkey()); + let new_update_authority = get_update_args_fields!(&mut args, new_update_authority); + *new_update_authority.0 = Some(new_collection_update_authority.pubkey()); let payer = context.payer.dirty_clone(); test_items @@ -2036,7 +2031,7 @@ mod unverify_collection { } #[tokio::test] - async fn pass_unverify_burned_pnft_parent_using_item_update_delegate() { + async fn pass_unverify_burned_pnft_parent_using_item_collection_item_delegate() { let mut context = program_test().start_with_context().await; let mut test_items = create_mint_verify_collection_check( @@ -2071,7 +2066,7 @@ mod unverify_collection { let payer = context.payer.dirty_clone(); let payer_pubkey = payer.pubkey(); - let delegate_args = DelegateArgs::UpdateV1 { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; test_items @@ -2083,7 +2078,7 @@ mod unverify_collection { // Find delegate record PDA. let (delegate_record, _) = find_metadata_delegate_record_account( &test_items.da.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::CollectionItem, &payer_pubkey, &delegate.pubkey(), ); @@ -2127,12 +2122,12 @@ mod unverify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_unverify_burned_pnft_parent() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify_burned_pnft_parent( AssetToDelegate::CollectionParent, diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index c94a6ecc85..b3bc167514 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -2,6 +2,7 @@ pub mod utils; use mpl_token_metadata::{ + get_update_args_fields, instruction::{builders::UpdateBuilder, InstructionBuilder}, state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, utils::puffed_out_string, @@ -67,10 +68,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); let mut builder = UpdateBuilder::new(); builder @@ -139,9 +138,10 @@ mod update { } let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + // remove the rule set - *rule_set = RuleSetToggle::Clear; + *rule_set.0 = RuleSetToggle::Clear; let mut builder = UpdateBuilder::new(); builder @@ -213,8 +213,8 @@ mod update { } let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - *rule_set = RuleSetToggle::Set(invalid_rule_set); + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + *rule_set.0 = RuleSetToggle::Set(invalid_rule_set); let mut builder = UpdateBuilder::new(); builder @@ -285,8 +285,8 @@ mod update { // Finally, try to update with the valid rule set, and it should succeed. let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - *rule_set = RuleSetToggle::Set(authorization_rules); + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + *rule_set.0 = RuleSetToggle::Set(authorization_rules); let mut builder = UpdateBuilder::new(); builder @@ -382,9 +382,10 @@ mod update { // Try to clear the rule set. let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + // remove the rule set - *rule_set = RuleSetToggle::Clear; + *rule_set.0 = RuleSetToggle::Clear; let mut builder = UpdateBuilder::new(); builder @@ -418,11 +419,9 @@ mod update { // Try to update the rule set. let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - rule_set, - authorization_data, - .. - } = &mut update_args; + let (rule_set, authorization_data) = + get_update_args_fields!(&mut update_args, rule_set, authorization_data); + // update the rule set *rule_set = RuleSetToggle::Set(new_auth_rules); *authorization_data = Some(new_auth_data); @@ -500,10 +499,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); let err = da .update(context, update_authority.dirty_clone(), update_args) @@ -558,10 +555,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); da.update(context, update_authority.dirty_clone(), update_args) .await @@ -582,10 +577,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); da.update(context, update_authority.dirty_clone(), update_args) .await diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index a3dbaff3fa..4fb2860c1a 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -561,6 +561,25 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { + DelegateArgs::AuthorityV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Authority, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + DelegateArgs::DataV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Data, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + DelegateArgs::CollectionV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), @@ -570,19 +589,10 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - DelegateArgs::SaleV1 { .. } - | DelegateArgs::TransferV1 { .. } - | DelegateArgs::UtilityV1 { .. } - | DelegateArgs::StakingV1 { .. } - | DelegateArgs::LockedTransferV1 { .. } => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - DelegateArgs::UpdateV1 { .. } => { + DelegateArgs::CollectionItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); @@ -597,6 +607,25 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } + DelegateArgs::ProgrammableConfigItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfigItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + + DelegateArgs::SaleV1 { .. } + | DelegateArgs::TransferV1 { .. } + | DelegateArgs::UtilityV1 { .. } + | DelegateArgs::StakingV1 { .. } + | DelegateArgs::LockedTransferV1 { .. } => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } } @@ -762,6 +791,24 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { + RevokeArgs::AuthorityV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Authority, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::DataV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Data, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } RevokeArgs::CollectionV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), @@ -771,20 +818,10 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::SaleV1 - | RevokeArgs::TransferV1 - | RevokeArgs::UtilityV1 - | RevokeArgs::StakingV1 - | RevokeArgs::LockedTransferV1 - | RevokeArgs::MigrationV1 => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - RevokeArgs::UpdateV1 => { + RevokeArgs::CollectionItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); @@ -799,6 +836,25 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } + RevokeArgs::ProgrammableConfigItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfigItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::SaleV1 + | RevokeArgs::TransferV1 + | RevokeArgs::UtilityV1 + | RevokeArgs::StakingV1 + | RevokeArgs::LockedTransferV1 + | RevokeArgs::MigrationV1 => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } } diff --git a/token-metadata/program/tests/verify.rs b/token-metadata/program/tests/verify.rs index ad3ec1f5db..2536f8144b 100644 --- a/token-metadata/program/tests/verify.rs +++ b/token-metadata/program/tests/verify.rs @@ -2118,12 +2118,12 @@ mod verify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_verify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_verify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_verify( AssetToDelegate::CollectionParent, @@ -2162,12 +2162,12 @@ mod verify_collection { } #[tokio::test] - async fn items_update_delegate_cannot_verify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn items_collection_item_delegate_cannot_verify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_verify(AssetToDelegate::Item, delegate_args, delegate_role) .await; From 7fcadd81911a21918c32ebb0777ecc25e5f15b3b Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:25:56 -0700 Subject: [PATCH 04/40] Fix delegate test based on auth rules update --- token-metadata/program/src/processor/delegate/delegate.rs | 1 - token-metadata/program/tests/delegate.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index 1e555c137a..5f7597267d 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -125,7 +125,6 @@ pub fn delegate<'a>( DelegateArgs::DataV1 { authorization_data } => { Some((MetadataDelegateRole::Data, authorization_data)) } - DelegateArgs::CollectionV1 { authorization_data } => { Some((MetadataDelegateRole::Collection, authorization_data)) } diff --git a/token-metadata/program/tests/delegate.rs b/token-metadata/program/tests/delegate.rs index b296e09134..3a5c30db71 100644 --- a/token-metadata/program/tests/delegate.rs +++ b/token-metadata/program/tests/delegate.rs @@ -587,6 +587,6 @@ mod delegate { // asserts - assert_custom_error_ix!(1, error, RuleSetError::ProgramOwnedListCheckFailed); + assert_custom_error_ix!(1, error, RuleSetError::DataIsEmpty); } } From ce19636e54e8da5792090f6c4cd1129e50841686 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:43:25 -0700 Subject: [PATCH 05/40] Put enums back in original order * Update is still changed to Data but order is preserved. * Also remove unnecessary Option for token_record in Update. * Add some comments clarifying authority types in Unverify. --- .../program/src/instruction/delegate.rs | 62 +++++++------- .../src/processor/delegate/delegate.rs | 41 ++++----- .../program/src/processor/delegate/revoke.rs | 6 +- .../program/src/processor/metadata/update.rs | 28 +++---- .../src/processor/verification/collection.rs | 9 +- token-metadata/program/src/state/metadata.rs | 16 +--- .../program/tests/utils/digital_asset.rs | 83 ++++++++++--------- 7 files changed, 122 insertions(+), 123 deletions(-) diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 667e088b6a..8f4f31b3a2 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -16,30 +16,10 @@ use crate::{instruction::MetadataInstruction, processor::AuthorizationData}; #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum DelegateArgs { - AuthorityV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, - DataV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, CollectionV1 { /// Required authorization data to validate the request. authorization_data: Option, }, - CollectionItemV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, - ProgrammableConfigV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, - ProgrammableConfigItemV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, SaleV1 { amount: u64, /// Required authorization data to validate the request. @@ -50,6 +30,10 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + DataV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, UtilityV1 { amount: u64, /// Required authorization data to validate the request. @@ -70,25 +54,41 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + ProgrammableConfigV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AuthorityV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + CollectionItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + ProgrammableConfigItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, } #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum RevokeArgs { - AuthorityV1, - DataV1, CollectionV1, - CollectionItemV1, - ProgrammableConfigV1, - ProgrammableConfigItemV1, SaleV1, TransferV1, + DataV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, + ProgrammableConfigV1, MigrationV1, + AuthorityV1, + CollectionItemV1, + ProgrammableConfigItemV1, } #[repr(C)] @@ -96,11 +96,11 @@ pub enum RevokeArgs { #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum MetadataDelegateRole { Authority, - Data, - Use, Collection, - CollectionItem, + Use, + Data, ProgrammableConfig, + CollectionItem, ProgrammableConfigItem, } @@ -108,11 +108,11 @@ impl fmt::Display for MetadataDelegateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self { Self::Authority => "authority_delegate".to_string(), - Self::Data => "data_delegate".to_string(), - Self::Use => "use_delegate".to_string(), Self::Collection => "collection_delegate".to_string(), - Self::CollectionItem => "collection_item_delegate".to_string(), + Self::Use => "use_delegate".to_string(), + Self::Data => "data_delegate".to_string(), Self::ProgrammableConfig => "programmable_config_delegate".to_string(), + Self::CollectionItem => "collection_item_delegate".to_string(), Self::ProgrammableConfigItem => "prog_config_item_delegate".to_string(), }; diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index 5f7597267d..c3be34ceae 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -36,11 +36,11 @@ impl Display for DelegateScenario { let message = match self { Self::Metadata(role) => match role { MetadataDelegateRole::Authority => "Authority".to_string(), - MetadataDelegateRole::Data => "Data".to_string(), - MetadataDelegateRole::Use => "Use".to_string(), MetadataDelegateRole::Collection => "Collection".to_string(), - MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), + MetadataDelegateRole::Use => "Use".to_string(), + MetadataDelegateRole::Data => "Data".to_string(), MetadataDelegateRole::ProgrammableConfig => "ProgrammableConfig".to_string(), + MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), MetadataDelegateRole::ProgrammableConfigItem => { "ProgrammableConfigItem".to_string() } @@ -79,16 +79,6 @@ pub fn delegate<'a>( amount, authorization_data, } => Some((TokenDelegateRole::Transfer, amount, authorization_data)), - // LockedTransfer - DelegateArgs::LockedTransferV1 { - amount, - authorization_data, - .. - } => Some(( - TokenDelegateRole::LockedTransfer, - amount, - authorization_data, - )), // Utility DelegateArgs::UtilityV1 { amount, @@ -101,6 +91,17 @@ pub fn delegate<'a>( } => Some((TokenDelegateRole::Staking, amount, authorization_data)), // Standard DelegateArgs::StandardV1 { amount } => Some((TokenDelegateRole::Standard, amount, &None)), + // LockedTransfer + DelegateArgs::LockedTransferV1 { + amount, + authorization_data, + .. + } => Some(( + TokenDelegateRole::LockedTransfer, + amount, + authorization_data, + )), + // we don't need to fail if did not find a match at this point _ => None, }; @@ -119,21 +120,21 @@ pub fn delegate<'a>( // checks if it is a MetadataDelegate creation let delegate_args = match &args { - DelegateArgs::AuthorityV1 { authorization_data } => { - Some((MetadataDelegateRole::Authority, authorization_data)) + DelegateArgs::CollectionV1 { authorization_data } => { + Some((MetadataDelegateRole::Collection, authorization_data)) } DelegateArgs::DataV1 { authorization_data } => { Some((MetadataDelegateRole::Data, authorization_data)) } - DelegateArgs::CollectionV1 { authorization_data } => { - Some((MetadataDelegateRole::Collection, authorization_data)) + DelegateArgs::ProgrammableConfigV1 { authorization_data } => { + Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) + } + DelegateArgs::AuthorityV1 { authorization_data } => { + Some((MetadataDelegateRole::Authority, authorization_data)) } DelegateArgs::CollectionItemV1 { authorization_data } => { Some((MetadataDelegateRole::CollectionItem, authorization_data)) } - DelegateArgs::ProgrammableConfigV1 { authorization_data } => { - Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) - } DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => Some(( MetadataDelegateRole::ProgrammableConfigItem, authorization_data, diff --git a/token-metadata/program/src/processor/delegate/revoke.rs b/token-metadata/program/src/processor/delegate/revoke.rs index 585a50ea29..ef17d273b4 100644 --- a/token-metadata/program/src/processor/delegate/revoke.rs +++ b/token-metadata/program/src/processor/delegate/revoke.rs @@ -54,11 +54,11 @@ pub fn revoke<'a>( // checks if it is a MetadataDelegate creation let metadata_delegate = match &args { - RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), - RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), - RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), + RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), + RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), + RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point _ => None, diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index fe59412f04..55d130081c 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -188,6 +188,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro { token_standard } else { + // TODO: What if they have an edition account but choose not to pass it in? check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? }; @@ -225,7 +226,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ctx.accounts.authority_info, ctx.accounts.metadata_info, token, - Some(token_standard), + token_standard, authority_type, metadata_delegate_role, )?; @@ -256,9 +257,7 @@ fn validate_update( // support for delegate update msg!("Auth type: Delegate"); } - _ => { - return Err(MetadataError::InvalidAuthorityType.into()); - } + _ => return Err(MetadataError::InvalidAuthorityType.into()), } // Destructure args. @@ -302,13 +301,13 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - MetadataDelegateRole::Data => { - // Fields allowed for `Data`: - // `data` + MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { + // Fields allowed for `Collection` and `CollectionItem`: + // `collection` if new_update_authority.is_some() + || data.is_some() || primary_sale_happened.is_some() || is_mutable.is_some() - || collection.is_some() || collection_details.is_some() || uses.is_some() || rule_set.is_some() @@ -317,14 +316,13 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - - MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { - // Fields allowed for `Collection` and `CollectionItem`: - // `collection` + MetadataDelegateRole::Data => { + // Fields allowed for `Data`: + // `data` if new_update_authority.is_some() - || data.is_some() || primary_sale_happened.is_some() || is_mutable.is_some() + || collection.is_some() || collection_details.is_some() || uses.is_some() || rule_set.is_some() @@ -361,8 +359,8 @@ fn check_desired_token_standard( existing_or_inferred_token_standard: TokenStandard, desired_token_standard: TokenStandard, ) -> ProgramResult { - // This code only allows switching between Fungible and FungibleAsset, and only when - // mint decimals is zero. + // This function only allows switching between Fungible and FungibleAsset. Mint decimals must + // be zero. if !mint_decimals_is_zero { return Err(MetadataError::InvalidTokenStandard.into()); } diff --git a/token-metadata/program/src/processor/verification/collection.rs b/token-metadata/program/src/processor/verification/collection.rs index aea56548cd..811aecb0d4 100644 --- a/token-metadata/program/src/processor/verification/collection.rs +++ b/token-metadata/program/src/processor/verification/collection.rs @@ -160,8 +160,9 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context let authority_response = if parent_burned { // If the collection parent is burned, we need to use an authority for the item rather than - // the collection. The required authority is either the item's metadata update authority, - // or an update delegate for the item. This call fails if no valid authority is present. + // the collection. The required authority is either the item's metadata update authority + // or a delegate for the item that can update the item's collection field. This call fails + // if no valid authority is present. auth_request.mint = &metadata.mint; auth_request.update_authority = &metadata.update_authority; auth_request.metadata_delegate_roles = vec![ @@ -185,6 +186,10 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context // If the collection parent is not burned, the required authority is either the collection // parent's metadata update authority, or a collection delegate for the collection parent. // This call fails if no valid authority is present. + // + // Note that this is sending the delegate in the `metadata_delegate_roles` vec and NOT the + // `collection_metadata_delegate_roles` vec because in this case we are authorizing using + // the collection parent's update authority. auth_request.mint = collection_mint_info.key; auth_request.update_authority = &collection_metadata.update_authority; auth_request.metadata_delegate_roles = vec![MetadataDelegateRole::Collection]; diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index a357998bea..2e6e12a82a 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -88,7 +88,7 @@ impl Metadata { update_authority: &AccountInfo<'a>, metadata: &AccountInfo<'a>, token: Option, - token_standard: Option, + token_standard: TokenStandard, authority_type: AuthorityType, delegate_role: Option, ) -> ProgramResult { @@ -114,18 +114,8 @@ impl Metadata { rule_set ); - // updates the token standard only if the current value is None - let token_standard = match self.token_standard { - Some(ts) => ts, - None => { - if let Some(ts) = token_standard { - self.token_standard = Some(ts); - ts - } else { - return Err(MetadataError::InvalidTokenStandard.into()); - } - } - }; + // Update the token standard. + self.token_standard = Some(token_standard); if matches!(authority_type, AuthorityType::Metadata) || matches!(delegate_role, Some(MetadataDelegateRole::Data)) diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index 4fb2860c1a..b6e7a0e772 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -561,10 +561,23 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { - DelegateArgs::AuthorityV1 { .. } => { + // Token delegates. + DelegateArgs::SaleV1 { .. } + | DelegateArgs::TransferV1 { .. } + | DelegateArgs::UtilityV1 { .. } + | DelegateArgs::StakingV1 { .. } + | DelegateArgs::LockedTransferV1 { .. } => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } + DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + DelegateArgs::CollectionV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Authority, + MetadataDelegateRole::Collection, &payer.pubkey(), &delegate, ); @@ -579,29 +592,28 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - - DelegateArgs::CollectionV1 { .. } => { + DelegateArgs::ProgrammableConfigV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - DelegateArgs::CollectionItemV1 { .. } => { + DelegateArgs::AuthorityV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::Authority, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - DelegateArgs::ProgrammableConfigV1 { .. } => { + DelegateArgs::CollectionItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); @@ -616,17 +628,6 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - - DelegateArgs::SaleV1 { .. } - | DelegateArgs::TransferV1 { .. } - | DelegateArgs::UtilityV1 { .. } - | DelegateArgs::StakingV1 { .. } - | DelegateArgs::LockedTransferV1 { .. } => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } } if let Some(edition) = self.edition { @@ -791,10 +792,24 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { - RevokeArgs::AuthorityV1 => { + // Token delegates. + RevokeArgs::SaleV1 + | RevokeArgs::TransferV1 + | RevokeArgs::UtilityV1 + | RevokeArgs::StakingV1 + | RevokeArgs::LockedTransferV1 + | RevokeArgs::MigrationV1 => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } + RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + RevokeArgs::CollectionV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Authority, + MetadataDelegateRole::Collection, &payer.pubkey(), &delegate, ); @@ -809,33 +824,34 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::CollectionV1 => { + RevokeArgs::ProgrammableConfigV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - RevokeArgs::CollectionItemV1 => { + RevokeArgs::AuthorityV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::Authority, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - RevokeArgs::ProgrammableConfigV1 => { + RevokeArgs::CollectionItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } + RevokeArgs::ProgrammableConfigItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), @@ -845,17 +861,6 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::SaleV1 - | RevokeArgs::TransferV1 - | RevokeArgs::UtilityV1 - | RevokeArgs::StakingV1 - | RevokeArgs::LockedTransferV1 - | RevokeArgs::MigrationV1 => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } } if let Some(edition) = self.edition { From 5789cfb7cc1dfa383ec9bd067770d219299f4d3f Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:51:40 -0700 Subject: [PATCH 06/40] Change unverify test on collection delegate behavior change --- token-metadata/program/tests/unverify.rs | 91 +++++++++++++++++++----- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index d2d5460f1b..6122a8de4a 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -2030,6 +2030,81 @@ mod unverify_collection { .await; } + #[tokio::test] + async fn pass_unverify_burned_pnft_parent_using_item_collection_delegate() { + let mut context = program_test().start_with_context().await; + + let mut test_items = create_mint_verify_collection_check( + &mut context, + DEFAULT_COLLECTION_DETAILS, + TokenStandard::ProgrammableNonFungible, + TokenStandard::ProgrammableNonFungible, + ) + .await; + + // Burn collection parent. + let args = BurnArgs::V1 { amount: 1 }; + let payer = context.payer.dirty_clone(); + test_items + .collection_parent_da + .burn(&mut context, payer, args, None, None) + .await + .unwrap(); + + // Assert that metadata, edition, token and token record accounts are closed. + test_items + .collection_parent_da + .assert_burned(&mut context) + .await + .unwrap(); + + // Create a metadata update delegate for the item. + let delegate = Keypair::new(); + airdrop(&mut context, &delegate.pubkey(), LAMPORTS_PER_SOL) + .await + .unwrap(); + + let payer = context.payer.dirty_clone(); + let payer_pubkey = payer.pubkey(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + test_items + .da + .delegate(&mut context, payer, delegate.pubkey(), delegate_args) + .await + .unwrap(); + + // Find delegate record PDA. + let (delegate_record, _) = find_metadata_delegate_record_account( + &test_items.da.mint.pubkey(), + MetadataDelegateRole::Collection, + &payer_pubkey, + &delegate.pubkey(), + ); + + // Unverify. + let args = VerificationArgs::CollectionV1; + test_items + .da + .unverify( + &mut context, + delegate, + args, + None, + Some(delegate_record), + Some(test_items.collection_parent_da.mint.pubkey()), + Some(test_items.collection_parent_da.metadata), + ) + .await + .unwrap(); + + test_items + .da + .assert_item_collection_matches_on_chain(&mut context, &test_items.collection) + .await; + } + #[tokio::test] async fn pass_unverify_burned_pnft_parent_using_item_collection_item_delegate() { let mut context = program_test().start_with_context().await; @@ -2153,22 +2228,6 @@ mod unverify_collection { .await; } - #[tokio::test] - async fn items_collection_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::CollectionV1 { - authorization_data: None, - }; - - let delegate_role = MetadataDelegateRole::Collection; - - other_metadata_delegates_cannot_unverify_burned_pnft_parent( - AssetToDelegate::Item, - delegate_args, - delegate_role, - ) - .await; - } - #[tokio::test] async fn items_prgm_config_delegate_cannot_unverify_burned_pnft_parent() { let delegate_args = DelegateArgs::ProgrammableConfigV1 { From 23cfe4338d91d3919bfbebd936e15bb7fd1702c2 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 02:14:54 -0700 Subject: [PATCH 07/40] Fix JS Update test --- token-metadata/js/test/update.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/token-metadata/js/test/update.test.ts b/token-metadata/js/test/update.test.ts index f627969d0a..abe03ca355 100644 --- a/token-metadata/js/test/update.test.ts +++ b/token-metadata/js/test/update.test.ts @@ -1201,7 +1201,8 @@ test('Update: Delegate Authority Type Not Supported', async (t) => { amman.addr.addLabel('Delegate Record', delegateRecord); const args: DelegateArgs = { - __kind: 'UpdateV1', + __kind: 'SaleV1', + amount: 1, authorizationData: null, }; From 71db91ac01e3912c5eb43d7cb65d1063bc0b4824 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:12:15 -0700 Subject: [PATCH 08/40] Modify DA test util object to return delegate or token record * Return the value derived in the delegate method. * Also add a test for Authority delegate. --- token-metadata/program/tests/update.rs | 78 ++++++++++++++++++- .../program/tests/utils/digital_asset.rs | 14 +++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index b3bc167514..78343bd6a0 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -29,7 +29,7 @@ mod update { use super::*; #[tokio::test] - async fn success_update() { + async fn success_update_by_update_authority() { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -101,6 +101,82 @@ mod update { assert_eq!(metadata.data.uri, new_uri); } + #[tokio::test] + async fn success_update_by_authority_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.update_authority, update_authority.pubkey()); + assert!(!metadata.primary_sale_happened); + assert!(metadata.is_mutable); + + // Create `Authority` metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate( + context, + update_authority, + delegate.pubkey(), + DelegateArgs::AuthorityV1 { + authorization_data: None, + }, + ) + .await + .unwrap() + .unwrap(); + + // Change a few values that this delegate is allowed to change. + let mut update_args = UpdateArgs::default(); + let (new_update_authority, primary_sale_happened, is_mutable) = get_update_args_fields!( + &mut update_args, + new_update_authority, + primary_sale_happened, + is_mutable + ); + *new_update_authority = Some(delegate.pubkey()); + *primary_sale_happened = Some(true); + *is_mutable = Some(false); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + //let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.update_authority, delegate.pubkey()); + assert!(metadata.primary_sale_happened); + assert!(!metadata.is_mutable); + } + #[tokio::test] async fn update_pfnt_config() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index b6e7a0e772..fae02f5136 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -550,7 +550,7 @@ impl DigitalAsset { payer: Keypair, delegate: Pubkey, args: DelegateArgs, - ) -> Result<(), BanksClientError> { + ) -> Result, BanksClientError> { let mut builder = DelegateBuilder::new(); builder .delegate(delegate) @@ -560,6 +560,8 @@ impl DigitalAsset { .authority(payer.pubkey()) .spl_token_program(spl_token::ID); + let mut delegate_or_token_record = None; + match args { // Token delegates. DelegateArgs::SaleV1 { .. } @@ -570,6 +572,7 @@ impl DigitalAsset { let (token_record, _) = find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); builder.token_record(token_record); + delegate_or_token_record = Some(token_record); } DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } @@ -582,6 +585,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::DataV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -591,6 +595,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::ProgrammableConfigV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -600,6 +605,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::AuthorityV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -609,6 +615,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::CollectionItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -618,6 +625,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::ProgrammableConfigItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -627,6 +635,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } } @@ -661,7 +670,8 @@ impl DigitalAsset { context.last_blockhash, ); - context.banks_client.process_transaction(tx).await + context.banks_client.process_transaction(tx).await?; + Ok(delegate_or_token_record) } pub async fn migrate( From 8cf0e57ccf18e661f3ef0003b9d2d7dbf12da0b3 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:35:00 -0700 Subject: [PATCH 09/40] Add collection delegate test --- token-metadata/program/tests/update.rs | 73 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 78343bd6a0..60c7ad0db0 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -20,8 +20,8 @@ mod update { use mpl_token_metadata::{ error::MetadataError, - instruction::{DelegateArgs, RuleSetToggle, UpdateArgs}, - state::{Creator, Data, ProgrammableConfig, TokenStandard}, + instruction::{CollectionToggle, DelegateArgs, RuleSetToggle, UpdateArgs}, + state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard}, }; use solana_program::pubkey::Pubkey; use solana_sdk::signature::Keypair; @@ -177,6 +177,75 @@ mod update { assert!(!metadata.is_mutable); } + #[tokio::test] + async fn success_update_by_collection_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Create `Collection` metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate( + context, + update_authority, + delegate.pubkey(), + DelegateArgs::CollectionV1 { + authorization_data: None, + }, + ) + .await + .unwrap() + .unwrap(); + + // Change a value that this delegate is allowed to change. + let mut update_args = UpdateArgs::default(); + let collection_toggle = get_update_args_fields!(&mut update_args, collection); + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + *collection_toggle.0 = CollectionToggle::Set(new_collection.clone()); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + //let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.collection, Some(new_collection)); + } + #[tokio::test] async fn update_pfnt_config() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); From 2920b21dec6247805b70bd10b2845c33dce4b4f6 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:35:39 -0700 Subject: [PATCH 10/40] Comment out JS test where the delegate is no longer available --- token-metadata/js/test/update.test.ts | 152 +++++++++++++------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/token-metadata/js/test/update.test.ts b/token-metadata/js/test/update.test.ts index abe03ca355..e09b0eee11 100644 --- a/token-metadata/js/test/update.test.ts +++ b/token-metadata/js/test/update.test.ts @@ -1177,82 +1177,82 @@ test('Update: Invalid Update Authority Fails', async (t) => { await updateTx.assertError(t, /Invalid authority type/); }); -test('Update: Delegate Authority Type Not Supported', async (t) => { - const API = new InitTransactions(); - const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); - - const daManager = await createDefaultAsset(t, connection, API, handler, payer); - - // creates a delegate - - const [, delegate] = await API.getKeypair('Delegate'); - // delegate PDA - const [delegateRecord] = PublicKey.findProgramAddressSync( - [ - Buffer.from('metadata'), - PROGRAM_ID.toBuffer(), - daManager.mint.toBuffer(), - Buffer.from('update_delegate'), - payer.publicKey.toBuffer(), - delegate.publicKey.toBuffer(), - ], - PROGRAM_ID, - ); - amman.addr.addLabel('Delegate Record', delegateRecord); - - const args: DelegateArgs = { - __kind: 'SaleV1', - amount: 1, - authorizationData: null, - }; - - const { tx: delegateTx } = await API.delegate( - delegate.publicKey, - daManager.mint, - daManager.metadata, - payer.publicKey, - payer, - args, - handler, - delegateRecord, - daManager.masterEdition, - ); - await delegateTx.assertSuccess(t); - - const assetData = await daManager.getAssetData(connection); - const authority = delegate; - - // Change some values and run update. - const data: Data = { - name: 'DigitalAsset2', - symbol: 'DA2', - uri: 'uri2', - sellerFeeBasisPoints: 10, - creators: assetData.creators, - }; - const authorizationData = daManager.emptyAuthorizationData(); - - const updateData = new UpdateTestData(); - updateData.data = data; - updateData.authorizationData = authorizationData; - - const { tx: updateTx } = await API.update( - t, - handler, - daManager.mint, - daManager.metadata, - authority, - updateData, - delegateRecord, - daManager.masterEdition, - ); - updateTx.then((x) => - x.assertLogs(t, [/Invalid authority type/i], { - txLabel: 'tx: Update', - }), - ); - await updateTx.assertError(t); -}); +// test('Update: Delegate Authority Type Not Supported', async (t) => { +// const API = new InitTransactions(); +// const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); + +// const daManager = await createDefaultAsset(t, connection, API, handler, payer); + +// // creates a delegate + +// const [, delegate] = await API.getKeypair('Delegate'); +// // delegate PDA +// const [delegateRecord] = PublicKey.findProgramAddressSync( +// [ +// Buffer.from('metadata'), +// PROGRAM_ID.toBuffer(), +// daManager.mint.toBuffer(), +// Buffer.from('update_delegate'), +// payer.publicKey.toBuffer(), +// delegate.publicKey.toBuffer(), +// ], +// PROGRAM_ID, +// ); +// amman.addr.addLabel('Delegate Record', delegateRecord); + +// const args: DelegateArgs = { +// __kind: 'UpdateV1', +// amount: 1, +// authorizationData: null, +// }; + +// const { tx: delegateTx } = await API.delegate( +// delegate.publicKey, +// daManager.mint, +// daManager.metadata, +// payer.publicKey, +// payer, +// args, +// handler, +// delegateRecord, +// daManager.masterEdition, +// ); +// await delegateTx.assertSuccess(t); + +// const assetData = await daManager.getAssetData(connection); +// const authority = delegate; + +// // Change some values and run update. +// const data: Data = { +// name: 'DigitalAsset2', +// symbol: 'DA2', +// uri: 'uri2', +// sellerFeeBasisPoints: 10, +// creators: assetData.creators, +// }; +// const authorizationData = daManager.emptyAuthorizationData(); + +// const updateData = new UpdateTestData(); +// updateData.data = data; +// updateData.authorizationData = authorizationData; + +// const { tx: updateTx } = await API.update( +// t, +// handler, +// daManager.mint, +// daManager.metadata, +// authority, +// updateData, +// delegateRecord, +// daManager.masterEdition, +// ); +// updateTx.then((x) => +// x.assertLogs(t, [/Invalid authority type/i], { +// txLabel: 'tx: Update', +// }), +// ); +// await updateTx.assertError(t); +// }); test('Update: Holder Authority Type Not Supported', async (t) => { const API = new InitTransactions(); From dc4852c76ef1d533043e0d46370d57d30b9f4dce Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:43:17 -0700 Subject: [PATCH 11/40] Add test for collection item delegate --- token-metadata/program/tests/update.rs | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 60c7ad0db0..1a6a0f977c 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -178,7 +178,24 @@ mod update { } #[tokio::test] - async fn success_update_by_collection_delegate() { + async fn success_update_by_items_collection_delegate() { + let args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + success_update_collection_by_items_delegate(args).await; + } + + #[tokio::test] + async fn success_update_by_items_collection_item_delegate() { + let args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + success_update_collection_by_items_delegate(args).await; + } + + async fn success_update_collection_by_items_delegate(delegate_args: DelegateArgs) { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -195,14 +212,7 @@ mod update { let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); let delegate_record = da - .delegate( - context, - update_authority, - delegate.pubkey(), - DelegateArgs::CollectionV1 { - authorization_data: None, - }, - ) + .delegate(context, update_authority, delegate.pubkey(), delegate_args) .await .unwrap() .unwrap(); From 5fd48e7172d5960f04e7374cea6b2b0e891329c8 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:46:05 -0700 Subject: [PATCH 12/40] Regenerate JS API --- token-metadata/js/idl/mpl_token_metadata.json | 131 +++++++++++++++++- .../js/src/generated/types/DelegateArgs.ts | 50 ++++++- .../generated/types/MetadataDelegateRole.ts | 4 +- .../js/src/generated/types/RevokeArgs.ts | 5 +- .../js/src/generated/types/UpdateArgs.ts | 34 +++++ 5 files changed, 212 insertions(+), 12 deletions(-) diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index 2526a52b2f..5ba20bfd41 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -4671,7 +4671,7 @@ ] }, { - "name": "UpdateV1", + "name": "DataV1", "fields": [ { "name": "authorization_data", @@ -4759,6 +4759,45 @@ } } ] + }, + { + "name": "AuthorityV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "CollectionItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "ProgrammableConfigItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } @@ -4778,7 +4817,7 @@ "name": "TransferV1" }, { - "name": "UpdateV1" + "name": "DataV1" }, { "name": "UtilityV1" @@ -4797,6 +4836,15 @@ }, { "name": "MigrationV1" + }, + { + "name": "AuthorityV1" + }, + { + "name": "CollectionItemV1" + }, + { + "name": "ProgrammableConfigItemV1" } ] } @@ -4816,10 +4864,16 @@ "name": "Use" }, { - "name": "Update" + "name": "Data" }, { "name": "ProgrammableConfig" + }, + { + "name": "CollectionItem" + }, + { + "name": "ProgrammableConfigItem" } ] } @@ -4974,6 +5028,77 @@ } } ] + }, + { + "name": "V2", + "fields": [ + { + "name": "new_update_authority", + "type": { + "option": "publicKey" + } + }, + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "primary_sale_happened", + "type": { + "option": "bool" + } + }, + { + "name": "is_mutable", + "type": { + "option": "bool" + } + }, + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "collection_details", + "type": { + "defined": "CollectionDetailsToggle" + } + }, + { + "name": "uses", + "type": { + "defined": "UsesToggle" + } + }, + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "token_standard", + "type": { + "option": { + "defined": "TokenStandard" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } diff --git a/token-metadata/js/src/generated/types/DelegateArgs.ts b/token-metadata/js/src/generated/types/DelegateArgs.ts index 395df7c152..df9a9538a6 100644 --- a/token-metadata/js/src/generated/types/DelegateArgs.ts +++ b/token-metadata/js/src/generated/types/DelegateArgs.ts @@ -22,7 +22,7 @@ export type DelegateArgsRecord = { CollectionV1: { authorizationData: beet.COption }; SaleV1: { amount: beet.bignum; authorizationData: beet.COption }; TransferV1: { amount: beet.bignum; authorizationData: beet.COption }; - UpdateV1: { authorizationData: beet.COption }; + DataV1: { authorizationData: beet.COption }; UtilityV1: { amount: beet.bignum; authorizationData: beet.COption }; StakingV1: { amount: beet.bignum; authorizationData: beet.COption }; StandardV1: { amount: beet.bignum }; @@ -32,6 +32,9 @@ export type DelegateArgsRecord = { authorizationData: beet.COption; }; ProgrammableConfigV1: { authorizationData: beet.COption }; + AuthorityV1: { authorizationData: beet.COption }; + CollectionItemV1: { authorizationData: beet.COption }; + ProgrammableConfigItemV1: { authorizationData: beet.COption }; }; /** @@ -55,9 +58,8 @@ export const isDelegateArgsSaleV1 = (x: DelegateArgs): x is DelegateArgs & { __k export const isDelegateArgsTransferV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'TransferV1' } => x.__kind === 'TransferV1'; -export const isDelegateArgsUpdateV1 = ( - x: DelegateArgs, -): x is DelegateArgs & { __kind: 'UpdateV1' } => x.__kind === 'UpdateV1'; +export const isDelegateArgsDataV1 = (x: DelegateArgs): x is DelegateArgs & { __kind: 'DataV1' } => + x.__kind === 'DataV1'; export const isDelegateArgsUtilityV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'UtilityV1' } => x.__kind === 'UtilityV1'; @@ -73,6 +75,16 @@ export const isDelegateArgsLockedTransferV1 = ( export const isDelegateArgsProgrammableConfigV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'ProgrammableConfigV1' } => x.__kind === 'ProgrammableConfigV1'; +export const isDelegateArgsAuthorityV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'AuthorityV1' } => x.__kind === 'AuthorityV1'; +export const isDelegateArgsCollectionItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'CollectionItemV1' } => x.__kind === 'CollectionItemV1'; +export const isDelegateArgsProgrammableConfigItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'ProgrammableConfigItemV1' } => + x.__kind === 'ProgrammableConfigItemV1'; /** * @category userTypes @@ -110,10 +122,10 @@ export const delegateArgsBeet = beet.dataEnum([ ], [ - 'UpdateV1', - new beet.FixableBeetArgsStruct( + 'DataV1', + new beet.FixableBeetArgsStruct( [['authorizationData', beet.coption(authorizationDataBeet)]], - 'DelegateArgsRecord["UpdateV1"]', + 'DelegateArgsRecord["DataV1"]', ), ], @@ -166,4 +178,28 @@ export const delegateArgsBeet = beet.dataEnum([ 'DelegateArgsRecord["ProgrammableConfigV1"]', ), ], + + [ + 'AuthorityV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["AuthorityV1"]', + ), + ], + + [ + 'CollectionItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["CollectionItemV1"]', + ), + ], + + [ + 'ProgrammableConfigItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["ProgrammableConfigItemV1"]', + ), + ], ]) as beet.FixableBeet; diff --git a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts index e103d46880..5ca7165221 100644 --- a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts +++ b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts @@ -14,8 +14,10 @@ export enum MetadataDelegateRole { Authority, Collection, Use, - Update, + Data, ProgrammableConfig, + CollectionItem, + ProgrammableConfigItem, } /** diff --git a/token-metadata/js/src/generated/types/RevokeArgs.ts b/token-metadata/js/src/generated/types/RevokeArgs.ts index 9ba7c3b1aa..fed9bd445d 100644 --- a/token-metadata/js/src/generated/types/RevokeArgs.ts +++ b/token-metadata/js/src/generated/types/RevokeArgs.ts @@ -14,13 +14,16 @@ export enum RevokeArgs { CollectionV1, SaleV1, TransferV1, - UpdateV1, + DataV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, ProgrammableConfigV1, MigrationV1, + AuthorityV1, + CollectionItemV1, + ProgrammableConfigItemV1, } /** diff --git a/token-metadata/js/src/generated/types/UpdateArgs.ts b/token-metadata/js/src/generated/types/UpdateArgs.ts index 7b731d9f0c..694c5a0328 100644 --- a/token-metadata/js/src/generated/types/UpdateArgs.ts +++ b/token-metadata/js/src/generated/types/UpdateArgs.ts @@ -14,6 +14,7 @@ import { CollectionDetailsToggle, collectionDetailsToggleBeet } from './Collecti import { UsesToggle, usesToggleBeet } from './UsesToggle'; import { RuleSetToggle, ruleSetToggleBeet } from './RuleSetToggle'; import { AuthorizationData, authorizationDataBeet } from './AuthorizationData'; +import { TokenStandard, tokenStandardBeet } from './TokenStandard'; /** * This type is used to derive the {@link UpdateArgs} type as well as the de/serializer. * However don't refer to it in your code but use the {@link UpdateArgs} type instead. @@ -35,6 +36,18 @@ export type UpdateArgsRecord = { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; + V2: { + newUpdateAuthority: beet.COption; + data: beet.COption; + primarySaleHappened: beet.COption; + isMutable: beet.COption; + collection: CollectionToggle; + collectionDetails: CollectionDetailsToggle; + uses: UsesToggle; + ruleSet: RuleSetToggle; + tokenStandard: beet.COption; + authorizationData: beet.COption; + }; }; /** @@ -52,6 +65,8 @@ export type UpdateArgs = beet.DataEnumKeyAsKind; export const isUpdateArgsV1 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V1' } => x.__kind === 'V1'; +export const isUpdateArgsV2 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V2' } => + x.__kind === 'V2'; /** * @category userTypes @@ -75,4 +90,23 @@ export const updateArgsBeet = beet.dataEnum([ 'UpdateArgsRecord["V1"]', ), ], + + [ + 'V2', + new beet.FixableBeetArgsStruct( + [ + ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], + ['data', beet.coption(dataBeet)], + ['primarySaleHappened', beet.coption(beet.bool)], + ['isMutable', beet.coption(beet.bool)], + ['collection', collectionToggleBeet], + ['collectionDetails', collectionDetailsToggleBeet], + ['uses', usesToggleBeet], + ['ruleSet', ruleSetToggleBeet], + ['tokenStandard', beet.coption(tokenStandardBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["V2"]', + ), + ], ]) as beet.FixableBeet; From ff4d372fe7ae9e400b8e209c64d14f2f99a0c3a0 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 15:42:52 -0700 Subject: [PATCH 13/40] Add more delegate success test cases --- token-metadata/program/tests/update.rs | 197 ++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 5 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 1a6a0f977c..ea472895c8 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -117,7 +117,7 @@ mod update { assert!(!metadata.primary_sale_happened); assert!(metadata.is_mutable); - // Create `Authority` metadata delegate. + // Create metadata delegate. let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); let delegate_record = da @@ -159,7 +159,6 @@ mod update { let update_ix = builder.build(update_args).unwrap().instruction(); - //let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); let tx = Transaction::new_signed_with_payer( &[update_ix], Some(&delegate.pubkey()), @@ -208,7 +207,7 @@ mod update { let metadata = da.get_metadata(context).await; assert_eq!(metadata.collection, None); - // Create `Collection` metadata delegate. + // Create metadata delegate. let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); let delegate_record = da @@ -240,7 +239,6 @@ mod update { let update_ix = builder.build(update_args).unwrap().instruction(); - //let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); let tx = Transaction::new_signed_with_payer( &[update_ix], Some(&delegate.pubkey()), @@ -257,7 +255,96 @@ mod update { } #[tokio::test] - async fn update_pfnt_config() { + async fn success_update_by_items_data_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!( + metadata.data.name, + puffed_out_string(DEFAULT_NAME, MAX_NAME_LENGTH) + ); + assert_eq!( + metadata.data.symbol, + puffed_out_string(DEFAULT_SYMBOL, MAX_SYMBOL_LENGTH) + ); + assert_eq!( + metadata.data.uri, + puffed_out_string(DEFAULT_URI, MAX_URI_LENGTH) + ); + assert_eq!(metadata.update_authority, update_authority.pubkey()); + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate( + context, + update_authority, + delegate.pubkey(), + DelegateArgs::DataV1 { + authorization_data: None, + }, + ) + .await + .unwrap() + .unwrap(); + + // Change some values that this delegate is allowed to change. + let new_name = puffed_out_string("New Name", MAX_NAME_LENGTH); + let new_symbol = puffed_out_string("NEW", MAX_SYMBOL_LENGTH); + let new_uri = puffed_out_string("https://new.digital.asset.org", MAX_URI_LENGTH); + let data = Data { + name: new_name.clone(), + symbol: new_symbol.clone(), + uri: new_uri.clone(), + creators: metadata.data.creators, // keep the same creators + seller_fee_basis_points: 0, + }; + + let mut update_args = UpdateArgs::default(); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.data.name, new_name); + assert_eq!(metadata.data.symbol, new_symbol); + assert_eq!(metadata.data.uri, new_uri); + } + + #[tokio::test] + async fn success_update_pfnt_config_by_update_authority() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); let context = &mut program_test.start_with_context().await; @@ -328,6 +415,106 @@ mod update { assert_eq!(metadata.programmable_config, None); } + #[tokio::test] + async fn success_update_pnft_by_items_programmable_config_delegate() { + let args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + + success_update_pnft_by_items_delegate(args).await; + } + + #[tokio::test] + async fn success_update_pnft_by_items_programmable_config_item_delegate() { + let args = DelegateArgs::ProgrammableConfigItemV1 { + authorization_data: None, + }; + + success_update_pnft_by_items_delegate(args).await; + } + + async fn success_update_pnft_by_items_delegate(delegate_args: DelegateArgs) { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change a value that this delegate is allowed to change. + let mut update_args = UpdateArgs::default(); + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + + // remove the rule set + *rule_set.0 = RuleSetToggle::Clear; + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.programmable_config, None); + } + #[tokio::test] async fn update_invalid_rule_set() { // Currently users can add an invalid rule set to their pNFT which will effectively From cd49af18e927b4b161773b601268fe510a174de8 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:11:40 -0700 Subject: [PATCH 14/40] Add tests for delegate not authorized --- token-metadata/program/tests/update.rs | 142 ++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index ea472895c8..5a3c63559a 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -20,8 +20,8 @@ mod update { use mpl_token_metadata::{ error::MetadataError, - instruction::{CollectionToggle, DelegateArgs, RuleSetToggle, UpdateArgs}, - state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard}, + instruction::{CollectionToggle, DelegateArgs, RuleSetToggle, UpdateArgs, UsesToggle}, + state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard, UseMethod, Uses}, }; use solana_program::pubkey::Pubkey; use solana_sdk::signature::Keypair; @@ -515,6 +515,144 @@ mod update { assert_eq!(metadata.programmable_config, None); } + #[tokio::test] + async fn fail_update_by_items_authority_delegate() { + let args = DelegateArgs::AuthorityV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_collection_delegate() { + let args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_data_delegate() { + let args = DelegateArgs::DataV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_programmable_config_delegate() { + let args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_collection_item_delegate() { + let args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_programmable_config_item_delegate() { + let args = DelegateArgs::ProgrammableConfigItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + async fn fail_update_by_items_delegate(delegate_args: DelegateArgs) { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.uses, None); + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change a value that no delegates are allowed to change. + let mut update_args = UpdateArgs::default(); + let uses = get_update_args_fields!(&mut update_args, uses); + + // remove the rule set + *uses.0 = UsesToggle::Set(Uses { + use_method: UseMethod::Multiple, + remaining: 333, + total: 333, + }); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidUpdateArgs); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.uses, None); + } + #[tokio::test] async fn update_invalid_rule_set() { // Currently users can add an invalid rule set to their pNFT which will effectively From 7a6e884a8573e75d37bf0143b5afa2afc7062d2d Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 7 Apr 2023 15:45:12 -0700 Subject: [PATCH 15/40] Add Update test for persistent delegate and token standard pass/fail --- token-metadata/program/tests/update.rs | 149 +++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 5a3c63559a..39a799fe38 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -653,6 +653,155 @@ mod update { assert_eq!(metadata.uses, None); } + #[tokio::test] + async fn fail_update_by_items_persistent_delegate() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.uses, None); + + // Create `TokenDelegate` type of delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::UtilityV1 { + amount: 1, + authorization_data: None, + }; + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change a value that no delegates are allowed to change. + let mut update_args = UpdateArgs::default(); + let uses = get_update_args_fields!(&mut update_args, uses); + + // remove the rule set + *uses.0 = UsesToggle::Set(Uses { + use_method: UseMethod::Multiple, + remaining: 333, + total: 333, + }); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.uses, None); + } + + #[tokio::test] + async fn success_update_token_standard() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::FungibleAsset, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); + + // Update token standard + let mut update_args = UpdateArgs::default(); + let token_standard = match &mut update_args { + UpdateArgs::V2 { token_standard, .. } => token_standard, + _ => panic!("Incompatible update args version"), + }; + + *token_standard = Some(TokenStandard::Fungible); + + da.update(context, update_authority.dirty_clone(), update_args) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::Fungible)); + } + + #[tokio::test] + async fn fail_invalid_update_token_standard() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::FungibleAsset, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); + + // Update token standard + let mut update_args = UpdateArgs::default(); + let token_standard = match &mut update_args { + UpdateArgs::V2 { token_standard, .. } => token_standard, + _ => panic!("Incompatible update args version"), + }; + + *token_standard = Some(TokenStandard::NonFungible); + + let err = da + .update(context, update_authority.dirty_clone(), update_args) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidTokenStandard); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); + } + #[tokio::test] async fn update_invalid_rule_set() { // Currently users can add an invalid rule set to their pNFT which will effectively From ee4219d3ece06b52e8903737ca6d795a65cc04a1 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 11 Apr 2023 09:38:57 -0700 Subject: [PATCH 16/40] Allow collection delegates to use update arg val for permission * If the collection toggle value passed in by a collection-level delegate contains a new collection value, that is used when determining authority. * This allows for setting a collection using a collection delegate when previously there was no collection in the metadata. * Also add tests for delegates on collection parent: collection delegate can update members of collection but collection item delegate cannot. * Also add test to fail updating to a verified collection. --- .../program/src/processor/metadata/update.rs | 19 +- token-metadata/program/src/state/metadata.rs | 2 +- token-metadata/program/tests/update.rs | 204 +++++++++++++++++- 3 files changed, 218 insertions(+), 7 deletions(-) diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 55d130081c..3c962c6245 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -11,7 +11,7 @@ use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, get_update_args_fields, - instruction::{Context, MetadataDelegateRole, Update, UpdateArgs}, + instruction::{CollectionToggle, Context, MetadataDelegateRole, Update, UpdateArgs}, pda::{EDITION, PREFIX}, state::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, @@ -127,10 +127,19 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // there is a special case for collection-level delegates, where the // validation should use the collection key as the mint parameter - let collection_mint = if let Some(Collection { key, .. }) = &metadata.collection { - Some(key) - } else { - None + let existing_collection_mint = metadata + .collection + .as_ref() + .map(|Collection { key, .. }| key); + + // Check if caller passed in a collection and if so use that. Note that if the + // delegate role from `get_authority_type` comes back as something other than + // `MetadataDelegateRole::Collection` or `MetadataDelegateRole::CollectionItem`, + // then it will fail in `validate_update` because those are the only roles that + // can change collection. + let collection_mint = match get_update_args_fields!(&args, collection).0 { + CollectionToggle::Set(Collection { key, .. }) => Some(key), + _ => existing_collection_mint, }; // Determines if we have a valid authority to perform the update. This must diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index 2e6e12a82a..7be38d0888 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -114,7 +114,7 @@ impl Metadata { rule_set ); - // Update the token standard. + // Update the token standard if it is changed. self.token_standard = Some(token_standard); if matches!(authority_type, AuthorityType::Metadata) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 39a799fe38..4b4059994a 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -178,7 +178,7 @@ mod update { #[tokio::test] async fn success_update_by_items_collection_delegate() { - let args = DelegateArgs::CollectionItemV1 { + let args = DelegateArgs::CollectionV1 { authorization_data: None, }; @@ -802,6 +802,208 @@ mod update { assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); } + #[tokio::test] + async fn fail_update_to_verified_collection() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::FungibleAsset, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Set collection to a value with verified set to true. + let mut update_args = UpdateArgs::default(); + let collection_toggle = get_update_args_fields!(&mut update_args, collection); + let new_collection = Collection { + verified: true, + key: Keypair::new().pubkey(), + }; + *collection_toggle.0 = CollectionToggle::Set(new_collection.clone()); + + let err = da + .update(context, update_authority.dirty_clone(), update_args) + .await + .unwrap_err(); + + assert_custom_error!( + err, + MetadataError::CollectionCannotBeVerifiedInThisInstruction + ); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + + #[tokio::test] + async fn success_update_collection_by_collections_collection_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let mut update_args = UpdateArgs::default(); + let collection_toggle = get_update_args_fields!(&mut update_args, collection); + let collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + *collection_toggle.0 = CollectionToggle::Set(collection.clone()); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // Check that collection changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, Some(collection)); + } + + #[tokio::test] + async fn fail_update_collection_by_collections_collection_item_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let mut update_args = UpdateArgs::default(); + let collection_toggle = get_update_args_fields!(&mut update_args, collection); + let collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + *collection_toggle.0 = CollectionToggle::Set(collection.clone()); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + #[tokio::test] async fn update_invalid_rule_set() { // Currently users can add an invalid rule set to their pNFT which will effectively From 2bda353e4fab8531581ad48a7a21d9ac1b55b932 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 11 Apr 2023 09:54:27 -0700 Subject: [PATCH 17/40] Test that collection programmable config delegate cannot update collection This is important because the passed in collection is also used for permissions. If we allowed the collection-level programmable config delegate to pass in a new collection, it would also use that value in `get_authority_type`. --- token-metadata/program/tests/update.rs | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 4b4059994a..25932110c6 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -1004,6 +1004,91 @@ mod update { assert_eq!(metadata.collection, None); } + #[tokio::test] + async fn fail_update_collection_by_collections_programmable_config_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let mut update_args = UpdateArgs::default(); + let collection_toggle = get_update_args_fields!(&mut update_args, collection); + let collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + *collection_toggle.0 = CollectionToggle::Set(collection.clone()); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidUpdateArgs); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + #[tokio::test] async fn update_invalid_rule_set() { // Currently users can add an invalid rule set to their pNFT which will effectively From 7e2e3607047333c884acd50877f39b1f179a28f0 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:00:25 -0700 Subject: [PATCH 18/40] Move dependencies up --- token-metadata/program/tests/update.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 25932110c6..d7bbc840ff 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -2,30 +2,28 @@ pub mod utils; use mpl_token_metadata::{ + error::MetadataError, get_update_args_fields, - instruction::{builders::UpdateBuilder, InstructionBuilder}, + instruction::{ + builders::UpdateBuilder, CollectionToggle, DelegateArgs, InstructionBuilder, RuleSetToggle, + UpdateArgs, UsesToggle, + }, + state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard, UseMethod, Uses}, state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, utils::puffed_out_string, }; use num_traits::FromPrimitive; +use solana_program::pubkey::Pubkey; use solana_program_test::*; use solana_sdk::{ instruction::InstructionError, + signature::Keypair, signature::Signer, transaction::{Transaction, TransactionError}, }; use utils::{DigitalAsset, *}; mod update { - - use mpl_token_metadata::{ - error::MetadataError, - instruction::{CollectionToggle, DelegateArgs, RuleSetToggle, UpdateArgs, UsesToggle}, - state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard, UseMethod, Uses}, - }; - use solana_program::pubkey::Pubkey; - use solana_sdk::signature::Keypair; - use super::*; #[tokio::test] From 61f863ed45c3cc1a63ba1fbc7776ec3325e7b959 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:46:35 -0700 Subject: [PATCH 19/40] Update deps in Cargo.lock and add test using old TM lib * Added a test that uses old token-metadata lib to build the instructions and interact with the data. * As part of this needed to run `cargo update -p mpl-token-metadata@1.9.1` which resulted in the following updates to lateset versions: Updating crates.io index Updating git repository `https://github.com/metaplex-foundation/rooster` Updating mpl-token-metadata v1.8.0 -> v1.9.1 Updating mpl-utils v0.0.6 -> v0.1.0 Updating rooster v0.1.0 (https://github.com/metaplex-foundation/rooster#6923ee3b) -> #ca1221c9 --- token-metadata/Cargo.lock | 39 +- token-metadata/program/Cargo.toml | 1 + .../program/tests/update_with_old_lib.rs | 389 ++++++++++++++++++ 3 files changed, 410 insertions(+), 19 deletions(-) create mode 100644 token-metadata/program/tests/update_with_old_lib.rs diff --git a/token-metadata/Cargo.lock b/token-metadata/Cargo.lock index bdfef551b3..5f8fe9a0ab 100644 --- a/token-metadata/Cargo.lock +++ b/token-metadata/Cargo.lock @@ -1959,19 +1959,25 @@ dependencies = [ [[package]] name = "mpl-token-metadata" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b20f44c87498baa14d504da1098da105cc1ddbb1adb7411fd60e8949c2b901" +version = "1.9.1" dependencies = [ "arrayref", + "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "mpl-utils 0.0.6", + "mpl-token-metadata 1.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-token-metadata-context-derive 0.2.1", + "mpl-utils 0.1.0", "num-derive", "num-traits", + "rmp-serde", + "rooster", + "serde", + "serde_with", "shank 0.0.11", "solana-program", + "solana-program-test", + "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -1980,23 +1986,18 @@ dependencies = [ [[package]] name = "mpl-token-metadata" version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410acbbc7d543108cc4d93f98290da008716c801f04a923cd83de2ec86244c1f" dependencies = [ "arrayref", - "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1", - "mpl-utils 0.1.0", + "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "num-derive", "num-traits", - "rmp-serde", - "rooster", - "serde", - "serde_with", "shank 0.0.11", "solana-program", - "solana-program-test", - "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -2034,9 +2035,7 @@ dependencies = [ [[package]] name = "mpl-utils" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6195ce98b92f1d0ea06d0cc9b2392d81673e02b8fb063589926fa73ee6b071a" +version = "0.1.0" dependencies = [ "arrayref", "borsh", @@ -2047,6 +2046,8 @@ dependencies = [ [[package]] name = "mpl-utils" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc48e64c50dba956acb46eec86d6968ef0401ef37031426da479f1f2b592066" dependencies = [ "arrayref", "borsh", @@ -2857,10 +2858,10 @@ dependencies = [ [[package]] name = "rooster" version = "0.1.0" -source = "git+https://github.com/metaplex-foundation/rooster#6923ee3bf83957920c64ad271ae7cff80b19ab0e" +source = "git+https://github.com/metaplex-foundation/rooster#ca1221c98fb425096f97277031bfa4dd73fe3f29" dependencies = [ "borsh", - "mpl-token-metadata 1.8.0", + "mpl-token-metadata 1.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "mpl-utils 0.0.5", "num-derive", "num-traits", diff --git a/token-metadata/program/Cargo.toml b/token-metadata/program/Cargo.toml index f770cfc9bc..e59894095d 100644 --- a/token-metadata/program/Cargo.toml +++ b/token-metadata/program/Cargo.toml @@ -36,6 +36,7 @@ solana-sdk = "1.14" solana-program-test = "1.14" serde = { version = "1.0.147", features = ["derive"]} async-trait = "0.1.64" +old-token-metadata = { package = "mpl-token-metadata", version = "=1.9.1", features = ["no-entrypoint"] } [lib] crate-type = ["cdylib", "lib"] diff --git a/token-metadata/program/tests/update_with_old_lib.rs b/token-metadata/program/tests/update_with_old_lib.rs new file mode 100644 index 0000000000..e3ee5b50e0 --- /dev/null +++ b/token-metadata/program/tests/update_with_old_lib.rs @@ -0,0 +1,389 @@ +#![cfg(feature = "test-bpf")] + +use old_token_metadata::{ + id, + instruction::{builders::UpdateBuilder, InstructionBuilder}, + instruction::{ + builders::{CreateBuilder, DelegateBuilder}, + CreateArgs, MetadataDelegateRole, + }, + instruction::{CollectionToggle, DelegateArgs, UpdateArgs}, + pda::{find_metadata_delegate_record_account, find_token_record_account}, + state::{ + AssetData, CollectionDetails, Metadata, PrintSupply, TokenMetadataAccount, EDITION, PREFIX, + }, + state::{Collection, Creator, ProgrammableConfig, TokenStandard}, +}; +use solana_program::{borsh::try_from_slice_unchecked, pubkey::Pubkey}; +use solana_program_test::*; +use solana_sdk::{ + account::Account, + compute_budget::ComputeBudgetInstruction, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; + +// This tests backwards compatibility of v1.10 changes with an older version of Token Metadata. +// It uses the binary created from the v1.10 version of token metadata but imports older instructions from +// the 1.9.1 version to ensure that the old instructions still work. + +// Note that to avoid version conflicts, requied test utilities are re-implemented in this file, including +// an `OldDigitalAsset` struct that is a limited version of `DigitalAsset` and compatible with 1.9.1. + +mod update { + use super::*; + + #[tokio::test] + async fn success_update_by_items_collection_item_delegate() { + let context = &mut ProgramTest::new("mpl_token_metadata", mpl_token_metadata::id(), None) + .start_with_context() + .await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = OldDigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Create metadata delegate. + let delegate = Keypair::new(); + airdrop(context, &delegate.pubkey(), 1_000_000_000) + .await + .unwrap(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change a value that this delegate is allowed to change. + let mut update_args = UpdateArgs::default(); + let UpdateArgs::V1 { + collection: collection_toggle, + .. + } = &mut update_args; + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + *collection_toggle = CollectionToggle::Set(new_collection.clone()); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.collection, Some(new_collection)); + } +} + +async fn airdrop( + context: &mut ProgramTestContext, + receiver: &Pubkey, + amount: u64, +) -> Result<(), BanksClientError> { + let tx = Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &context.payer.pubkey(), + receiver, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + Ok(()) +} + +async fn get_account(context: &mut ProgramTestContext, pubkey: &Pubkey) -> Account { + context + .banks_client + .get_account(*pubkey) + .await + .expect("account not found") + .expect("account empty") +} + +// This represents a generic Metaplex Digital asset of various Token Standards. +// It is used to abstract away the various accounts that are created for a given +// Digital Asset. Since different asset types have different accounts, care +// should be taken that appropriate handlers update appropriate accounts, such as when +// transferring a DigitalAsset, the token account should be updated. +struct OldDigitalAsset { + pub metadata: Pubkey, + pub mint: Keypair, + pub token: Option, + pub edition: Option, + pub token_standard: Option, +} + +impl Default for OldDigitalAsset { + fn default() -> Self { + Self::new() + } +} + +impl OldDigitalAsset { + fn new() -> Self { + let mint = Keypair::new(); + let mint_pubkey = mint.pubkey(); + let program_id = id(); + + let metadata_seeds = &[PREFIX.as_bytes(), program_id.as_ref(), mint_pubkey.as_ref()]; + let (metadata, _) = Pubkey::find_program_address(metadata_seeds, &program_id); + + Self { + metadata, + mint, + token: None, + edition: None, + token_standard: None, + } + } + + async fn create( + &mut self, + context: &mut ProgramTestContext, + token_standard: TokenStandard, + authorization_rules: Option, + ) -> Result<(), BanksClientError> { + let creators = Some(vec![Creator { + address: context.payer.pubkey(), + share: 100, + verified: true, + }]); + + self.create_advanced( + context, + token_standard, + String::from("Old Digital Asset"), + String::from("DA"), + String::from("https://digital.asset.org"), + 500, + creators, + None, + None, + authorization_rules, + PrintSupply::Zero, + ) + .await + } + + async fn create_advanced( + &mut self, + context: &mut ProgramTestContext, + token_standard: TokenStandard, + name: String, + symbol: String, + uri: String, + seller_fee_basis_points: u16, + creators: Option>, + collection: Option, + collection_details: Option, + authorization_rules: Option, + print_supply: PrintSupply, + ) -> Result<(), BanksClientError> { + let mut asset = AssetData::new(token_standard, name, symbol, uri); + asset.seller_fee_basis_points = seller_fee_basis_points; + asset.creators = creators; + asset.collection = collection; + asset.collection_details = collection_details; + asset.rule_set = authorization_rules; + + let payer_pubkey = context.payer.pubkey(); + let mint_pubkey = self.mint.pubkey(); + + let program_id = id(); + let mut builder = CreateBuilder::new(); + builder + .metadata(self.metadata) + .mint(self.mint.pubkey()) + .authority(payer_pubkey) + .payer(payer_pubkey) + .update_authority(payer_pubkey) + .initialize_mint(true) + .update_authority_as_signer(true); + + let edition = match token_standard { + TokenStandard::NonFungible | TokenStandard::ProgrammableNonFungible => { + // master edition PDA address + let edition_seeds = &[ + PREFIX.as_bytes(), + program_id.as_ref(), + mint_pubkey.as_ref(), + EDITION.as_bytes(), + ]; + let (edition, _) = Pubkey::find_program_address(edition_seeds, &id()); + // sets the master edition to the builder + builder.master_edition(edition); + Some(edition) + } + _ => None, + }; + // builds the instruction + let create_ix = builder + .build(CreateArgs::V1 { + asset_data: asset, + decimals: Some(0), + print_supply: Some(print_supply), + }) + .unwrap() + .instruction(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(800_000); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, create_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &self.mint], + context.last_blockhash, + ); + + self.edition = edition; + self.token_standard = Some(token_standard); + + context.banks_client.process_transaction(tx).await + } + + async fn delegate( + &mut self, + context: &mut ProgramTestContext, + payer: Keypair, + delegate: Pubkey, + args: DelegateArgs, + ) -> Result, BanksClientError> { + let mut builder = DelegateBuilder::new(); + builder + .delegate(delegate) + .mint(self.mint.pubkey()) + .metadata(self.metadata) + .payer(payer.pubkey()) + .authority(payer.pubkey()) + .spl_token_program(spl_token::ID); + + let mut delegate_or_token_record = None; + + match args { + // Token delegates. + DelegateArgs::SaleV1 { .. } + | DelegateArgs::TransferV1 { .. } + | DelegateArgs::UtilityV1 { .. } + | DelegateArgs::StakingV1 { .. } + | DelegateArgs::LockedTransferV1 { .. } => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + delegate_or_token_record = Some(token_record); + } + DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + DelegateArgs::CollectionV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Collection, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::UpdateV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Update, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::ProgrammableConfigV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfig, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + } + + if let Some(edition) = self.edition { + builder.master_edition(edition); + } + + if let Some(token) = self.token { + builder.token(token); + } + + // determines if we need to set the rule set + let metadata_account = get_account(context, &self.metadata).await; + let metadata: Metadata = try_from_slice_unchecked(&metadata_account.data).unwrap(); + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + builder.authorization_rules(rule_set); + builder.authorization_rules_program(mpl_token_auth_rules::ID); + } + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + + let delegate_ix = builder.build(args.clone()).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, delegate_ix], + Some(&payer.pubkey()), + &[&payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await?; + Ok(delegate_or_token_record) + } + + pub async fn get_metadata(&self, context: &mut ProgramTestContext) -> Metadata { + let metadata_account = context + .banks_client + .get_account(self.metadata) + .await + .unwrap() + .unwrap(); + + Metadata::safe_deserialize(&metadata_account.data).unwrap() + } +} From 882084f29e5ca076ebf2eb8b670c8c1d138dd23d Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:14:43 -0700 Subject: [PATCH 20/40] Update JS test to check invalid role --- token-metadata/js/test/update.test.ts | 151 +++++++++++++------------- 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/token-metadata/js/test/update.test.ts b/token-metadata/js/test/update.test.ts index e09b0eee11..19a2bf8e04 100644 --- a/token-metadata/js/test/update.test.ts +++ b/token-metadata/js/test/update.test.ts @@ -1177,82 +1177,81 @@ test('Update: Invalid Update Authority Fails', async (t) => { await updateTx.assertError(t, /Invalid authority type/); }); -// test('Update: Delegate Authority Type Not Supported', async (t) => { -// const API = new InitTransactions(); -// const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); - -// const daManager = await createDefaultAsset(t, connection, API, handler, payer); - -// // creates a delegate - -// const [, delegate] = await API.getKeypair('Delegate'); -// // delegate PDA -// const [delegateRecord] = PublicKey.findProgramAddressSync( -// [ -// Buffer.from('metadata'), -// PROGRAM_ID.toBuffer(), -// daManager.mint.toBuffer(), -// Buffer.from('update_delegate'), -// payer.publicKey.toBuffer(), -// delegate.publicKey.toBuffer(), -// ], -// PROGRAM_ID, -// ); -// amman.addr.addLabel('Delegate Record', delegateRecord); - -// const args: DelegateArgs = { -// __kind: 'UpdateV1', -// amount: 1, -// authorizationData: null, -// }; - -// const { tx: delegateTx } = await API.delegate( -// delegate.publicKey, -// daManager.mint, -// daManager.metadata, -// payer.publicKey, -// payer, -// args, -// handler, -// delegateRecord, -// daManager.masterEdition, -// ); -// await delegateTx.assertSuccess(t); - -// const assetData = await daManager.getAssetData(connection); -// const authority = delegate; - -// // Change some values and run update. -// const data: Data = { -// name: 'DigitalAsset2', -// symbol: 'DA2', -// uri: 'uri2', -// sellerFeeBasisPoints: 10, -// creators: assetData.creators, -// }; -// const authorizationData = daManager.emptyAuthorizationData(); - -// const updateData = new UpdateTestData(); -// updateData.data = data; -// updateData.authorizationData = authorizationData; - -// const { tx: updateTx } = await API.update( -// t, -// handler, -// daManager.mint, -// daManager.metadata, -// authority, -// updateData, -// delegateRecord, -// daManager.masterEdition, -// ); -// updateTx.then((x) => -// x.assertLogs(t, [/Invalid authority type/i], { -// txLabel: 'tx: Update', -// }), -// ); -// await updateTx.assertError(t); -// }); +test('Update: Delegate Authority Role Not Allowed to Update Data', async (t) => { + const API = new InitTransactions(); + const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); + + const daManager = await createDefaultAsset(t, connection, API, handler, payer); + + // creates a delegate + + const [, delegate] = await API.getKeypair('Delegate'); + // delegate PDA + const [delegateRecord] = PublicKey.findProgramAddressSync( + [ + Buffer.from('metadata'), + PROGRAM_ID.toBuffer(), + daManager.mint.toBuffer(), + Buffer.from('collection_item_delegate'), + payer.publicKey.toBuffer(), + delegate.publicKey.toBuffer(), + ], + PROGRAM_ID, + ); + amman.addr.addLabel('Delegate Record', delegateRecord); + + const args: DelegateArgs = { + __kind: 'CollectionItemV1', + authorizationData: null, + }; + + const { tx: delegateTx } = await API.delegate( + delegate.publicKey, + daManager.mint, + daManager.metadata, + payer.publicKey, + payer, + args, + handler, + delegateRecord, + daManager.masterEdition, + ); + await delegateTx.assertSuccess(t); + + const assetData = await daManager.getAssetData(connection); + const authority = delegate; + + // Change some values and run update. + const data: Data = { + name: 'DigitalAsset2', + symbol: 'DA2', + uri: 'uri2', + sellerFeeBasisPoints: 10, + creators: assetData.creators, + }; + const authorizationData = daManager.emptyAuthorizationData(); + + const updateData = new UpdateTestData(); + updateData.data = data; + updateData.authorizationData = authorizationData; + + const { tx: updateTx } = await API.update( + t, + handler, + daManager.mint, + daManager.metadata, + authority, + updateData, + delegateRecord, + daManager.masterEdition, + ); + updateTx.then((x) => + x.assertLogs(t, [/Authority cannot apply all update args/i], { + txLabel: 'tx: Update', + }), + ); + await updateTx.assertError(t); +}); test('Update: Holder Authority Type Not Supported', async (t) => { const API = new InitTransactions(); From 2facf407f54ac2dccf3a8fa6e61dc043b5c9cc85 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:39:00 -0700 Subject: [PATCH 21/40] Minor deconstruct update and comments --- .../program/src/processor/metadata/update.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 3c962c6245..844fa34194 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -57,6 +57,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // This account should always be a signer regardless of the authority type, // because at least one signer is required to update the metadata. assert_signer(ctx.accounts.authority_info)?; + // Note that payer is not checked because it is not used. // Assert program ownership @@ -97,14 +98,15 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } // If the current rule set is passed in, also require the mpl-token-auth-rules program - // to be passed in. + // to be passed in. Note that we do NOT check the ownership of authorization rules + // here as this allows `Update` to be used to correct a previously invalid `RuleSet`. if ctx.accounts.authorization_rules_info.is_some() { - if let Some(authorization_rules_program) = ctx.accounts.authorization_rules_program_info { - if authorization_rules_program.key != &mpl_token_auth_rules::ID { - return Err(ProgramError::IncorrectProgramId); - } - } else { - return Err(MetadataError::MissingAuthorizationRulesProgram.into()); + let authorization_rules_program = ctx + .accounts + .authorization_rules_program_info + .ok_or(MetadataError::MissingAuthorizationRulesProgram)?; + if authorization_rules_program.key != &mpl_token_auth_rules::ID { + return Err(ProgramError::IncorrectProgramId); } } From 1aea4e4e792caf760b88178a4bc2953e16a890ef Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Mon, 17 Apr 2023 23:08:32 -0700 Subject: [PATCH 22/40] Remove check for mint decimals zero when changing token standard --- token-metadata/program/src/instruction/metadata.rs | 2 +- token-metadata/program/src/processor/metadata/update.rs | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index f44d4cb2c5..01bb39ef0c 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -71,7 +71,7 @@ pub enum TransferArgs { /// Struct representing the values to be updated for an `update` instructions. /// -/// Values that are set to 'None' are not changed; any value set to `Some(_)` will +/// Values that are set to `None` are not changed. Any value set to `Some(...)` will /// have its value updated. There are properties that have three valid states, and /// use a "toggle" type that allows the value to be set, cleared, or remain the same. #[repr(C)] diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 844fa34194..402593ebd6 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -17,7 +17,7 @@ use crate::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, ProgrammableConfig, TokenMetadataAccount, TokenStandard, }, - utils::{assert_derivation, check_token_standard, mint_decimals_is_zero}, + utils::{assert_derivation, check_token_standard}, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -199,7 +199,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro { token_standard } else { - // TODO: What if they have an edition account but choose not to pass it in? check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? }; @@ -208,7 +207,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro let token_standard = match desired_token_standard { Some(desired_token_standard) => { check_desired_token_standard( - mint_decimals_is_zero(ctx.accounts.mint_info)?, existing_or_inferred_token_standard, desired_token_standard, )?; @@ -366,16 +364,11 @@ fn validate_update( } fn check_desired_token_standard( - mint_decimals_is_zero: bool, existing_or_inferred_token_standard: TokenStandard, desired_token_standard: TokenStandard, ) -> ProgramResult { // This function only allows switching between Fungible and FungibleAsset. Mint decimals must // be zero. - if !mint_decimals_is_zero { - return Err(MetadataError::InvalidTokenStandard.into()); - } - match existing_or_inferred_token_standard { TokenStandard::Fungible | TokenStandard::FungibleAsset => match desired_token_standard { TokenStandard::Fungible | TokenStandard::FungibleAsset => Ok(()), From 2e61c7709010e7d27a8388f1488b0f9e8001cbd1 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:14:26 -0700 Subject: [PATCH 23/40] Make Data delegate collection-level and add DataItem delegate Modify tests as well --- .../program/src/instruction/delegate.rs | 15 ++- .../src/processor/delegate/delegate.rs | 10 +- .../program/src/processor/delegate/revoke.rs | 3 +- .../program/src/processor/metadata/update.rs | 22 ++-- token-metadata/program/src/state/metadata.rs | 103 ++++++++++-------- token-metadata/program/tests/update.rs | 41 +++++-- .../program/tests/utils/digital_asset.rs | 27 ++++- 7 files changed, 140 insertions(+), 81 deletions(-) diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 8f4f31b3a2..e8dddfe377 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -58,7 +58,11 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - AuthorityV1 { + AuthorityItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + DataItemV1 { /// Required authorization data to validate the request. authorization_data: Option, }, @@ -86,7 +90,8 @@ pub enum RevokeArgs { LockedTransferV1, ProgrammableConfigV1, MigrationV1, - AuthorityV1, + AuthorityItemV1, + DataItemV1, CollectionItemV1, ProgrammableConfigItemV1, } @@ -95,11 +100,12 @@ pub enum RevokeArgs { #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum MetadataDelegateRole { - Authority, + AuthorityItem, Collection, Use, Data, ProgrammableConfig, + DataItem, CollectionItem, ProgrammableConfigItem, } @@ -107,11 +113,12 @@ pub enum MetadataDelegateRole { impl fmt::Display for MetadataDelegateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self { - Self::Authority => "authority_delegate".to_string(), + Self::AuthorityItem => "authority_item_delegate".to_string(), Self::Collection => "collection_delegate".to_string(), Self::Use => "use_delegate".to_string(), Self::Data => "data_delegate".to_string(), Self::ProgrammableConfig => "programmable_config_delegate".to_string(), + Self::DataItem => "data_item_delegate".to_string(), Self::CollectionItem => "collection_item_delegate".to_string(), Self::ProgrammableConfigItem => "prog_config_item_delegate".to_string(), }; diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index 1ef919e41e..8f370b12c0 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -35,11 +35,12 @@ impl Display for DelegateScenario { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let message = match self { Self::Metadata(role) => match role { - MetadataDelegateRole::Authority => "Authority".to_string(), + MetadataDelegateRole::AuthorityItem => "AuthorityItem".to_string(), MetadataDelegateRole::Collection => "Collection".to_string(), MetadataDelegateRole::Use => "Use".to_string(), MetadataDelegateRole::Data => "Data".to_string(), MetadataDelegateRole::ProgrammableConfig => "ProgrammableConfig".to_string(), + MetadataDelegateRole::DataItem => "DataItem".to_string(), MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), MetadataDelegateRole::ProgrammableConfigItem => { "ProgrammableConfigItem".to_string() @@ -129,8 +130,11 @@ pub fn delegate<'a>( DelegateArgs::ProgrammableConfigV1 { authorization_data } => { Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) } - DelegateArgs::AuthorityV1 { authorization_data } => { - Some((MetadataDelegateRole::Authority, authorization_data)) + DelegateArgs::AuthorityItemV1 { authorization_data } => { + Some((MetadataDelegateRole::AuthorityItem, authorization_data)) + } + DelegateArgs::DataItemV1 { authorization_data } => { + Some((MetadataDelegateRole::DataItem, authorization_data)) } DelegateArgs::CollectionItemV1 { authorization_data } => { Some((MetadataDelegateRole::CollectionItem, authorization_data)) diff --git a/token-metadata/program/src/processor/delegate/revoke.rs b/token-metadata/program/src/processor/delegate/revoke.rs index 0725f1c1ef..70c8441ac7 100644 --- a/token-metadata/program/src/processor/delegate/revoke.rs +++ b/token-metadata/program/src/processor/delegate/revoke.rs @@ -57,7 +57,8 @@ pub fn revoke<'a>( RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), - RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), + RevokeArgs::AuthorityItemV1 => Some(MetadataDelegateRole::AuthorityItem), + RevokeArgs::DataItemV1 => Some(MetadataDelegateRole::DataItem), RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 402593ebd6..2a0a39922d 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -160,14 +160,16 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro token_account: token.as_ref(), metadata_delegate_record_info: ctx.accounts.delegate_record_info, metadata_delegate_roles: vec![ - MetadataDelegateRole::Authority, + MetadataDelegateRole::AuthorityItem, MetadataDelegateRole::Data, + MetadataDelegateRole::DataItem, MetadataDelegateRole::Collection, MetadataDelegateRole::CollectionItem, MetadataDelegateRole::ProgrammableConfig, MetadataDelegateRole::ProgrammableConfigItem, ], collection_metadata_delegate_roles: vec![ + MetadataDelegateRole::Data, MetadataDelegateRole::Collection, MetadataDelegateRole::ProgrammableConfig, ], @@ -295,7 +297,7 @@ fn validate_update( // the delegate is only updating fields that it has access to if let Some(metadata_delegate_role) = metadata_delegate_role { match metadata_delegate_role { - MetadataDelegateRole::Authority => { + MetadataDelegateRole::AuthorityItem => { // Fields allowed for `Authority`: // `new_update_authority` // `primary_sale_happened` @@ -310,13 +312,13 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { - // Fields allowed for `Collection` and `CollectionItem`: - // `collection` + MetadataDelegateRole::Data | MetadataDelegateRole::DataItem => { + // Fields allowed for `Data`: + // `data` if new_update_authority.is_some() - || data.is_some() || primary_sale_happened.is_some() || is_mutable.is_some() + || collection.is_some() || collection_details.is_some() || uses.is_some() || rule_set.is_some() @@ -325,13 +327,13 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - MetadataDelegateRole::Data => { - // Fields allowed for `Data`: - // `data` + MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { + // Fields allowed for `Collection` and `CollectionItem`: + // `collection` if new_update_authority.is_some() + || data.is_some() || primary_sale_happened.is_some() || is_mutable.is_some() - || collection.is_some() || collection_details.is_some() || uses.is_some() || rule_set.is_some() diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index 477282c7e7..5a773fccf1 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -131,8 +131,58 @@ impl Metadata { // Update the token standard if it is changed. self.token_standard = Some(token_standard); + // Update the Update Authority only sections. + if matches!(authority_type, AuthorityType::Metadata) { + if uses.is_some() { + let uses_option = uses.to_option(); + // If already None leave it as None. + assert_valid_use(&uses_option, &self.uses)?; + self.uses = uses_option; + } + + if let CollectionDetailsToggle::Set(collection_details) = collection_details { + // only unsized collections can have the size set, and only once. + if self.collection_details.is_some() { + return Err(MetadataError::SizedCollection.into()); + } + + self.collection_details = Some(collection_details); + } + } + + // Update Authority sections. + if matches!(authority_type, AuthorityType::Metadata) + || matches!(delegate_role, Some(MetadataDelegateRole::AuthorityItem)) + { + if let Some(authority) = new_update_authority { + self.update_authority = authority; + } + + if let Some(primary_sale) = primary_sale_happened { + // If received primary_sale is true, flip to true. + if primary_sale || !self.primary_sale_happened { + self.primary_sale_happened = primary_sale + } else { + return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into()); + } + } + + if let Some(mutable) = is_mutable { + // If received value is false, flip to false. + if !mutable || self.is_mutable { + self.is_mutable = mutable + } else { + return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); + } + } + } + + // Update Data section. if matches!(authority_type, AuthorityType::Metadata) - || matches!(delegate_role, Some(MetadataDelegateRole::Data)) + || matches!( + delegate_role, + Some(MetadataDelegateRole::Data | MetadataDelegateRole::DataItem) + ) { if let Some(data) = data { if !self.is_mutable { @@ -150,6 +200,7 @@ impl Metadata { } } + // Update Collection section. if matches!(authority_type, AuthorityType::Metadata) || matches!( delegate_role, @@ -179,50 +230,7 @@ impl Metadata { } } - if matches!(authority_type, AuthorityType::Metadata) { - if uses.is_some() { - let uses_option = uses.to_option(); - // If already None leave it as None. - assert_valid_use(&uses_option, &self.uses)?; - self.uses = uses_option; - } - - if let CollectionDetailsToggle::Set(collection_details) = collection_details { - // only unsized collections can have the size set, and only once. - if self.collection_details.is_some() { - return Err(MetadataError::SizedCollection.into()); - } - - self.collection_details = Some(collection_details); - } - } - - if matches!(authority_type, AuthorityType::Metadata) - || matches!(delegate_role, Some(MetadataDelegateRole::Authority)) - { - if let Some(authority) = new_update_authority { - self.update_authority = authority; - } - - if let Some(primary_sale) = primary_sale_happened { - // If received primary_sale is true, flip to true. - if primary_sale || !self.primary_sale_happened { - self.primary_sale_happened = primary_sale - } else { - return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into()); - } - } - - if let Some(mutable) = is_mutable { - // If received value is false, flip to false. - if !mutable || self.is_mutable { - self.is_mutable = mutable - } else { - return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); - } - } - } - + // Update Programmable Config section. if matches!(authority_type, AuthorityType::Metadata) || matches!( delegate_role, @@ -254,10 +262,9 @@ impl Metadata { } } + // Re-serialize metadata. puff_out_data_fields(self); - clean_write_metadata(self, metadata)?; - - Ok(()) + clean_write_metadata(self, metadata) } pub fn into_asset_data(self) -> AssetData { diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index d7bbc840ff..5640f5ceba 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -100,7 +100,7 @@ mod update { } #[tokio::test] - async fn success_update_by_authority_delegate() { + async fn success_update_by_authority_item_delegate() { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -123,7 +123,7 @@ mod update { context, update_authority, delegate.pubkey(), - DelegateArgs::AuthorityV1 { + DelegateArgs::AuthorityItemV1 { authorization_data: None, }, ) @@ -254,6 +254,23 @@ mod update { #[tokio::test] async fn success_update_by_items_data_delegate() { + let args = DelegateArgs::DataV1 { + authorization_data: None, + }; + + success_update_data_by_items_delegate(args).await; + } + + #[tokio::test] + async fn success_update_by_items_data_item_delegate() { + let args = DelegateArgs::DataItemV1 { + authorization_data: None, + }; + + success_update_data_by_items_delegate(args).await; + } + + async fn success_update_data_by_items_delegate(delegate_args: DelegateArgs) { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -282,14 +299,7 @@ mod update { let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); let delegate_record = da - .delegate( - context, - update_authority, - delegate.pubkey(), - DelegateArgs::DataV1 { - authorization_data: None, - }, - ) + .delegate(context, update_authority, delegate.pubkey(), delegate_args) .await .unwrap() .unwrap(); @@ -515,7 +525,7 @@ mod update { #[tokio::test] async fn fail_update_by_items_authority_delegate() { - let args = DelegateArgs::AuthorityV1 { + let args = DelegateArgs::AuthorityItemV1 { authorization_data: None, }; @@ -549,6 +559,15 @@ mod update { fail_update_by_items_delegate(args).await; } + #[tokio::test] + async fn fail_update_by_items_data_item_delegate() { + let args = DelegateArgs::DataItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + #[tokio::test] async fn fail_update_by_items_collection_item_delegate() { let args = DelegateArgs::CollectionItemV1 { diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index c21e6d0719..6eb85d7f61 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -617,10 +617,20 @@ impl DigitalAsset { builder.delegate_record(delegate_record); delegate_or_token_record = Some(delegate_record); } - DelegateArgs::AuthorityV1 { .. } => { + DelegateArgs::AuthorityItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Authority, + MetadataDelegateRole::AuthorityItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::DataItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::DataItem, &payer.pubkey(), &delegate, ); @@ -853,10 +863,19 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::AuthorityV1 => { + RevokeArgs::AuthorityItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::AuthorityItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::DataItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Authority, + MetadataDelegateRole::DataItem, &payer.pubkey(), &delegate, ); From fe489a646af966b2d67519d0088527b43eb4bde5 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:42:20 -0700 Subject: [PATCH 24/40] Regenerate IDL and SDK --- token-metadata/js/idl/mpl_token_metadata.json | 25 ++++++++++++++++--- .../js/src/generated/types/DelegateArgs.ts | 24 +++++++++++++----- .../generated/types/MetadataDelegateRole.ts | 3 ++- .../js/src/generated/types/RevokeArgs.ts | 3 ++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index 12983fe4f2..4929319ed3 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -4761,7 +4761,20 @@ ] }, { - "name": "AuthorityV1", + "name": "AuthorityItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "DataItemV1", "fields": [ { "name": "authorization_data", @@ -4838,7 +4851,10 @@ "name": "MigrationV1" }, { - "name": "AuthorityV1" + "name": "AuthorityItemV1" + }, + { + "name": "DataItemV1" }, { "name": "CollectionItemV1" @@ -4855,7 +4871,7 @@ "kind": "enum", "variants": [ { - "name": "Authority" + "name": "AuthorityItem" }, { "name": "Collection" @@ -4869,6 +4885,9 @@ { "name": "ProgrammableConfig" }, + { + "name": "DataItem" + }, { "name": "CollectionItem" }, diff --git a/token-metadata/js/src/generated/types/DelegateArgs.ts b/token-metadata/js/src/generated/types/DelegateArgs.ts index df9a9538a6..4ed1910498 100644 --- a/token-metadata/js/src/generated/types/DelegateArgs.ts +++ b/token-metadata/js/src/generated/types/DelegateArgs.ts @@ -32,7 +32,8 @@ export type DelegateArgsRecord = { authorizationData: beet.COption; }; ProgrammableConfigV1: { authorizationData: beet.COption }; - AuthorityV1: { authorizationData: beet.COption }; + AuthorityItemV1: { authorizationData: beet.COption }; + DataItemV1: { authorizationData: beet.COption }; CollectionItemV1: { authorizationData: beet.COption }; ProgrammableConfigItemV1: { authorizationData: beet.COption }; }; @@ -75,9 +76,12 @@ export const isDelegateArgsLockedTransferV1 = ( export const isDelegateArgsProgrammableConfigV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'ProgrammableConfigV1' } => x.__kind === 'ProgrammableConfigV1'; -export const isDelegateArgsAuthorityV1 = ( +export const isDelegateArgsAuthorityItemV1 = ( x: DelegateArgs, -): x is DelegateArgs & { __kind: 'AuthorityV1' } => x.__kind === 'AuthorityV1'; +): x is DelegateArgs & { __kind: 'AuthorityItemV1' } => x.__kind === 'AuthorityItemV1'; +export const isDelegateArgsDataItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'DataItemV1' } => x.__kind === 'DataItemV1'; export const isDelegateArgsCollectionItemV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'CollectionItemV1' } => x.__kind === 'CollectionItemV1'; @@ -180,10 +184,18 @@ export const delegateArgsBeet = beet.dataEnum([ ], [ - 'AuthorityV1', - new beet.FixableBeetArgsStruct( + 'AuthorityItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["AuthorityItemV1"]', + ), + ], + + [ + 'DataItemV1', + new beet.FixableBeetArgsStruct( [['authorizationData', beet.coption(authorizationDataBeet)]], - 'DelegateArgsRecord["AuthorityV1"]', + 'DelegateArgsRecord["DataItemV1"]', ), ], diff --git a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts index 5ca7165221..8c80ea6c9f 100644 --- a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts +++ b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts @@ -11,11 +11,12 @@ import * as beet from '@metaplex-foundation/beet'; * @category generated */ export enum MetadataDelegateRole { - Authority, + AuthorityItem, Collection, Use, Data, ProgrammableConfig, + DataItem, CollectionItem, ProgrammableConfigItem, } diff --git a/token-metadata/js/src/generated/types/RevokeArgs.ts b/token-metadata/js/src/generated/types/RevokeArgs.ts index fed9bd445d..07758b25ed 100644 --- a/token-metadata/js/src/generated/types/RevokeArgs.ts +++ b/token-metadata/js/src/generated/types/RevokeArgs.ts @@ -21,7 +21,8 @@ export enum RevokeArgs { LockedTransferV1, ProgrammableConfigV1, MigrationV1, - AuthorityV1, + AuthorityItemV1, + DataItemV1, CollectionItemV1, ProgrammableConfigItemV1, } From 308efece828cfad200c2699201da17bb8611b6ee Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:31:01 -0700 Subject: [PATCH 25/40] Use different UpdateArg types for different delegates (#1066) * Use different UpdateArg types for different delegates * Change new UpdateArg types to V2 Also use a Default method to cleanup InternalUpdateArgs --- .../program/src/instruction/metadata.rs | 161 +++++++++++++++++- .../program/src/processor/metadata/update.rs | 159 +++++------------ token-metadata/program/src/state/metadata.rs | 55 +++--- 3 files changed, 215 insertions(+), 160 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 01bb39ef0c..fbb5a22a0b 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -100,7 +100,7 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - V2 { + UpdateAuthorityV2 { /// The new update authority. new_update_authority: Option, /// The metadata details. @@ -124,11 +124,47 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + AuthorityItemDelegateV2 { + /// The new update authority. + new_update_authority: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + is_mutable: Option, + /// Token standard. + token_standard: Option, + }, + CollectionDelegateV2 { + /// Collection information. + collection: CollectionToggle, + }, + DataDelegateV2 { + /// The metadata details. + data: Option, + }, + ProgConfigDelegateV2 { + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + }, + DataItemDelegateV2 { + /// The metadata details. + data: Option, + }, + CollectionItemDelegateV2 { + /// Collection information. + collection: CollectionToggle, + }, + ProgConfigItemDelegateV2 { + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + }, } impl Default for UpdateArgs { fn default() -> Self { - Self::V2 { + Self::UpdateAuthorityV2 { new_update_authority: None, data: None, primary_sale_happened: None, @@ -143,14 +179,121 @@ impl Default for UpdateArgs { } } -#[macro_export] -macro_rules! get_update_args_fields { - ($args:expr, $($field:ident),+) => { - match $args { - UpdateArgs::V1 { $($field,)+ .. } => ($($field,)+), - UpdateArgs::V2 { $($field,)+ .. } => ($($field,)+), +pub(crate) struct InternalUpdateArgs { + /// The new update authority. + pub new_update_authority: Option, + /// The metadata details. + pub data: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + pub primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + pub is_mutable: Option, + /// Collection information. + pub collection: CollectionToggle, + /// Additional details of the collection. + pub collection_details: CollectionDetailsToggle, + /// Uses information. + pub uses: UsesToggle, + // Programmable rule set configuration (only applicable to `Programmable` asset types). + pub rule_set: RuleSetToggle, + /// Token standard. + pub token_standard: Option, +} + +impl Default for InternalUpdateArgs { + fn default() -> Self { + Self { + new_update_authority: None, + data: None, + primary_sale_happened: None, + is_mutable: None, + collection: CollectionToggle::None, + collection_details: CollectionDetailsToggle::None, + uses: UsesToggle::None, + rule_set: RuleSetToggle::None, + token_standard: None, } - }; + } +} + +impl From for InternalUpdateArgs { + fn from(args: UpdateArgs) -> Self { + match args { + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } => Self { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard: None, + }, + UpdateArgs::UpdateAuthorityV2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + .. + } => Self { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + }, + UpdateArgs::AuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard, + .. + } => Self { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard, + ..Default::default() + }, + UpdateArgs::CollectionDelegateV2 { collection, .. } + | UpdateArgs::CollectionItemDelegateV2 { collection, .. } => Self { + collection, + ..Default::default() + }, + UpdateArgs::DataDelegateV2 { data, .. } + | UpdateArgs::DataItemDelegateV2 { data, .. } => Self { + data, + ..Default::default() + }, + UpdateArgs::ProgConfigDelegateV2 { rule_set, .. } + | UpdateArgs::ProgConfigItemDelegateV2 { rule_set, .. } => Self { + rule_set, + ..Default::default() + }, + } + } } //-- Toggle implementations diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 2a0a39922d..e6608b476d 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -10,8 +10,9 @@ use spl_token::state::Account; use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, - get_update_args_fields, - instruction::{CollectionToggle, Context, MetadataDelegateRole, Update, UpdateArgs}, + instruction::{ + CollectionToggle, Context, InternalUpdateArgs, MetadataDelegateRole, Update, UpdateArgs, + }, pda::{EDITION, PREFIX}, state::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, @@ -44,10 +45,7 @@ pub fn update<'a>( ) -> ProgramResult { let context = Update::to_context(accounts)?; - match args { - UpdateArgs::V1 { .. } => update_v1(program_id, context, args), - UpdateArgs::V2 { .. } => update_v1(program_id, context, args), - } + update_v1(program_id, context, args) } fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> ProgramResult { @@ -127,6 +125,9 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro (None, None) }; + // Copy the args into a struct that contains all available fields. + let internal_args = InternalUpdateArgs::from(args.clone()); + // there is a special case for collection-level delegates, where the // validation should use the collection key as the mint parameter let existing_collection_mint = metadata @@ -139,7 +140,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // `MetadataDelegateRole::Collection` or `MetadataDelegateRole::CollectionItem`, // then it will fail in `validate_update` because those are the only roles that // can change collection. - let collection_mint = match get_update_args_fields!(&args, collection).0 { + let collection_mint = match &internal_args.collection { CollectionToggle::Set(Collection { key, .. }) => Some(key), _ => existing_collection_mint, }; @@ -181,24 +182,11 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ..Default::default() })?; - // Check if caller passed in a desired token standard. - let desired_token_standard = match args { - UpdateArgs::V1 { .. } => None, - UpdateArgs::V2 { token_standard, .. } => token_standard, - }; - - // Validate that authority has permission to update the fields that have been specified in the - // update args. - validate_update( - &args, - &authority_type, - metadata_delegate_role, - desired_token_standard, - )?; + // Validate that authority has permission to use the update args that were provided. + validate_update(&args, &authority_type, metadata_delegate_role)?; // Find existing token standard from metadata or infer it. - let existing_or_inferred_token_standard = if let Some(token_standard) = metadata.token_standard - { + let existing_or_inferred_token_std = if let Some(token_standard) = metadata.token_standard { token_standard } else { check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? @@ -206,15 +194,12 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // If there is a desired token standard, use it if it passes the check. If there is not a // desired token standard, use the existing or inferred token standard. - let token_standard = match desired_token_standard { + let token_standard = match internal_args.token_standard { Some(desired_token_standard) => { - check_desired_token_standard( - existing_or_inferred_token_standard, - desired_token_standard, - )?; + check_desired_token_standard(existing_or_inferred_token_std, desired_token_standard)?; desired_token_standard } - None => existing_or_inferred_token_standard, + None => existing_or_inferred_token_std, }; // For pNFTs, we need to validate the authorization rules. @@ -251,7 +236,6 @@ fn validate_update( args: &UpdateArgs, authority_type: &AuthorityType, metadata_delegate_role: Option, - desired_token_standard: Option, ) -> ProgramResult { // validate the authority type match authority_type { @@ -271,93 +255,38 @@ fn validate_update( _ => return Err(MetadataError::InvalidAuthorityType.into()), } - // Destructure args. - let ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - ) = get_update_args_fields!( - args, - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set - ); - // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to if let Some(metadata_delegate_role) = metadata_delegate_role { match metadata_delegate_role { - MetadataDelegateRole::AuthorityItem => { - // Fields allowed for `Authority`: - // `new_update_authority` - // `primary_sale_happened` - // `is_mutable` - // `token_standard` - if data.is_some() - || collection.is_some() - || collection_details.is_some() - || uses.is_some() - || rule_set.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); - } - } - MetadataDelegateRole::Data | MetadataDelegateRole::DataItem => { - // Fields allowed for `Data`: - // `data` - if new_update_authority.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection.is_some() - || collection_details.is_some() - || uses.is_some() - || rule_set.is_some() - || desired_token_standard.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); - } - } - MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { - // Fields allowed for `Collection` and `CollectionItem`: - // `collection` - if new_update_authority.is_some() - || data.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection_details.is_some() - || uses.is_some() - || rule_set.is_some() - || desired_token_standard.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); - } - } - MetadataDelegateRole::ProgrammableConfig - | MetadataDelegateRole::ProgrammableConfigItem => { - // Fields allowed for `ProgrammableConfig` and `ProgrammableConfigItem`: - // `rule_set` - if new_update_authority.is_some() - || data.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection.is_some() - || collection_details.is_some() - || uses.is_some() - || desired_token_standard.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); - } - } + MetadataDelegateRole::AuthorityItem => match args { + UpdateArgs::AuthorityItemDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, + MetadataDelegateRole::Data => match args { + UpdateArgs::DataDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, + MetadataDelegateRole::DataItem => match args { + UpdateArgs::DataItemDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, + MetadataDelegateRole::Collection => match args { + UpdateArgs::CollectionDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, + MetadataDelegateRole::CollectionItem => match args { + UpdateArgs::CollectionItemDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, + MetadataDelegateRole::ProgrammableConfig => match args { + UpdateArgs::ProgConfigDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, + MetadataDelegateRole::ProgrammableConfigItem => match args { + UpdateArgs::ProgConfigItemDelegateV2 { .. } => (), + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }, _ => return Err(MetadataError::InvalidAuthorityType.into()), } } @@ -366,12 +295,12 @@ fn validate_update( } fn check_desired_token_standard( - existing_or_inferred_token_standard: TokenStandard, + existing_or_inferred_token_std: TokenStandard, desired_token_standard: TokenStandard, ) -> ProgramResult { // This function only allows switching between Fungible and FungibleAsset. Mint decimals must // be zero. - match existing_or_inferred_token_standard { + match existing_or_inferred_token_std { TokenStandard::Fungible | TokenStandard::FungibleAsset => match desired_token_standard { TokenStandard::Fungible | TokenStandard::FungibleAsset => Ok(()), _ => Err(MetadataError::InvalidTokenStandard.into()), diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index 5a773fccf1..a81c46a574 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -4,9 +4,9 @@ use crate::{ collection::assert_collection_update_is_valid, metadata::assert_data_valid, uses::assert_valid_use, }, - get_update_args_fields, instruction::{ - CollectionDetailsToggle, CollectionToggle, MetadataDelegateRole, RuleSetToggle, UpdateArgs, + CollectionDetailsToggle, CollectionToggle, InternalUpdateArgs, MetadataDelegateRole, + RuleSetToggle, UpdateArgs, }, utils::{clean_write_metadata, puff_out_data_fields}, }; @@ -106,41 +106,22 @@ impl Metadata { authority_type: AuthorityType, delegate_role: Option, ) -> ProgramResult { - // Destructure args. - let ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - ) = get_update_args_fields!( - args, - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set - ); + // Convert the args into a struct that contains all available fields. + let args = InternalUpdateArgs::from(args); // Update the token standard if it is changed. self.token_standard = Some(token_standard); // Update the Update Authority only sections. if matches!(authority_type, AuthorityType::Metadata) { - if uses.is_some() { - let uses_option = uses.to_option(); + if args.uses.is_some() { + let uses_option = args.uses.to_option(); // If already None leave it as None. assert_valid_use(&uses_option, &self.uses)?; self.uses = uses_option; } - if let CollectionDetailsToggle::Set(collection_details) = collection_details { + if let CollectionDetailsToggle::Set(collection_details) = args.collection_details { // only unsized collections can have the size set, and only once. if self.collection_details.is_some() { return Err(MetadataError::SizedCollection.into()); @@ -154,11 +135,11 @@ impl Metadata { if matches!(authority_type, AuthorityType::Metadata) || matches!(delegate_role, Some(MetadataDelegateRole::AuthorityItem)) { - if let Some(authority) = new_update_authority { + if let Some(authority) = args.new_update_authority { self.update_authority = authority; } - if let Some(primary_sale) = primary_sale_happened { + if let Some(primary_sale) = args.primary_sale_happened { // If received primary_sale is true, flip to true. if primary_sale || !self.primary_sale_happened { self.primary_sale_happened = primary_sale @@ -167,7 +148,7 @@ impl Metadata { } } - if let Some(mutable) = is_mutable { + if let Some(mutable) = args.is_mutable { // If received value is false, flip to false. if !mutable || self.is_mutable { self.is_mutable = mutable @@ -184,7 +165,7 @@ impl Metadata { Some(MetadataDelegateRole::Data | MetadataDelegateRole::DataItem) ) { - if let Some(data) = data { + if let Some(data) = args.data { if !self.is_mutable { return Err(MetadataError::DataIsImmutable.into()); } @@ -210,9 +191,9 @@ impl Metadata { // if the Collection data is 'Set', only allow updating if it is unverified // or if it exactly matches the existing collection info; if the Collection data // is 'Clear', then only set to 'None' if it is unverified. - match collection { + match args.collection { CollectionToggle::Set(_) => { - let collection_option = collection.to_option(); + let collection_option = args.collection.to_option(); assert_collection_update_is_valid(false, &self.collection, &collection_option)?; self.collection = collection_option; } @@ -242,7 +223,7 @@ impl Metadata { { // if the rule_set data is either 'Set' or 'Clear', only allow updating if the // token standard is equal to `ProgrammableNonFungible` and no SPL delegate is set. - if matches!(rule_set, RuleSetToggle::Clear | RuleSetToggle::Set(_)) { + if matches!(args.rule_set, RuleSetToggle::Clear | RuleSetToggle::Set(_)) { if token_standard != TokenStandard::ProgrammableNonFungible { return Err(MetadataError::InvalidTokenStandard.into()); } @@ -256,9 +237,11 @@ impl Metadata { } self.programmable_config = - rule_set.to_option().map(|rule_set| ProgrammableConfig::V1 { - rule_set: Some(rule_set), - }); + args.rule_set + .to_option() + .map(|rule_set| ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }); } } From 84e333a9727c5ab477b5c8f5968883c75cacb3d4 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 20 Apr 2023 21:33:38 -0700 Subject: [PATCH 26/40] Revert to master Cargo.toml and lock file --- token-metadata/Cargo.lock | 45 +++++++++++++++---------------- token-metadata/program/Cargo.toml | 5 ++-- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/token-metadata/Cargo.lock b/token-metadata/Cargo.lock index 5f8fe9a0ab..8b0b80eb67 100644 --- a/token-metadata/Cargo.lock +++ b/token-metadata/Cargo.lock @@ -1865,7 +1865,7 @@ dependencies = [ "bincode", "borsh", "clap 2.34.0", - "mpl-token-metadata 1.9.1", + "mpl-token-metadata 1.10.0", "solana-clap-utils", "solana-cli-config", "solana-client", @@ -1959,25 +1959,19 @@ dependencies = [ [[package]] name = "mpl-token-metadata" -version = "1.9.1" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b20f44c87498baa14d504da1098da105cc1ddbb1adb7411fd60e8949c2b901" dependencies = [ "arrayref", - "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata 1.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "mpl-token-metadata-context-derive 0.2.1", - "mpl-utils 0.1.0", + "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-utils 0.0.6", "num-derive", "num-traits", - "rmp-serde", - "rooster", - "serde", - "serde_with", "shank 0.0.11", "solana-program", - "solana-program-test", - "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -1985,19 +1979,24 @@ dependencies = [ [[package]] name = "mpl-token-metadata" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410acbbc7d543108cc4d93f98290da008716c801f04a923cd83de2ec86244c1f" +version = "1.10.0" dependencies = [ "arrayref", + "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "mpl-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-token-metadata-context-derive 0.2.1", + "mpl-utils 0.2.0", "num-derive", "num-traits", + "rmp-serde", + "rooster", + "serde", + "serde_with", "shank 0.0.11", "solana-program", + "solana-program-test", + "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -2035,7 +2034,9 @@ dependencies = [ [[package]] name = "mpl-utils" -version = "0.1.0" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6195ce98b92f1d0ea06d0cc9b2392d81673e02b8fb063589926fa73ee6b071a" dependencies = [ "arrayref", "borsh", @@ -2045,9 +2046,7 @@ dependencies = [ [[package]] name = "mpl-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc48e64c50dba956acb46eec86d6968ef0401ef37031426da479f1f2b592066" +version = "0.2.0" dependencies = [ "arrayref", "borsh", @@ -2858,10 +2857,10 @@ dependencies = [ [[package]] name = "rooster" version = "0.1.0" -source = "git+https://github.com/metaplex-foundation/rooster#ca1221c98fb425096f97277031bfa4dd73fe3f29" +source = "git+https://github.com/metaplex-foundation/rooster#6923ee3bf83957920c64ad271ae7cff80b19ab0e" dependencies = [ "borsh", - "mpl-token-metadata 1.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-token-metadata 1.8.0", "mpl-utils 0.0.5", "num-derive", "num-traits", diff --git a/token-metadata/program/Cargo.toml b/token-metadata/program/Cargo.toml index e59894095d..fb764b067e 100644 --- a/token-metadata/program/Cargo.toml +++ b/token-metadata/program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mpl-token-metadata" -version = "1.9.1" +version = "1.10.0" description = "Metaplex Metadata" authors = ["Metaplex Developers "] repository = "https://github.com/metaplex-foundation/metaplex-program-library" @@ -26,7 +26,7 @@ borsh = "0.9.2" shank = { version = "0.0.11" } serde = { version = "1.0.149", optional = true } serde_with = { version = "1.14.0", optional = true } -mpl-utils = { version = "0.1.0", path="../../core/rust/utils" } +mpl-utils = { version = "0.2.0", path="../../core/rust/utils" } mpl-token-metadata-context-derive = { version = "0.2.1", path = "../macro" } [dev-dependencies] @@ -36,7 +36,6 @@ solana-sdk = "1.14" solana-program-test = "1.14" serde = { version = "1.0.147", features = ["derive"]} async-trait = "0.1.64" -old-token-metadata = { package = "mpl-token-metadata", version = "=1.9.1", features = ["no-entrypoint"] } [lib] crate-type = ["cdylib", "lib"] From 3afec31dd1ec13d844d20407f604930208d04824 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 20 Apr 2023 21:41:42 -0700 Subject: [PATCH 27/40] Doing cargo update -p mpl-token-metadata@1.10.0 --- token-metadata/Cargo.lock | 37 ++++++++++++++++--------------- token-metadata/program/Cargo.toml | 1 + 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/token-metadata/Cargo.lock b/token-metadata/Cargo.lock index 8b0b80eb67..2a2bd2ca80 100644 --- a/token-metadata/Cargo.lock +++ b/token-metadata/Cargo.lock @@ -1959,19 +1959,25 @@ dependencies = [ [[package]] name = "mpl-token-metadata" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b20f44c87498baa14d504da1098da105cc1ddbb1adb7411fd60e8949c2b901" +version = "1.10.0" dependencies = [ "arrayref", + "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "mpl-utils 0.0.6", + "mpl-token-metadata 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-token-metadata-context-derive 0.2.1", + "mpl-utils 0.2.0", "num-derive", "num-traits", + "rmp-serde", + "rooster", + "serde", + "serde_with", "shank 0.0.11", "solana-program", + "solana-program-test", + "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -1980,23 +1986,18 @@ dependencies = [ [[package]] name = "mpl-token-metadata" version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e674db8846d4a603454ce9c93233dd8d503d62f2afe1b9e6486ce81e431f89" dependencies = [ "arrayref", - "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1", - "mpl-utils 0.2.0", + "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-utils 0.1.0", "num-derive", "num-traits", - "rmp-serde", - "rooster", - "serde", - "serde_with", "shank 0.0.11", "solana-program", - "solana-program-test", - "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -2034,9 +2035,9 @@ dependencies = [ [[package]] name = "mpl-utils" -version = "0.0.6" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6195ce98b92f1d0ea06d0cc9b2392d81673e02b8fb063589926fa73ee6b071a" +checksum = "7fc48e64c50dba956acb46eec86d6968ef0401ef37031426da479f1f2b592066" dependencies = [ "arrayref", "borsh", @@ -2857,10 +2858,10 @@ dependencies = [ [[package]] name = "rooster" version = "0.1.0" -source = "git+https://github.com/metaplex-foundation/rooster#6923ee3bf83957920c64ad271ae7cff80b19ab0e" +source = "git+https://github.com/metaplex-foundation/rooster#ca1221c98fb425096f97277031bfa4dd73fe3f29" dependencies = [ "borsh", - "mpl-token-metadata 1.8.0", + "mpl-token-metadata 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "mpl-utils 0.0.5", "num-derive", "num-traits", diff --git a/token-metadata/program/Cargo.toml b/token-metadata/program/Cargo.toml index fb764b067e..b7b369dd8c 100644 --- a/token-metadata/program/Cargo.toml +++ b/token-metadata/program/Cargo.toml @@ -36,6 +36,7 @@ solana-sdk = "1.14" solana-program-test = "1.14" serde = { version = "1.0.147", features = ["derive"]} async-trait = "0.1.64" +old-token-metadata = { package = "mpl-token-metadata", version = "=1.10.0", features = ["no-entrypoint"] } [lib] crate-type = ["cdylib", "lib"] From a075afdd8ebb2d041b765d031556d2bac50b75c9 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 21 Apr 2023 01:27:51 -0700 Subject: [PATCH 28/40] Update tests for new UpdateArgs variants - Adding authorization data to all UpdateArgs variants. - Add helper functions to create default versions for each UpdateArgs variant. - Add back in legacy support for ProgrammableConfig delegate so that it can use V1 UpdateArgs. - Update tests to use new UpdateArgs. - Add more tests for ProgrammableConfig delegate. - Update "old lib" test to test ProgrammableConfig delegate. --- .../program/src/instruction/metadata.rs | 106 ++- .../program/src/processor/metadata/update.rs | 24 + token-metadata/program/tests/unverify.rs | 23 +- token-metadata/program/tests/update.rs | 755 ++++++++++++++---- .../program/tests/update_with_old_lib.rs | 300 ++++++- 5 files changed, 984 insertions(+), 224 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index fbb5a22a0b..21d9aa981c 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -135,48 +135,128 @@ pub enum UpdateArgs { is_mutable: Option, /// Token standard. token_standard: Option, + /// Required authorization data to validate the request. + authorization_data: Option, }, CollectionDelegateV2 { /// Collection information. collection: CollectionToggle, + /// Required authorization data to validate the request. + authorization_data: Option, }, DataDelegateV2 { /// The metadata details. data: Option, + /// Required authorization data to validate the request. + authorization_data: Option, }, ProgConfigDelegateV2 { // Programmable rule set configuration (only applicable to `Programmable` asset types). rule_set: RuleSetToggle, + /// Required authorization data to validate the request. + authorization_data: Option, }, DataItemDelegateV2 { /// The metadata details. data: Option, + /// Required authorization data to validate the request. + authorization_data: Option, }, CollectionItemDelegateV2 { /// Collection information. collection: CollectionToggle, + /// Required authorization data to validate the request. + authorization_data: Option, }, ProgConfigItemDelegateV2 { // Programmable rule set configuration (only applicable to `Programmable` asset types). rule_set: RuleSetToggle, + /// Required authorization data to validate the request. + authorization_data: Option, }, } -impl Default for UpdateArgs { - fn default() -> Self { +impl UpdateArgs { + pub fn default_v1() -> Self { + Self::V1 { + new_update_authority: None, + data: None, + primary_sale_happened: None, + is_mutable: None, + collection: CollectionToggle::default(), + collection_details: CollectionDetailsToggle::default(), + uses: UsesToggle::default(), + rule_set: RuleSetToggle::default(), + authorization_data: None, + } + } + + pub fn default_update_authority() -> Self { Self::UpdateAuthorityV2 { new_update_authority: None, data: None, primary_sale_happened: None, is_mutable: None, - collection: CollectionToggle::None, - collection_details: CollectionDetailsToggle::None, - uses: UsesToggle::None, - rule_set: RuleSetToggle::None, + collection: CollectionToggle::default(), + collection_details: CollectionDetailsToggle::default(), + uses: UsesToggle::default(), + rule_set: RuleSetToggle::default(), + token_standard: None, + authorization_data: None, + } + } + + pub fn default_authority_item_delegate() -> Self { + Self::AuthorityItemDelegateV2 { + new_update_authority: None, + primary_sale_happened: None, + is_mutable: None, token_standard: None, authorization_data: None, } } + + pub fn default_collection_delegate() -> Self { + Self::CollectionDelegateV2 { + collection: CollectionToggle::default(), + authorization_data: None, + } + } + + pub fn default_data_delegate() -> Self { + Self::DataDelegateV2 { + data: None, + authorization_data: None, + } + } + + pub fn default_prog_config_delegate() -> Self { + Self::ProgConfigDelegateV2 { + rule_set: RuleSetToggle::default(), + authorization_data: None, + } + } + + pub fn default_data_item_delegate() -> Self { + Self::DataItemDelegateV2 { + data: None, + authorization_data: None, + } + } + + pub fn default_collection_item_delegate() -> Self { + Self::CollectionItemDelegateV2 { + collection: CollectionToggle::default(), + authorization_data: None, + } + } + + pub fn default_prog_config_item_delegate() -> Self { + Self::ProgConfigItemDelegateV2 { + rule_set: RuleSetToggle::default(), + authorization_data: None, + } + } } pub(crate) struct InternalUpdateArgs { @@ -240,7 +320,7 @@ impl From for InternalUpdateArgs { collection_details, uses, rule_set, - token_standard: None, + ..Default::default() }, UpdateArgs::UpdateAuthorityV2 { new_update_authority, @@ -300,8 +380,9 @@ impl From for InternalUpdateArgs { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum CollectionToggle { + #[default] None, Clear, Set(Collection), @@ -335,8 +416,9 @@ impl CollectionToggle { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum UsesToggle { + #[default] None, Clear, Set(Uses), @@ -370,8 +452,9 @@ impl UsesToggle { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum CollectionDetailsToggle { + #[default] None, Clear, Set(CollectionDetails), @@ -408,8 +491,9 @@ impl CollectionDetailsToggle { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum RuleSetToggle { + #[default] None, Clear, Set(Pubkey), diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index e6608b476d..04c27478a8 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -280,6 +280,30 @@ fn validate_update( _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::ProgrammableConfig => match args { + // V1 supported Programmable config, leaving here for backwards + // compatibility. + UpdateArgs::V1 { + data, + primary_sale_happened, + is_mutable, + collection, + uses, + new_update_authority, + collection_details, + .. + } => { + // can only update the programmable config + if data.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection.is_some() + || uses.is_some() + || new_update_authority.is_some() + || collection_details.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } UpdateArgs::ProgConfigDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index 6122a8de4a..f551661a82 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -4,7 +4,6 @@ pub mod utils; use mpl_token_metadata::{ error::MetadataError, - get_update_args_fields, instruction::{BurnArgs, DelegateArgs, MetadataDelegateRole, UpdateArgs, VerificationArgs}, pda::{find_metadata_delegate_record_account, find_token_record_account}, state::{Collection, CollectionDetails, Creator, TokenStandard}, @@ -1909,9 +1908,14 @@ mod unverify_collection { .await .unwrap(); - let mut args = UpdateArgs::default(); - let new_update_authority = get_update_args_fields!(&mut args, new_update_authority); - *new_update_authority.0 = Some(new_collection_update_authority.pubkey()); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + new_update_authority, + .. + } => *new_update_authority = Some(new_collection_update_authority.pubkey()), + _ => panic!("Unexpected enum variant"), + } let payer = context.payer.dirty_clone(); test_items @@ -1980,9 +1984,14 @@ mod unverify_collection { // Change the collection to have a different update authority. let new_collection_update_authority = Keypair::new(); - let mut args = UpdateArgs::default(); - let new_update_authority = get_update_args_fields!(&mut args, new_update_authority); - *new_update_authority.0 = Some(new_collection_update_authority.pubkey()); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + new_update_authority, + .. + } => *new_update_authority = Some(new_collection_update_authority.pubkey()), + _ => panic!("Unexpected enum variant"), + } let payer = context.payer.dirty_clone(); test_items diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 5640f5ceba..2dcdd695b2 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -3,12 +3,11 @@ pub mod utils; use mpl_token_metadata::{ error::MetadataError, - get_update_args_fields, instruction::{ builders::UpdateBuilder, CollectionToggle, DelegateArgs, InstructionBuilder, RuleSetToggle, - UpdateArgs, UsesToggle, + UpdateArgs, }, - state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard, UseMethod, Uses}, + state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard}, state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, utils::puffed_out_string, }; @@ -65,9 +64,13 @@ mod update { seller_fee_basis_points: 0, }; - let mut update_args = UpdateArgs::default(); - let current_data = get_update_args_fields!(&mut update_args, data); - *current_data.0 = Some(data); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -80,7 +83,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -132,16 +135,21 @@ mod update { .unwrap(); // Change a few values that this delegate is allowed to change. - let mut update_args = UpdateArgs::default(); - let (new_update_authority, primary_sale_happened, is_mutable) = get_update_args_fields!( - &mut update_args, - new_update_authority, - primary_sale_happened, - is_mutable - ); - *new_update_authority = Some(delegate.pubkey()); - *primary_sale_happened = Some(true); - *is_mutable = Some(false); + let mut args = UpdateArgs::default_authority_item_delegate(); + + match &mut args { + UpdateArgs::AuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + .. + } => { + *new_update_authority = Some(delegate.pubkey()); + *primary_sale_happened = Some(true); + *is_mutable = Some(false); + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -155,7 +163,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -176,23 +184,55 @@ mod update { #[tokio::test] async fn success_update_by_items_collection_delegate() { - let args = DelegateArgs::CollectionV1 { + let delegate_args = DelegateArgs::CollectionV1 { authorization_data: None, }; - success_update_collection_by_items_delegate(args).await; + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + + let mut update_args = UpdateArgs::default_collection_delegate(); + match &mut update_args { + UpdateArgs::CollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + success_update_collection_by_items_delegate(delegate_args, new_collection, update_args) + .await; } #[tokio::test] async fn success_update_by_items_collection_item_delegate() { - let args = DelegateArgs::CollectionItemV1 { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - success_update_collection_by_items_delegate(args).await; + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + + let mut update_args = UpdateArgs::default_collection_item_delegate(); + match &mut update_args { + UpdateArgs::CollectionItemDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + success_update_collection_by_items_delegate(delegate_args, new_collection, update_args) + .await; } - async fn success_update_collection_by_items_delegate(delegate_args: DelegateArgs) { + async fn success_update_collection_by_items_delegate( + delegate_args: DelegateArgs, + collection: Collection, + update_args: UpdateArgs, + ) { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -214,15 +254,7 @@ mod update { .unwrap() .unwrap(); - // Change a value that this delegate is allowed to change. - let mut update_args = UpdateArgs::default(); - let collection_toggle = get_update_args_fields!(&mut update_args, collection); - let new_collection = Collection { - verified: false, - key: Keypair::new().pubkey(), - }; - *collection_toggle.0 = CollectionToggle::Set(new_collection.clone()); - + // Change the collection. let mut builder = UpdateBuilder::new(); builder .authority(delegate.pubkey()) @@ -249,28 +281,36 @@ mod update { // checks the created metadata values let metadata = da.get_metadata(context).await; - assert_eq!(metadata.collection, Some(new_collection)); + assert_eq!(metadata.collection, Some(collection)); } #[tokio::test] async fn success_update_by_items_data_delegate() { - let args = DelegateArgs::DataV1 { + let delegate_args = DelegateArgs::DataV1 { authorization_data: None, }; - success_update_data_by_items_delegate(args).await; + success_update_data_by_items_delegate(delegate_args, UpdateArgs::default_data_delegate()) + .await; } #[tokio::test] async fn success_update_by_items_data_item_delegate() { - let args = DelegateArgs::DataItemV1 { + let delegate_args = DelegateArgs::DataItemV1 { authorization_data: None, }; - success_update_data_by_items_delegate(args).await; + success_update_data_by_items_delegate( + delegate_args, + UpdateArgs::default_data_item_delegate(), + ) + .await; } - async fn success_update_data_by_items_delegate(delegate_args: DelegateArgs) { + async fn success_update_data_by_items_delegate( + delegate_args: DelegateArgs, + mut update_args: UpdateArgs, + ) { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -316,9 +356,15 @@ mod update { seller_fee_basis_points: 0, }; - let mut update_args = UpdateArgs::default(); - let current_data = get_update_args_fields!(&mut update_args, data); - *current_data.0 = Some(data); + match &mut update_args { + UpdateArgs::DataDelegateV2 { + data: current_data, .. + } => *current_data = Some(data), + UpdateArgs::DataItemDelegateV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + }; let mut builder = UpdateBuilder::new(); builder @@ -387,11 +433,12 @@ mod update { panic!("Missing rule set programmable config"); } - let mut update_args = UpdateArgs::default(); - let rule_set = get_update_args_fields!(&mut update_args, rule_set); - // remove the rule set - *rule_set.0 = RuleSetToggle::Clear; + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -406,7 +453,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -425,23 +472,40 @@ mod update { #[tokio::test] async fn success_update_pnft_by_items_programmable_config_delegate() { - let args = DelegateArgs::ProgrammableConfigV1 { + let delegate_args = DelegateArgs::ProgrammableConfigV1 { authorization_data: None, }; - success_update_pnft_by_items_delegate(args).await; + let mut update_args = UpdateArgs::default_prog_config_delegate(); + match &mut update_args { + UpdateArgs::ProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + success_update_pnft_by_items_delegate(delegate_args, update_args).await; } #[tokio::test] async fn success_update_pnft_by_items_programmable_config_item_delegate() { - let args = DelegateArgs::ProgrammableConfigItemV1 { + let delegate_args = DelegateArgs::ProgrammableConfigItemV1 { authorization_data: None, }; - success_update_pnft_by_items_delegate(args).await; + let mut update_args = UpdateArgs::default_prog_config_item_delegate(); + match &mut update_args { + UpdateArgs::ProgConfigItemDelegateV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Clear + } + _ => panic!("Unexpected enum variant"), + } + + success_update_pnft_by_items_delegate(delegate_args, update_args).await; } - async fn success_update_pnft_by_items_delegate(delegate_args: DelegateArgs) { + async fn success_update_pnft_by_items_delegate( + delegate_args: DelegateArgs, + update_args: UpdateArgs, + ) { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); let context = &mut program_test.start_with_context().await; @@ -486,12 +550,6 @@ mod update { .unwrap(); // Change a value that this delegate is allowed to change. - let mut update_args = UpdateArgs::default(); - let rule_set = get_update_args_fields!(&mut update_args, rule_set); - - // remove the rule set - *rule_set.0 = RuleSetToggle::Clear; - let mut builder = UpdateBuilder::new(); builder .authority(delegate.pubkey()) @@ -519,7 +577,6 @@ mod update { // checks the created metadata values let metadata = da.get_metadata(context).await; - assert_eq!(metadata.programmable_config, None); } @@ -610,9 +667,6 @@ mod update { .await .unwrap(); - let metadata = da.get_metadata(context).await; - assert_eq!(metadata.uses, None); - // Create metadata delegate. let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); @@ -622,16 +676,12 @@ mod update { .unwrap() .unwrap(); - // Change a value that no delegates are allowed to change. - let mut update_args = UpdateArgs::default(); - let uses = get_update_args_fields!(&mut update_args, uses); - - // remove the rule set - *uses.0 = UsesToggle::Set(Uses { - use_method: UseMethod::Multiple, - remaining: 333, - total: 333, - }); + // Use update args variant that no delegates are allowed to use. + let update_args = UpdateArgs::default_update_authority(); + match update_args { + UpdateArgs::UpdateAuthorityV2 { .. } => (), + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -663,11 +713,6 @@ mod update { .unwrap_err(); assert_custom_error!(err, MetadataError::InvalidUpdateArgs); - - // checks the created metadata values - let metadata = da.get_metadata(context).await; - - assert_eq!(metadata.uses, None); } #[tokio::test] @@ -689,9 +734,6 @@ mod update { .await .unwrap(); - let metadata = da.get_metadata(context).await; - assert_eq!(metadata.uses, None); - // Create `TokenDelegate` type of delegate. let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); @@ -705,16 +747,12 @@ mod update { .unwrap() .unwrap(); - // Change a value that no delegates are allowed to change. - let mut update_args = UpdateArgs::default(); - let uses = get_update_args_fields!(&mut update_args, uses); - - // remove the rule set - *uses.0 = UsesToggle::Set(Uses { - use_method: UseMethod::Multiple, - remaining: 333, - total: 333, - }); + // Use update args variant that no delegates are allowed to use. + let update_args = UpdateArgs::default_update_authority(); + match update_args { + UpdateArgs::UpdateAuthorityV2 { .. } => (), + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -745,11 +783,6 @@ mod update { .unwrap_err(); assert_custom_error!(err, MetadataError::InvalidAuthorityType); - - // checks the created metadata values - let metadata = da.get_metadata(context).await; - - assert_eq!(metadata.uses, None); } #[tokio::test] @@ -768,15 +801,15 @@ mod update { assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); // Update token standard - let mut update_args = UpdateArgs::default(); - let token_standard = match &mut update_args { - UpdateArgs::V2 { token_standard, .. } => token_standard, - _ => panic!("Incompatible update args version"), - }; - - *token_standard = Some(TokenStandard::Fungible); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { token_standard, .. } => { + *token_standard = Some(TokenStandard::Fungible) + } + _ => panic!("Unexpected enum variant"), + } - da.update(context, update_authority.dirty_clone(), update_args) + da.update(context, update_authority.dirty_clone(), args) .await .unwrap(); @@ -800,16 +833,16 @@ mod update { assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); // Update token standard - let mut update_args = UpdateArgs::default(); - let token_standard = match &mut update_args { - UpdateArgs::V2 { token_standard, .. } => token_standard, - _ => panic!("Incompatible update args version"), - }; - - *token_standard = Some(TokenStandard::NonFungible); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { token_standard, .. } => { + *token_standard = Some(TokenStandard::NonFungible) + } + _ => panic!("Unexpected enum variant"), + } let err = da - .update(context, update_authority.dirty_clone(), update_args) + .update(context, update_authority.dirty_clone(), args) .await .unwrap_err(); @@ -835,16 +868,21 @@ mod update { assert_eq!(metadata.collection, None); // Set collection to a value with verified set to true. - let mut update_args = UpdateArgs::default(); - let collection_toggle = get_update_args_fields!(&mut update_args, collection); let new_collection = Collection { verified: true, key: Keypair::new().pubkey(), }; - *collection_toggle.0 = CollectionToggle::Set(new_collection.clone()); + + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } let err = da - .update(context, update_authority.dirty_clone(), update_args) + .update(context, update_authority.dirty_clone(), args) .await .unwrap_err(); @@ -899,14 +937,18 @@ mod update { assert_eq!(metadata.collection, None); // Change collection. - let mut update_args = UpdateArgs::default(); - let collection_toggle = get_update_args_fields!(&mut update_args, collection); - let collection = Collection { + let new_collection = Collection { key: collection_parent_da.mint.pubkey(), verified: false, }; - *collection_toggle.0 = CollectionToggle::Set(collection.clone()); + let mut args = UpdateArgs::default_collection_delegate(); + match &mut args { + UpdateArgs::CollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -920,7 +962,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -933,7 +975,7 @@ mod update { // Check that collection changed. let metadata = da.get_metadata(context).await; - assert_eq!(metadata.collection, Some(collection)); + assert_eq!(metadata.collection, Some(new_collection)); } #[tokio::test] @@ -978,14 +1020,18 @@ mod update { assert_eq!(metadata.collection, None); // Change collection. - let mut update_args = UpdateArgs::default(); - let collection_toggle = get_update_args_fields!(&mut update_args, collection); - let collection = Collection { + let new_collection = Collection { key: collection_parent_da.mint.pubkey(), verified: false, }; - *collection_toggle.0 = CollectionToggle::Set(collection.clone()); + let mut args = UpdateArgs::default_collection_item_delegate(); + match &mut args { + UpdateArgs::CollectionItemDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -999,7 +1045,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1063,14 +1109,18 @@ mod update { assert_eq!(metadata.collection, None); // Change collection. - let mut update_args = UpdateArgs::default(); - let collection_toggle = get_update_args_fields!(&mut update_args, collection); - let collection = Collection { + let new_collection = Collection { key: collection_parent_da.mint.pubkey(), verified: false, }; - *collection_toggle.0 = CollectionToggle::Set(collection.clone()); + let mut args = UpdateArgs::default_collection_delegate(); + match &mut args { + UpdateArgs::CollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -1084,7 +1134,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1106,6 +1156,359 @@ mod update { assert_eq!(metadata.collection, None); } + #[tokio::test] + async fn success_update_prog_config_by_collections_prog_config_delegate_v2_args() { + // Change programmable config, removing the RuleSet. + let mut args = UpdateArgs::default_prog_config_delegate(); + match &mut args { + UpdateArgs::ProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + success_update_prog_config_by_collections_prog_config_delegate(args).await + } + + #[tokio::test] + async fn success_update_prog_config_by_collections_prog_config_delegate_v1_args() { + // Change programmable config, removing the RuleSet. + let mut args = UpdateArgs::default_v1(); + match &mut args { + UpdateArgs::V1 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + success_update_prog_config_by_collections_prog_config_delegate(args).await + } + + async fn success_update_prog_config_by_collections_prog_config_delegate( + update_args: UpdateArgs, + ) { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = DigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + // Check collection. + assert_eq!(metadata.collection, collection); + + // Check programmable config. + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.programmable_config, None); + } + + #[tokio::test] + async fn fail_update_prog_config_by_col_prog_config_delegate_wrong_v1_args() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = DigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + // Check collection. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, collection); + + // Check primary sale. + let metadata = da.get_metadata(context).await; + assert!(!metadata.primary_sale_happened); + + // Collection-level programmable config delegate is allowed to use V1 args for backwards + // compatibility. But RuleSet is the only allowed field this delegate can change. + let mut args = UpdateArgs::default_v1(); + match &mut args { + UpdateArgs::V1 { + primary_sale_happened, + .. + } => *primary_sale_happened = Some(true), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidUpdateArgs); + + // Check that metadata not changed. + let metadata = da.get_metadata(context).await; + assert!(!metadata.primary_sale_happened); + } + + #[tokio::test] + async fn fail_update_by_col_prog_config_delegate_using_new_collection_in_v1_args() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = DigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + // Check collection. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, collection); + + let new_collection = Collection { + key: Keypair::new().pubkey(), + verified: false, + }; + + // Collection-level programmable config delegate is allowed to use V1 args for backwards + // compatibility. But RuleSet is the only allowed field this delegate can change. But it + // won't get to that point in the code because it will not be authorized as a collection- + // level delegate based on the new collection it sent in. + let mut args = UpdateArgs::default_v1(); + match &mut args { + UpdateArgs::V1 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, collection); + } + #[tokio::test] async fn update_invalid_rule_set() { // Currently users can add an invalid rule set to their pNFT which will effectively @@ -1145,9 +1548,13 @@ mod update { panic!("Missing rule set programmable config"); } - let mut update_args = UpdateArgs::default(); - let rule_set = get_update_args_fields!(&mut update_args, rule_set); - *rule_set.0 = RuleSetToggle::Set(invalid_rule_set); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Set(invalid_rule_set) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -1161,7 +1568,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args.clone()).unwrap().instruction(); + let update_ix = builder.build(args.clone()).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1199,7 +1606,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1217,9 +1624,13 @@ mod update { assert_custom_error!(err, MetadataError::InvalidAuthorizationRules); // Finally, try to update with the valid rule set, and it should succeed. - let mut update_args = UpdateArgs::default(); - let rule_set = get_update_args_fields!(&mut update_args, rule_set); - *rule_set.0 = RuleSetToggle::Set(authorization_rules); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Set(authorization_rules) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -1234,7 +1645,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1271,7 +1682,7 @@ mod update { let (authorization_rules, auth_data) = create_default_metaplex_rule_set(context, authority.dirty_clone(), false).await; - let (new_auth_rules, new_auth_data) = + let (new_auth_rules, _) = create_default_metaplex_rule_set(context, authority.dirty_clone(), false).await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -1314,11 +1725,11 @@ mod update { .unwrap(); // Try to clear the rule set. - let mut update_args = UpdateArgs::default(); - let rule_set = get_update_args_fields!(&mut update_args, rule_set); - - // remove the rule set - *rule_set.0 = RuleSetToggle::Clear; + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -1333,7 +1744,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1351,13 +1762,13 @@ mod update { assert_custom_error!(err, MetadataError::CannotUpdateAssetWithDelegate); // Try to update the rule set. - let mut update_args = UpdateArgs::default(); - let (rule_set, authorization_data) = - get_update_args_fields!(&mut update_args, rule_set, authorization_data); - - // update the rule set - *rule_set = RuleSetToggle::Set(new_auth_rules); - *authorization_data = Some(new_auth_data); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Set(new_auth_rules) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -1372,7 +1783,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -1431,12 +1842,16 @@ mod update { seller_fee_basis_points: 0, }; - let mut update_args = UpdateArgs::default(); - let current_data = get_update_args_fields!(&mut update_args, data); - *current_data.0 = Some(data); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } let err = da - .update(context, update_authority.dirty_clone(), update_args) + .update(context, update_authority.dirty_clone(), args) .await .unwrap_err(); @@ -1487,11 +1902,15 @@ mod update { seller_fee_basis_points: metadata.data.seller_fee_basis_points, }; - let mut update_args = UpdateArgs::default(); - let current_data = get_update_args_fields!(&mut update_args, data); - *current_data.0 = Some(data); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } - da.update(context, update_authority.dirty_clone(), update_args) + da.update(context, update_authority.dirty_clone(), args) .await .unwrap(); @@ -1509,11 +1928,15 @@ mod update { seller_fee_basis_points: metadata.data.seller_fee_basis_points, }; - let mut update_args = UpdateArgs::default(); - let current_data = get_update_args_fields!(&mut update_args, data); - *current_data.0 = Some(data); + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } - da.update(context, update_authority.dirty_clone(), update_args) + da.update(context, update_authority.dirty_clone(), args) .await .unwrap(); diff --git a/token-metadata/program/tests/update_with_old_lib.rs b/token-metadata/program/tests/update_with_old_lib.rs index e3ee5b50e0..5c66aea26a 100644 --- a/token-metadata/program/tests/update_with_old_lib.rs +++ b/token-metadata/program/tests/update_with_old_lib.rs @@ -1,19 +1,29 @@ #![cfg(feature = "test-bpf")] +use mpl_token_auth_rules::{ + instruction::{ + builders::CreateOrUpdateBuilder, CreateOrUpdateArgs, + InstructionBuilder as AuthRulesInstructionBuilder, + }, + payload::Payload, + state::{CompareOp, Rule, RuleSetV1}, +}; use old_token_metadata::{ id, - instruction::{builders::UpdateBuilder, InstructionBuilder}, instruction::{ - builders::{CreateBuilder, DelegateBuilder}, - CreateArgs, MetadataDelegateRole, + builders::{CreateBuilder, DelegateBuilder, MintBuilder, UpdateBuilder}, + CreateArgs, DelegateArgs, InstructionBuilder, MetadataDelegateRole, MintArgs, + RuleSetToggle, UpdateArgs, }, - instruction::{CollectionToggle, DelegateArgs, UpdateArgs}, pda::{find_metadata_delegate_record_account, find_token_record_account}, + processor::{AuthorizationData, TransferScenario}, state::{ - AssetData, CollectionDetails, Metadata, PrintSupply, TokenMetadataAccount, EDITION, PREFIX, + AssetData, Collection, CollectionDetails, Creator, Metadata, Operation, PayloadKey, + PrintSupply, ProgrammableConfig, TokenMetadataAccount, TokenStandard, EDITION, PREFIX, }, - state::{Collection, Creator, ProgrammableConfig, TokenStandard}, }; +use rmp_serde::Serializer; +use serde::Serialize; use solana_program::{borsh::try_from_slice_unchecked, pubkey::Pubkey}; use solana_program_test::*; use solana_sdk::{ @@ -35,46 +45,79 @@ mod update { use super::*; #[tokio::test] - async fn success_update_by_items_collection_item_delegate() { - let context = &mut ProgramTest::new("mpl_token_metadata", mpl_token_metadata::id(), None) - .start_with_context() - .await; - - let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); - - let mut da = OldDigitalAsset::new(); - da.create(context, TokenStandard::NonFungible, None) + async fn old_lib_success_update_by_collections_programmable_config_delegate() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = OldDigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + Some(CollectionDetails::V1 { size: 0 }), + ) .await .unwrap(); - let metadata = da.get_metadata(context).await; - assert_eq!(metadata.collection, None); - - // Create metadata delegate. + // Create metadata delegate on the collection. let delegate = Keypair::new(); airdrop(context, &delegate.pubkey(), 1_000_000_000) .await .unwrap(); - let delegate_args = DelegateArgs::CollectionV1 { + let delegate_args = DelegateArgs::ProgrammableConfigV1 { authorization_data: None, }; - let delegate_record = da + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da .delegate(context, update_authority, delegate.pubkey(), delegate_args) .await .unwrap() .unwrap(); - // Change a value that this delegate is allowed to change. - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - collection: collection_toggle, - .. - } = &mut update_args; - let new_collection = Collection { + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = create_rule_set(context, authority).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), verified: false, - key: Keypair::new().pubkey(), - }; - *collection_toggle = CollectionToggle::Set(new_collection.clone()); + }); + + let mut da = OldDigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection, + ) + .await + .unwrap(); + + // Check programmable config. + let metadata = da.get_metadata(context).await; + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Change programmable config. + let mut update_args = UpdateArgs::default(); + let UpdateArgs::V1 { rule_set, .. } = &mut update_args; + // remove the rule set + *rule_set = RuleSetToggle::Clear; let mut builder = UpdateBuilder::new(); builder @@ -82,6 +125,8 @@ mod update { .delegate_record(delegate_record) .metadata(da.metadata) .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) .payer(delegate.pubkey()); if let Some(edition) = da.edition { @@ -101,8 +146,7 @@ mod update { // checks the created metadata values let metadata = da.get_metadata(context).await; - - assert_eq!(metadata.collection, Some(new_collection)); + assert_eq!(metadata.programmable_config, None); } } @@ -145,6 +189,7 @@ struct OldDigitalAsset { pub mint: Keypair, pub token: Option, pub edition: Option, + pub token_record: Option, pub token_standard: Option, } @@ -168,22 +213,52 @@ impl OldDigitalAsset { mint, token: None, edition: None, + token_record: None, token_standard: None, } } - async fn create( + async fn create_and_mint_item_with_collection( &mut self, context: &mut ProgramTestContext, token_standard: TokenStandard, authorization_rules: Option, + authorization_data: Option, + amount: u64, + collection: Option, ) -> Result<(), BanksClientError> { - let creators = Some(vec![Creator { - address: context.payer.pubkey(), - share: 100, - verified: true, - }]); + // creates the metadata + self.create_advanced( + context, + token_standard, + String::from("Old Digital Asset"), + String::from("DA"), + String::from("https://digital.asset.org"), + 500, + None, + collection, + None, + authorization_rules, + PrintSupply::Zero, + ) + .await + .unwrap(); + // mints tokens + self.mint(context, authorization_rules, authorization_data, amount) + .await + } + + async fn create_and_mint_collection_parent( + &mut self, + context: &mut ProgramTestContext, + token_standard: TokenStandard, + authorization_rules: Option, + authorization_data: Option, + amount: u64, + collection_details: Option, + ) -> Result<(), BanksClientError> { + // creates the metadata self.create_advanced( context, token_standard, @@ -191,13 +266,18 @@ impl OldDigitalAsset { String::from("DA"), String::from("https://digital.asset.org"), 500, - creators, None, None, + collection_details, authorization_rules, PrintSupply::Zero, ) .await + .unwrap(); + + // mints tokens + self.mint(context, authorization_rules, authorization_data, amount) + .await } async fn create_advanced( @@ -276,6 +356,72 @@ impl OldDigitalAsset { context.banks_client.process_transaction(tx).await } + async fn mint( + &mut self, + context: &mut ProgramTestContext, + authorization_rules: Option, + authorization_data: Option, + amount: u64, + ) -> Result<(), BanksClientError> { + let payer_pubkey = context.payer.pubkey(); + let (token, _) = Pubkey::find_program_address( + &[ + &payer_pubkey.to_bytes(), + &spl_token::id().to_bytes(), + &self.mint.pubkey().to_bytes(), + ], + &spl_associated_token_account::id(), + ); + + let (token_record, _) = find_token_record_account(&self.mint.pubkey(), &token); + + let token_record_opt = if self.is_pnft(context).await { + Some(token_record) + } else { + None + }; + + let mut builder = MintBuilder::new(); + builder + .token(token) + .token_record(token_record) + .token_owner(payer_pubkey) + .metadata(self.metadata) + .mint(self.mint.pubkey()) + .payer(payer_pubkey) + .authority(payer_pubkey); + + if let Some(edition) = self.edition { + builder.master_edition(edition); + } + + if let Some(authorization_rules) = authorization_rules { + builder.authorization_rules(authorization_rules); + } + + let mint_ix = builder + .build(MintArgs::V1 { + amount, + authorization_data, + }) + .unwrap() + .instruction(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(800_000); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, mint_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.map(|_| { + self.token = Some(token); + self.token_record = token_record_opt; + }) + } + async fn delegate( &mut self, context: &mut ProgramTestContext, @@ -386,4 +532,78 @@ impl OldDigitalAsset { Metadata::safe_deserialize(&metadata_account.data).unwrap() } + + async fn is_pnft(&self, context: &mut ProgramTestContext) -> bool { + let md = self.get_metadata(context).await; + if let Some(standard) = md.token_standard { + if standard == TokenStandard::ProgrammableNonFungible { + return true; + } + } + + false + } +} + +async fn create_rule_set( + context: &mut ProgramTestContext, + creator: Keypair, +) -> (Pubkey, AuthorizationData) { + let name = String::from("RuleSet"); + let (ruleset_addr, _ruleset_bump) = + mpl_token_auth_rules::pda::find_rule_set_address(creator.pubkey(), name.clone()); + + let nft_amount = Rule::Amount { + field: PayloadKey::Amount.to_string(), + amount: 1, + operator: CompareOp::Eq, + }; + + let owner_operation = Operation::Transfer { + scenario: TransferScenario::Holder, + }; + + let mut rule_set = RuleSetV1::new(name, creator.pubkey()); + rule_set + .add(owner_operation.to_string(), nft_amount) + .unwrap(); + + // Serialize the RuleSet using RMP serde. + let mut serialized_data = Vec::new(); + rule_set + .serialize(&mut Serializer::new(&mut serialized_data)) + .unwrap(); + + // Create a `create` instruction. + let create_ix = CreateOrUpdateBuilder::new() + .rule_set_pda(ruleset_addr) + .payer(creator.pubkey()) + .build(CreateOrUpdateArgs::V1 { + serialized_rule_set: serialized_data, + }) + .unwrap() + .instruction(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + + // Add it to a transaction. + let create_tx = Transaction::new_signed_with_payer( + &[compute_ix, create_ix], + Some(&creator.pubkey()), + &[&creator], + context.last_blockhash, + ); + + // Process the transaction. + context + .banks_client + .process_transaction(create_tx) + .await + .expect("creation should succeed"); + + // Client can add additional rules to the Payload but does not need to in this case. + let payload = Payload::new(); + let auth_data = AuthorizationData { payload }; + + (ruleset_addr, auth_data) } From 7acf16cb34cbc1de6a58f0b02bef5659a0932f0d Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 21 Apr 2023 01:29:22 -0700 Subject: [PATCH 29/40] Regenerate IDL and JS lib --- token-metadata/js/idl/mpl_token_metadata.json | 159 +++++++++++++++++- .../js/src/generated/types/UpdateArgs.ts | 145 +++++++++++++++- 2 files changed, 297 insertions(+), 7 deletions(-) diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index de6e634d8e..b5a8fd1d8f 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -5049,7 +5049,7 @@ ] }, { - "name": "V2", + "name": "UpdateAuthorityV2", "fields": [ { "name": "new_update_authority", @@ -5118,6 +5118,163 @@ } } ] + }, + { + "name": "AuthorityItemDelegateV2", + "fields": [ + { + "name": "new_update_authority", + "type": { + "option": "publicKey" + } + }, + { + "name": "primary_sale_happened", + "type": { + "option": "bool" + } + }, + { + "name": "is_mutable", + "type": { + "option": "bool" + } + }, + { + "name": "token_standard", + "type": { + "option": { + "defined": "TokenStandard" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "CollectionDelegateV2", + "fields": [ + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "DataDelegateV2", + "fields": [ + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "ProgConfigDelegateV2", + "fields": [ + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "DataItemDelegateV2", + "fields": [ + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "CollectionItemDelegateV2", + "fields": [ + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "ProgConfigItemDelegateV2", + "fields": [ + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } diff --git a/token-metadata/js/src/generated/types/UpdateArgs.ts b/token-metadata/js/src/generated/types/UpdateArgs.ts index 694c5a0328..4849892b20 100644 --- a/token-metadata/js/src/generated/types/UpdateArgs.ts +++ b/token-metadata/js/src/generated/types/UpdateArgs.ts @@ -36,7 +36,7 @@ export type UpdateArgsRecord = { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; - V2: { + UpdateAuthorityV2: { newUpdateAuthority: beet.COption; data: beet.COption; primarySaleHappened: beet.COption; @@ -48,6 +48,34 @@ export type UpdateArgsRecord = { tokenStandard: beet.COption; authorizationData: beet.COption; }; + AuthorityItemDelegateV2: { + newUpdateAuthority: beet.COption; + primarySaleHappened: beet.COption; + isMutable: beet.COption; + tokenStandard: beet.COption; + authorizationData: beet.COption; + }; + CollectionDelegateV2: { + collection: CollectionToggle; + authorizationData: beet.COption; + }; + DataDelegateV2: { data: beet.COption; authorizationData: beet.COption }; + ProgConfigDelegateV2: { + ruleSet: RuleSetToggle; + authorizationData: beet.COption; + }; + DataItemDelegateV2: { + data: beet.COption; + authorizationData: beet.COption; + }; + CollectionItemDelegateV2: { + collection: CollectionToggle; + authorizationData: beet.COption; + }; + ProgConfigItemDelegateV2: { + ruleSet: RuleSetToggle; + authorizationData: beet.COption; + }; }; /** @@ -65,8 +93,33 @@ export type UpdateArgs = beet.DataEnumKeyAsKind; export const isUpdateArgsV1 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V1' } => x.__kind === 'V1'; -export const isUpdateArgsV2 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V2' } => - x.__kind === 'V2'; +export const isUpdateArgsUpdateAuthorityV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'UpdateAuthorityV2' } => x.__kind === 'UpdateAuthorityV2'; +export const isUpdateArgsAuthorityItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AuthorityItemDelegateV2' } => + x.__kind === 'AuthorityItemDelegateV2'; +export const isUpdateArgsCollectionDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'CollectionDelegateV2' } => x.__kind === 'CollectionDelegateV2'; +export const isUpdateArgsDataDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'DataDelegateV2' } => x.__kind === 'DataDelegateV2'; +export const isUpdateArgsProgConfigDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'ProgConfigDelegateV2' } => x.__kind === 'ProgConfigDelegateV2'; +export const isUpdateArgsDataItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'DataItemDelegateV2' } => x.__kind === 'DataItemDelegateV2'; +export const isUpdateArgsCollectionItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'CollectionItemDelegateV2' } => + x.__kind === 'CollectionItemDelegateV2'; +export const isUpdateArgsProgConfigItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'ProgConfigItemDelegateV2' } => + x.__kind === 'ProgConfigItemDelegateV2'; /** * @category userTypes @@ -92,8 +145,8 @@ export const updateArgsBeet = beet.dataEnum([ ], [ - 'V2', - new beet.FixableBeetArgsStruct( + 'UpdateAuthorityV2', + new beet.FixableBeetArgsStruct( [ ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], ['data', beet.coption(dataBeet)], @@ -106,7 +159,87 @@ export const updateArgsBeet = beet.dataEnum([ ['tokenStandard', beet.coption(tokenStandardBeet)], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["V2"]', + 'UpdateArgsRecord["UpdateAuthorityV2"]', + ), + ], + + [ + 'AuthorityItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], + ['primarySaleHappened', beet.coption(beet.bool)], + ['isMutable', beet.coption(beet.bool)], + ['tokenStandard', beet.coption(tokenStandardBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AuthorityItemDelegateV2"]', + ), + ], + + [ + 'CollectionDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['collection', collectionToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["CollectionDelegateV2"]', + ), + ], + + [ + 'DataDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['data', beet.coption(dataBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["DataDelegateV2"]', + ), + ], + + [ + 'ProgConfigDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['ruleSet', ruleSetToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["ProgConfigDelegateV2"]', + ), + ], + + [ + 'DataItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['data', beet.coption(dataBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["DataItemDelegateV2"]', + ), + ], + + [ + 'CollectionItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['collection', collectionToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["CollectionItemDelegateV2"]', + ), + ], + + [ + 'ProgConfigItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['ruleSet', ruleSetToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["ProgConfigItemDelegateV2"]', ), ], ]) as beet.FixableBeet; From 1fcb16537cfddd32d9e17f81ae6f92357b039244 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:29:39 -0700 Subject: [PATCH 30/40] Make delegate_record, token, and edition readonly Make consistent in instruction builder and shank annotation. --- .../program/src/instruction/metadata.rs | 18 +++--------------- token-metadata/program/src/instruction/mod.rs | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 21d9aa981c..3620b1b8f6 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -985,23 +985,11 @@ impl InstructionBuilder for super::builders::Update { fn instruction(&self) -> solana_program::instruction::Instruction { let mut accounts = vec![ AccountMeta::new_readonly(self.authority, true), - if let Some(record) = self.delegate_record { - AccountMeta::new(record, false) - } else { - AccountMeta::new_readonly(crate::ID, false) - }, - if let Some(token) = self.token { - AccountMeta::new(token, false) - } else { - AccountMeta::new_readonly(crate::ID, false) - }, + AccountMeta::new_readonly(self.delegate_record.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(self.token.unwrap_or(crate::ID), false), AccountMeta::new_readonly(self.mint, false), AccountMeta::new(self.metadata, false), - if let Some(edition) = self.edition { - AccountMeta::new(edition, false) - } else { - AccountMeta::new_readonly(crate::ID, false) - }, + AccountMeta::new_readonly(self.edition.unwrap_or(crate::ID), false), AccountMeta::new(self.payer, true), AccountMeta::new_readonly(self.system_program, false), AccountMeta::new_readonly(self.sysvar_instructions, false), diff --git a/token-metadata/program/src/instruction/mod.rs b/token-metadata/program/src/instruction/mod.rs index 23e7196619..dec14c4258 100644 --- a/token-metadata/program/src/instruction/mod.rs +++ b/token-metadata/program/src/instruction/mod.rs @@ -725,7 +725,7 @@ pub enum MetadataInstruction { #[account(2, optional, name="token", desc="Token account")] #[account(3, name="mint", desc="Mint account")] #[account(4, writable, name="metadata", desc="Metadata account")] - #[account(5, optional, writable, name="edition", desc="Edition account")] + #[account(5, optional, name="edition", desc="Edition account")] #[account(6, signer, writable, name="payer", desc="Payer")] #[account(7, name="system_program", desc="System program")] #[account(8, name="sysvar_instructions", desc="System program")] From 8f066c00d010dc864e75bf7bcd04693f94d1a5ea Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:53:19 -0700 Subject: [PATCH 31/40] Add some tests for collection-level Data delegate --- token-metadata/program/tests/update.rs | 156 ++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 2dcdd695b2..238bdd4835 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -320,6 +320,7 @@ mod update { .await .unwrap(); + // Check initial data and update authority. let metadata = da.get_metadata(context).await; assert_eq!( metadata.data.name, @@ -344,7 +345,7 @@ mod update { .unwrap() .unwrap(); - // Change some values that this delegate is allowed to change. + // Change some data. let new_name = puffed_out_string("New Name", MAX_NAME_LENGTH); let new_symbol = puffed_out_string("NEW", MAX_SYMBOL_LENGTH); let new_uri = puffed_out_string("https://new.digital.asset.org", MAX_URI_LENGTH); @@ -389,7 +390,7 @@ mod update { context.banks_client.process_transaction(tx).await.unwrap(); - // checks the created metadata values + // Check the updated data. let metadata = da.get_metadata(context).await; assert_eq!(metadata.data.name, new_name); @@ -1069,6 +1070,23 @@ mod update { #[tokio::test] async fn fail_update_collection_by_collections_programmable_config_delegate() { + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_delegate(delegate_args).await + } + + #[tokio::test] + async fn fail_update_collection_by_collections_data_delegate() { + let delegate_args = DelegateArgs::DataV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_delegate(delegate_args).await + } + + async fn fail_update_collection_by_collections_delegate(delegate_args: DelegateArgs) { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -1090,9 +1108,6 @@ mod update { // Create metadata delegate on the collection. let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); - let delegate_args = DelegateArgs::ProgrammableConfigV1 { - authorization_data: None, - }; let delegate_record = collection_parent_da .delegate(context, update_authority, delegate.pubkey(), delegate_args) .await @@ -1283,6 +1298,137 @@ mod update { assert_eq!(metadata.programmable_config, None); } + #[tokio::test] + async fn success_update_data_by_collections_data_delegate() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::DataV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = DigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + // Check collection. + assert_eq!(metadata.collection, collection); + + // Check data and update authority. + let metadata = da.get_metadata(context).await; + assert_eq!( + metadata.data.name, + puffed_out_string(DEFAULT_NAME, MAX_NAME_LENGTH) + ); + assert_eq!( + metadata.data.symbol, + puffed_out_string(DEFAULT_SYMBOL, MAX_SYMBOL_LENGTH) + ); + assert_eq!( + metadata.data.uri, + puffed_out_string(DEFAULT_URI, MAX_URI_LENGTH) + ); + assert_eq!(metadata.update_authority, context.payer.pubkey()); + + // Change some data. + let new_name = puffed_out_string("New Name", MAX_NAME_LENGTH); + let new_symbol = puffed_out_string("NEW", MAX_SYMBOL_LENGTH); + let new_uri = puffed_out_string("https://new.digital.asset.org", MAX_URI_LENGTH); + let data = Data { + name: new_name.clone(), + symbol: new_symbol.clone(), + uri: new_uri.clone(), + creators: metadata.data.creators, // keep the same creators + seller_fee_basis_points: 0, + }; + + let mut args = UpdateArgs::default_data_delegate(); + match &mut args { + UpdateArgs::DataDelegateV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // Check the updated data. + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.data.name, new_name); + assert_eq!(metadata.data.symbol, new_symbol); + assert_eq!(metadata.data.uri, new_uri); + } + #[tokio::test] async fn fail_update_prog_config_by_col_prog_config_delegate_wrong_v1_args() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); From 5241144957e13df5a19b72981d1162c87325c1f1 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 25 Apr 2023 12:00:59 -0700 Subject: [PATCH 32/40] Add collection delegate update authority mismatch test --- token-metadata/program/tests/update.rs | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 238bdd4835..03ffbdf02c 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -1068,6 +1068,145 @@ mod update { assert_eq!(metadata.collection, None); } + #[tokio::test] + async fn fail_update_collection_delegate_update_authority_mismatch() { + let context = &mut program_test().start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Change the collection to have a different update authority. + let new_collection_update_authority = Keypair::new(); + new_collection_update_authority + .airdrop(context, 1_000_000_000) + .await + .unwrap(); + + let mut args = UpdateArgs::default_update_authority(); + match &mut args { + UpdateArgs::UpdateAuthorityV2 { + new_update_authority, + .. + } => *new_update_authority = Some(new_collection_update_authority.pubkey()), + _ => panic!("Unexpected enum variant"), + } + + let payer = context.payer.dirty_clone(); + collection_parent_da + .update(context, payer, args) + .await + .unwrap(); + + // Verify update authority is changed. + let metadata = collection_parent_da.get_metadata(context).await; + assert_eq!( + metadata.update_authority, + new_collection_update_authority.pubkey() + ); + + // Verify cannot create metadata delegate on the collection using the old update authority. + let old_update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let fail_delegate = Keypair::new(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let err = collection_parent_da + .delegate( + context, + old_update_authority, + fail_delegate.pubkey(), + delegate_args, + ) + .await + .unwrap_err(); + + assert_custom_error_ix!(1, err, MetadataError::UpdateAuthorityIncorrect); + + // Create metadata delegate on the collection using the new update authority. + let pass_delegate = Keypair::new(); + pass_delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let pass_delegate_record = collection_parent_da + .delegate( + context, + new_collection_update_authority, + pass_delegate.pubkey(), + delegate_args, + ) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + let mut args = UpdateArgs::default_collection_delegate(); + match &mut args { + UpdateArgs::CollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(pass_delegate.pubkey()) + .delegate_record(pass_delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(pass_delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&pass_delegate.pubkey()), + &[&pass_delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + #[tokio::test] async fn fail_update_collection_by_collections_programmable_config_delegate() { let delegate_args = DelegateArgs::ProgrammableConfigV1 { From 0cc7129a1426edd6736516b317f9b89242a6b74c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 25 Apr 2023 17:04:30 -0700 Subject: [PATCH 33/40] Rename UpdateArgs to indicate updating as a role --- .../program/src/instruction/metadata.rs | 64 +++++----- .../program/src/processor/metadata/update.rs | 14 +-- token-metadata/program/tests/unverify.rs | 8 +- token-metadata/program/tests/update.rs | 117 +++++++++--------- 4 files changed, 103 insertions(+), 100 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 3620b1b8f6..ad56e59fd6 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -100,7 +100,7 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - UpdateAuthorityV2 { + AsUpdateAuthorityV2 { /// The new update authority. new_update_authority: Option, /// The metadata details. @@ -124,7 +124,7 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - AuthorityItemDelegateV2 { + AsAuthorityItemDelegateV2 { /// The new update authority. new_update_authority: Option, /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be @@ -138,37 +138,37 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - CollectionDelegateV2 { + AsCollectionDelegateV2 { /// Collection information. collection: CollectionToggle, /// Required authorization data to validate the request. authorization_data: Option, }, - DataDelegateV2 { + AsDataDelegateV2 { /// The metadata details. data: Option, /// Required authorization data to validate the request. authorization_data: Option, }, - ProgConfigDelegateV2 { + AsProgConfigDelegateV2 { // Programmable rule set configuration (only applicable to `Programmable` asset types). rule_set: RuleSetToggle, /// Required authorization data to validate the request. authorization_data: Option, }, - DataItemDelegateV2 { + AsDataItemDelegateV2 { /// The metadata details. data: Option, /// Required authorization data to validate the request. authorization_data: Option, }, - CollectionItemDelegateV2 { + AsCollectionItemDelegateV2 { /// Collection information. collection: CollectionToggle, /// Required authorization data to validate the request. authorization_data: Option, }, - ProgConfigItemDelegateV2 { + AsProgrammableConfigItemDelegateV2 { // Programmable rule set configuration (only applicable to `Programmable` asset types). rule_set: RuleSetToggle, /// Required authorization data to validate the request. @@ -191,8 +191,8 @@ impl UpdateArgs { } } - pub fn default_update_authority() -> Self { - Self::UpdateAuthorityV2 { + pub fn default_as_update_authority() -> Self { + Self::AsUpdateAuthorityV2 { new_update_authority: None, data: None, primary_sale_happened: None, @@ -206,8 +206,8 @@ impl UpdateArgs { } } - pub fn default_authority_item_delegate() -> Self { - Self::AuthorityItemDelegateV2 { + pub fn default_as_authority_item_delegate() -> Self { + Self::AsAuthorityItemDelegateV2 { new_update_authority: None, primary_sale_happened: None, is_mutable: None, @@ -216,43 +216,43 @@ impl UpdateArgs { } } - pub fn default_collection_delegate() -> Self { - Self::CollectionDelegateV2 { + pub fn default_as_collection_delegate() -> Self { + Self::AsCollectionDelegateV2 { collection: CollectionToggle::default(), authorization_data: None, } } - pub fn default_data_delegate() -> Self { - Self::DataDelegateV2 { + pub fn default_as_data_delegate() -> Self { + Self::AsDataDelegateV2 { data: None, authorization_data: None, } } - pub fn default_prog_config_delegate() -> Self { - Self::ProgConfigDelegateV2 { + pub fn default_as_programmable_config_delegate() -> Self { + Self::AsProgConfigDelegateV2 { rule_set: RuleSetToggle::default(), authorization_data: None, } } - pub fn default_data_item_delegate() -> Self { - Self::DataItemDelegateV2 { + pub fn default_as_data_item_delegate() -> Self { + Self::AsDataItemDelegateV2 { data: None, authorization_data: None, } } - pub fn default_collection_item_delegate() -> Self { - Self::CollectionItemDelegateV2 { + pub fn default_as_collection_item_delegate() -> Self { + Self::AsCollectionItemDelegateV2 { collection: CollectionToggle::default(), authorization_data: None, } } - pub fn default_prog_config_item_delegate() -> Self { - Self::ProgConfigItemDelegateV2 { + pub fn default_as_programmable_config_item_delegate() -> Self { + Self::AsProgrammableConfigItemDelegateV2 { rule_set: RuleSetToggle::default(), authorization_data: None, } @@ -322,7 +322,7 @@ impl From for InternalUpdateArgs { rule_set, ..Default::default() }, - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { new_update_authority, data, primary_sale_happened, @@ -344,7 +344,7 @@ impl From for InternalUpdateArgs { rule_set, token_standard, }, - UpdateArgs::AuthorityItemDelegateV2 { + UpdateArgs::AsAuthorityItemDelegateV2 { new_update_authority, primary_sale_happened, is_mutable, @@ -357,18 +357,18 @@ impl From for InternalUpdateArgs { token_standard, ..Default::default() }, - UpdateArgs::CollectionDelegateV2 { collection, .. } - | UpdateArgs::CollectionItemDelegateV2 { collection, .. } => Self { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } + | UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => Self { collection, ..Default::default() }, - UpdateArgs::DataDelegateV2 { data, .. } - | UpdateArgs::DataItemDelegateV2 { data, .. } => Self { + UpdateArgs::AsDataDelegateV2 { data, .. } + | UpdateArgs::AsDataItemDelegateV2 { data, .. } => Self { data, ..Default::default() }, - UpdateArgs::ProgConfigDelegateV2 { rule_set, .. } - | UpdateArgs::ProgConfigItemDelegateV2 { rule_set, .. } => Self { + UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } + | UpdateArgs::AsProgrammableConfigItemDelegateV2 { rule_set, .. } => Self { rule_set, ..Default::default() }, diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 04c27478a8..9d6091bf65 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -260,23 +260,23 @@ fn validate_update( if let Some(metadata_delegate_role) = metadata_delegate_role { match metadata_delegate_role { MetadataDelegateRole::AuthorityItem => match args { - UpdateArgs::AuthorityItemDelegateV2 { .. } => (), + UpdateArgs::AsAuthorityItemDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::Data => match args { - UpdateArgs::DataDelegateV2 { .. } => (), + UpdateArgs::AsDataDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::DataItem => match args { - UpdateArgs::DataItemDelegateV2 { .. } => (), + UpdateArgs::AsDataItemDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::Collection => match args { - UpdateArgs::CollectionDelegateV2 { .. } => (), + UpdateArgs::AsCollectionDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::CollectionItem => match args { - UpdateArgs::CollectionItemDelegateV2 { .. } => (), + UpdateArgs::AsCollectionItemDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::ProgrammableConfig => match args { @@ -304,11 +304,11 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - UpdateArgs::ProgConfigDelegateV2 { .. } => (), + UpdateArgs::AsProgConfigDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, MetadataDelegateRole::ProgrammableConfigItem => match args { - UpdateArgs::ProgConfigItemDelegateV2 { .. } => (), + UpdateArgs::AsProgrammableConfigItemDelegateV2 { .. } => (), _ => return Err(MetadataError::InvalidUpdateArgs.into()), }, _ => return Err(MetadataError::InvalidAuthorityType.into()), diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index f551661a82..8c602c8fe7 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -1908,9 +1908,9 @@ mod unverify_collection { .await .unwrap(); - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { new_update_authority, .. } => *new_update_authority = Some(new_collection_update_authority.pubkey()), @@ -1984,9 +1984,9 @@ mod unverify_collection { // Change the collection to have a different update authority. let new_collection_update_authority = Keypair::new(); - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { new_update_authority, .. } => *new_update_authority = Some(new_collection_update_authority.pubkey()), diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 03ffbdf02c..6497efb78e 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -64,9 +64,9 @@ mod update { seller_fee_basis_points: 0, }; - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { data: current_data, .. } => *current_data = Some(data), _ => panic!("Unexpected enum variant"), @@ -135,10 +135,10 @@ mod update { .unwrap(); // Change a few values that this delegate is allowed to change. - let mut args = UpdateArgs::default_authority_item_delegate(); + let mut args = UpdateArgs::default_as_authority_item_delegate(); match &mut args { - UpdateArgs::AuthorityItemDelegateV2 { + UpdateArgs::AsAuthorityItemDelegateV2 { new_update_authority, primary_sale_happened, is_mutable, @@ -193,9 +193,9 @@ mod update { key: Keypair::new().pubkey(), }; - let mut update_args = UpdateArgs::default_collection_delegate(); + let mut update_args = UpdateArgs::default_as_collection_delegate(); match &mut update_args { - UpdateArgs::CollectionDelegateV2 { collection, .. } => { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -216,9 +216,9 @@ mod update { key: Keypair::new().pubkey(), }; - let mut update_args = UpdateArgs::default_collection_item_delegate(); + let mut update_args = UpdateArgs::default_as_collection_item_delegate(); match &mut update_args { - UpdateArgs::CollectionItemDelegateV2 { collection, .. } => { + UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -290,8 +290,11 @@ mod update { authorization_data: None, }; - success_update_data_by_items_delegate(delegate_args, UpdateArgs::default_data_delegate()) - .await; + success_update_data_by_items_delegate( + delegate_args, + UpdateArgs::default_as_data_delegate(), + ) + .await; } #[tokio::test] @@ -302,7 +305,7 @@ mod update { success_update_data_by_items_delegate( delegate_args, - UpdateArgs::default_data_item_delegate(), + UpdateArgs::default_as_data_item_delegate(), ) .await; } @@ -358,10 +361,10 @@ mod update { }; match &mut update_args { - UpdateArgs::DataDelegateV2 { + UpdateArgs::AsDataDelegateV2 { data: current_data, .. } => *current_data = Some(data), - UpdateArgs::DataItemDelegateV2 { + UpdateArgs::AsDataItemDelegateV2 { data: current_data, .. } => *current_data = Some(data), _ => panic!("Unexpected enum variant"), @@ -435,9 +438,9 @@ mod update { } // remove the rule set - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, _ => panic!("Unexpected enum variant"), } @@ -477,9 +480,9 @@ mod update { authorization_data: None, }; - let mut update_args = UpdateArgs::default_prog_config_delegate(); + let mut update_args = UpdateArgs::default_as_programmable_config_delegate(); match &mut update_args { - UpdateArgs::ProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, _ => panic!("Unexpected enum variant"), } @@ -492,9 +495,9 @@ mod update { authorization_data: None, }; - let mut update_args = UpdateArgs::default_prog_config_item_delegate(); + let mut update_args = UpdateArgs::default_as_programmable_config_item_delegate(); match &mut update_args { - UpdateArgs::ProgConfigItemDelegateV2 { rule_set, .. } => { + UpdateArgs::AsProgrammableConfigItemDelegateV2 { rule_set, .. } => { *rule_set = RuleSetToggle::Clear } _ => panic!("Unexpected enum variant"), @@ -678,9 +681,9 @@ mod update { .unwrap(); // Use update args variant that no delegates are allowed to use. - let update_args = UpdateArgs::default_update_authority(); + let update_args = UpdateArgs::default_as_update_authority(); match update_args { - UpdateArgs::UpdateAuthorityV2 { .. } => (), + UpdateArgs::AsUpdateAuthorityV2 { .. } => (), _ => panic!("Unexpected enum variant"), } @@ -749,9 +752,9 @@ mod update { .unwrap(); // Use update args variant that no delegates are allowed to use. - let update_args = UpdateArgs::default_update_authority(); + let update_args = UpdateArgs::default_as_update_authority(); match update_args { - UpdateArgs::UpdateAuthorityV2 { .. } => (), + UpdateArgs::AsUpdateAuthorityV2 { .. } => (), _ => panic!("Unexpected enum variant"), } @@ -802,9 +805,9 @@ mod update { assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); // Update token standard - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { token_standard, .. } => { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } => { *token_standard = Some(TokenStandard::Fungible) } _ => panic!("Unexpected enum variant"), @@ -834,9 +837,9 @@ mod update { assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); // Update token standard - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { token_standard, .. } => { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } => { *token_standard = Some(TokenStandard::NonFungible) } _ => panic!("Unexpected enum variant"), @@ -874,9 +877,9 @@ mod update { key: Keypair::new().pubkey(), }; - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { collection, .. } => { + UpdateArgs::AsUpdateAuthorityV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -943,9 +946,9 @@ mod update { verified: false, }; - let mut args = UpdateArgs::default_collection_delegate(); + let mut args = UpdateArgs::default_as_collection_delegate(); match &mut args { - UpdateArgs::CollectionDelegateV2 { collection, .. } => { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -1026,9 +1029,9 @@ mod update { verified: false, }; - let mut args = UpdateArgs::default_collection_item_delegate(); + let mut args = UpdateArgs::default_as_collection_item_delegate(); match &mut args { - UpdateArgs::CollectionItemDelegateV2 { collection, .. } => { + UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -1093,9 +1096,9 @@ mod update { .await .unwrap(); - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { new_update_authority, .. } => *new_update_authority = Some(new_collection_update_authority.pubkey()), @@ -1165,9 +1168,9 @@ mod update { verified: false, }; - let mut args = UpdateArgs::default_collection_delegate(); + let mut args = UpdateArgs::default_as_collection_delegate(); match &mut args { - UpdateArgs::CollectionDelegateV2 { collection, .. } => { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -1268,9 +1271,9 @@ mod update { verified: false, }; - let mut args = UpdateArgs::default_collection_delegate(); + let mut args = UpdateArgs::default_as_collection_delegate(); match &mut args { - UpdateArgs::CollectionDelegateV2 { collection, .. } => { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { *collection = CollectionToggle::Set(new_collection.clone()) } _ => panic!("Unexpected enum variant"), @@ -1313,9 +1316,9 @@ mod update { #[tokio::test] async fn success_update_prog_config_by_collections_prog_config_delegate_v2_args() { // Change programmable config, removing the RuleSet. - let mut args = UpdateArgs::default_prog_config_delegate(); + let mut args = UpdateArgs::default_as_programmable_config_delegate(); match &mut args { - UpdateArgs::ProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, _ => panic!("Unexpected enum variant"), } @@ -1527,9 +1530,9 @@ mod update { seller_fee_basis_points: 0, }; - let mut args = UpdateArgs::default_data_delegate(); + let mut args = UpdateArgs::default_as_data_delegate(); match &mut args { - UpdateArgs::DataDelegateV2 { + UpdateArgs::AsDataDelegateV2 { data: current_data, .. } => *current_data = Some(data), _ => panic!("Unexpected enum variant"), @@ -1833,9 +1836,9 @@ mod update { panic!("Missing rule set programmable config"); } - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => { *rule_set = RuleSetToggle::Set(invalid_rule_set) } _ => panic!("Unexpected enum variant"), @@ -1909,9 +1912,9 @@ mod update { assert_custom_error!(err, MetadataError::InvalidAuthorizationRules); // Finally, try to update with the valid rule set, and it should succeed. - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => { *rule_set = RuleSetToggle::Set(authorization_rules) } _ => panic!("Unexpected enum variant"), @@ -2010,9 +2013,9 @@ mod update { .unwrap(); // Try to clear the rule set. - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, _ => panic!("Unexpected enum variant"), } @@ -2047,9 +2050,9 @@ mod update { assert_custom_error!(err, MetadataError::CannotUpdateAssetWithDelegate); // Try to update the rule set. - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { rule_set, .. } => { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => { *rule_set = RuleSetToggle::Set(new_auth_rules) } _ => panic!("Unexpected enum variant"), @@ -2127,9 +2130,9 @@ mod update { seller_fee_basis_points: 0, }; - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { data: current_data, .. } => *current_data = Some(data), _ => panic!("Unexpected enum variant"), @@ -2187,9 +2190,9 @@ mod update { seller_fee_basis_points: metadata.data.seller_fee_basis_points, }; - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { data: current_data, .. } => *current_data = Some(data), _ => panic!("Unexpected enum variant"), @@ -2213,9 +2216,9 @@ mod update { seller_fee_basis_points: metadata.data.seller_fee_basis_points, }; - let mut args = UpdateArgs::default_update_authority(); + let mut args = UpdateArgs::default_as_update_authority(); match &mut args { - UpdateArgs::UpdateAuthorityV2 { + UpdateArgs::AsUpdateAuthorityV2 { data: current_data, .. } => *current_data = Some(data), _ => panic!("Unexpected enum variant"), From be50b1eb43d8fbad7d9b156f5098c2f1e49ba619 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 25 Apr 2023 17:05:21 -0700 Subject: [PATCH 34/40] Regenerate IDL and SDK --- token-metadata/js/idl/mpl_token_metadata.json | 18 +-- .../js/src/generated/instructions/Update.ts | 4 +- .../js/src/generated/types/UpdateArgs.ts | 105 +++++++++--------- 3 files changed, 65 insertions(+), 62 deletions(-) diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index b5a8fd1d8f..b8e85ffde6 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -3332,7 +3332,7 @@ }, { "name": "edition", - "isMut": true, + "isMut": false, "isSigner": false, "desc": "Edition account", "optional": true @@ -5049,7 +5049,7 @@ ] }, { - "name": "UpdateAuthorityV2", + "name": "AsUpdateAuthorityV2", "fields": [ { "name": "new_update_authority", @@ -5120,7 +5120,7 @@ ] }, { - "name": "AuthorityItemDelegateV2", + "name": "AsAuthorityItemDelegateV2", "fields": [ { "name": "new_update_authority", @@ -5159,7 +5159,7 @@ ] }, { - "name": "CollectionDelegateV2", + "name": "AsCollectionDelegateV2", "fields": [ { "name": "collection", @@ -5178,7 +5178,7 @@ ] }, { - "name": "DataDelegateV2", + "name": "AsDataDelegateV2", "fields": [ { "name": "data", @@ -5199,7 +5199,7 @@ ] }, { - "name": "ProgConfigDelegateV2", + "name": "AsProgConfigDelegateV2", "fields": [ { "name": "rule_set", @@ -5218,7 +5218,7 @@ ] }, { - "name": "DataItemDelegateV2", + "name": "AsDataItemDelegateV2", "fields": [ { "name": "data", @@ -5239,7 +5239,7 @@ ] }, { - "name": "CollectionItemDelegateV2", + "name": "AsCollectionItemDelegateV2", "fields": [ { "name": "collection", @@ -5258,7 +5258,7 @@ ] }, { - "name": "ProgConfigItemDelegateV2", + "name": "AsProgrammableConfigItemDelegateV2", "fields": [ { "name": "rule_set", diff --git a/token-metadata/js/src/generated/instructions/Update.ts b/token-metadata/js/src/generated/instructions/Update.ts index 0a01bef877..568103ff5a 100644 --- a/token-metadata/js/src/generated/instructions/Update.ts +++ b/token-metadata/js/src/generated/instructions/Update.ts @@ -41,7 +41,7 @@ export const UpdateStruct = new beet.FixableBeetArgsStruct< * @property [] token (optional) Token account * @property [] mint Mint account * @property [_writable_] metadata Metadata account - * @property [_writable_] edition (optional) Edition account + * @property [] edition (optional) Edition account * @property [_writable_, **signer**] payer Payer * @property [] sysvarInstructions System program * @property [] authorizationRulesProgram (optional) Token Authorization Rules Program @@ -116,7 +116,7 @@ export function createUpdateInstruction( }, { pubkey: accounts.edition ?? programId, - isWritable: accounts.edition != null, + isWritable: false, isSigner: false, }, { diff --git a/token-metadata/js/src/generated/types/UpdateArgs.ts b/token-metadata/js/src/generated/types/UpdateArgs.ts index 4849892b20..309458acf1 100644 --- a/token-metadata/js/src/generated/types/UpdateArgs.ts +++ b/token-metadata/js/src/generated/types/UpdateArgs.ts @@ -36,7 +36,7 @@ export type UpdateArgsRecord = { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; - UpdateAuthorityV2: { + AsUpdateAuthorityV2: { newUpdateAuthority: beet.COption; data: beet.COption; primarySaleHappened: beet.COption; @@ -48,31 +48,34 @@ export type UpdateArgsRecord = { tokenStandard: beet.COption; authorizationData: beet.COption; }; - AuthorityItemDelegateV2: { + AsAuthorityItemDelegateV2: { newUpdateAuthority: beet.COption; primarySaleHappened: beet.COption; isMutable: beet.COption; tokenStandard: beet.COption; authorizationData: beet.COption; }; - CollectionDelegateV2: { + AsCollectionDelegateV2: { collection: CollectionToggle; authorizationData: beet.COption; }; - DataDelegateV2: { data: beet.COption; authorizationData: beet.COption }; - ProgConfigDelegateV2: { + AsDataDelegateV2: { + data: beet.COption; + authorizationData: beet.COption; + }; + AsProgConfigDelegateV2: { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; - DataItemDelegateV2: { + AsDataItemDelegateV2: { data: beet.COption; authorizationData: beet.COption; }; - CollectionItemDelegateV2: { + AsCollectionItemDelegateV2: { collection: CollectionToggle; authorizationData: beet.COption; }; - ProgConfigItemDelegateV2: { + AsProgrammableConfigItemDelegateV2: { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; @@ -93,33 +96,33 @@ export type UpdateArgs = beet.DataEnumKeyAsKind; export const isUpdateArgsV1 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V1' } => x.__kind === 'V1'; -export const isUpdateArgsUpdateAuthorityV2 = ( +export const isUpdateArgsAsUpdateAuthorityV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'UpdateAuthorityV2' } => x.__kind === 'UpdateAuthorityV2'; -export const isUpdateArgsAuthorityItemDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsUpdateAuthorityV2' } => x.__kind === 'AsUpdateAuthorityV2'; +export const isUpdateArgsAsAuthorityItemDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'AuthorityItemDelegateV2' } => - x.__kind === 'AuthorityItemDelegateV2'; -export const isUpdateArgsCollectionDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsAuthorityItemDelegateV2' } => + x.__kind === 'AsAuthorityItemDelegateV2'; +export const isUpdateArgsAsCollectionDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'CollectionDelegateV2' } => x.__kind === 'CollectionDelegateV2'; -export const isUpdateArgsDataDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsCollectionDelegateV2' } => x.__kind === 'AsCollectionDelegateV2'; +export const isUpdateArgsAsDataDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'DataDelegateV2' } => x.__kind === 'DataDelegateV2'; -export const isUpdateArgsProgConfigDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsDataDelegateV2' } => x.__kind === 'AsDataDelegateV2'; +export const isUpdateArgsAsProgConfigDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'ProgConfigDelegateV2' } => x.__kind === 'ProgConfigDelegateV2'; -export const isUpdateArgsDataItemDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsProgConfigDelegateV2' } => x.__kind === 'AsProgConfigDelegateV2'; +export const isUpdateArgsAsDataItemDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'DataItemDelegateV2' } => x.__kind === 'DataItemDelegateV2'; -export const isUpdateArgsCollectionItemDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsDataItemDelegateV2' } => x.__kind === 'AsDataItemDelegateV2'; +export const isUpdateArgsAsCollectionItemDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'CollectionItemDelegateV2' } => - x.__kind === 'CollectionItemDelegateV2'; -export const isUpdateArgsProgConfigItemDelegateV2 = ( +): x is UpdateArgs & { __kind: 'AsCollectionItemDelegateV2' } => + x.__kind === 'AsCollectionItemDelegateV2'; +export const isUpdateArgsAsProgrammableConfigItemDelegateV2 = ( x: UpdateArgs, -): x is UpdateArgs & { __kind: 'ProgConfigItemDelegateV2' } => - x.__kind === 'ProgConfigItemDelegateV2'; +): x is UpdateArgs & { __kind: 'AsProgrammableConfigItemDelegateV2' } => + x.__kind === 'AsProgrammableConfigItemDelegateV2'; /** * @category userTypes @@ -145,8 +148,8 @@ export const updateArgsBeet = beet.dataEnum([ ], [ - 'UpdateAuthorityV2', - new beet.FixableBeetArgsStruct( + 'AsUpdateAuthorityV2', + new beet.FixableBeetArgsStruct( [ ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], ['data', beet.coption(dataBeet)], @@ -159,13 +162,13 @@ export const updateArgsBeet = beet.dataEnum([ ['tokenStandard', beet.coption(tokenStandardBeet)], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["UpdateAuthorityV2"]', + 'UpdateArgsRecord["AsUpdateAuthorityV2"]', ), ], [ - 'AuthorityItemDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsAuthorityItemDelegateV2', + new beet.FixableBeetArgsStruct( [ ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], ['primarySaleHappened', beet.coption(beet.bool)], @@ -173,73 +176,73 @@ export const updateArgsBeet = beet.dataEnum([ ['tokenStandard', beet.coption(tokenStandardBeet)], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["AuthorityItemDelegateV2"]', + 'UpdateArgsRecord["AsAuthorityItemDelegateV2"]', ), ], [ - 'CollectionDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsCollectionDelegateV2', + new beet.FixableBeetArgsStruct( [ ['collection', collectionToggleBeet], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["CollectionDelegateV2"]', + 'UpdateArgsRecord["AsCollectionDelegateV2"]', ), ], [ - 'DataDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsDataDelegateV2', + new beet.FixableBeetArgsStruct( [ ['data', beet.coption(dataBeet)], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["DataDelegateV2"]', + 'UpdateArgsRecord["AsDataDelegateV2"]', ), ], [ - 'ProgConfigDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsProgConfigDelegateV2', + new beet.FixableBeetArgsStruct( [ ['ruleSet', ruleSetToggleBeet], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["ProgConfigDelegateV2"]', + 'UpdateArgsRecord["AsProgConfigDelegateV2"]', ), ], [ - 'DataItemDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsDataItemDelegateV2', + new beet.FixableBeetArgsStruct( [ ['data', beet.coption(dataBeet)], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["DataItemDelegateV2"]', + 'UpdateArgsRecord["AsDataItemDelegateV2"]', ), ], [ - 'CollectionItemDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsCollectionItemDelegateV2', + new beet.FixableBeetArgsStruct( [ ['collection', collectionToggleBeet], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["CollectionItemDelegateV2"]', + 'UpdateArgsRecord["AsCollectionItemDelegateV2"]', ), ], [ - 'ProgConfigItemDelegateV2', - new beet.FixableBeetArgsStruct( + 'AsProgrammableConfigItemDelegateV2', + new beet.FixableBeetArgsStruct( [ ['ruleSet', ruleSetToggleBeet], ['authorizationData', beet.coption(authorizationDataBeet)], ], - 'UpdateArgsRecord["ProgConfigItemDelegateV2"]', + 'UpdateArgsRecord["AsProgrammableConfigItemDelegateV2"]', ), ], ]) as beet.FixableBeet; From b28444512314862999678c188c84dfae1278bd0c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Wed, 26 Apr 2023 18:29:53 -0700 Subject: [PATCH 35/40] Space savings * Remove InternalUpdateArgs struct and `From` trait implementation. * Deconstruct `UpdateArgs` where used. * Remove redundant authority type and metadata delegate role checks from `Metadata::update_v1` because `validate_update` already checks that the authority is allowed to perform an update, and the `UpdateArgs` enforces that the authority is only changing fields for which it is authorized. --- .../program/src/instruction/metadata.rs | 117 --------- .../program/src/processor/metadata/update.rs | 40 +-- token-metadata/program/src/state/metadata.rs | 227 ++++++++++-------- 3 files changed, 146 insertions(+), 238 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index ad56e59fd6..0e471e44d4 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -259,123 +259,6 @@ impl UpdateArgs { } } -pub(crate) struct InternalUpdateArgs { - /// The new update authority. - pub new_update_authority: Option, - /// The metadata details. - pub data: Option, - /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be - /// changed back). - pub primary_sale_happened: Option, - // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be - /// changed back). - pub is_mutable: Option, - /// Collection information. - pub collection: CollectionToggle, - /// Additional details of the collection. - pub collection_details: CollectionDetailsToggle, - /// Uses information. - pub uses: UsesToggle, - // Programmable rule set configuration (only applicable to `Programmable` asset types). - pub rule_set: RuleSetToggle, - /// Token standard. - pub token_standard: Option, -} - -impl Default for InternalUpdateArgs { - fn default() -> Self { - Self { - new_update_authority: None, - data: None, - primary_sale_happened: None, - is_mutable: None, - collection: CollectionToggle::None, - collection_details: CollectionDetailsToggle::None, - uses: UsesToggle::None, - rule_set: RuleSetToggle::None, - token_standard: None, - } - } -} - -impl From for InternalUpdateArgs { - fn from(args: UpdateArgs) -> Self { - match args { - UpdateArgs::V1 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } => Self { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - ..Default::default() - }, - UpdateArgs::AsUpdateAuthorityV2 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - token_standard, - .. - } => Self { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - token_standard, - }, - UpdateArgs::AsAuthorityItemDelegateV2 { - new_update_authority, - primary_sale_happened, - is_mutable, - token_standard, - .. - } => Self { - new_update_authority, - primary_sale_happened, - is_mutable, - token_standard, - ..Default::default() - }, - UpdateArgs::AsCollectionDelegateV2 { collection, .. } - | UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => Self { - collection, - ..Default::default() - }, - UpdateArgs::AsDataDelegateV2 { data, .. } - | UpdateArgs::AsDataItemDelegateV2 { data, .. } => Self { - data, - ..Default::default() - }, - UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } - | UpdateArgs::AsProgrammableConfigItemDelegateV2 { rule_set, .. } => Self { - rule_set, - ..Default::default() - }, - } - } -} - //-- Toggle implementations #[repr(C)] diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 9d6091bf65..df560e443f 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -10,9 +10,7 @@ use spl_token::state::Account; use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, - instruction::{ - CollectionToggle, Context, InternalUpdateArgs, MetadataDelegateRole, Update, UpdateArgs, - }, + instruction::{CollectionToggle, Context, MetadataDelegateRole, Update, UpdateArgs}, pda::{EDITION, PREFIX}, state::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, @@ -125,23 +123,24 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro (None, None) }; - // Copy the args into a struct that contains all available fields. - let internal_args = InternalUpdateArgs::from(args.clone()); - - // there is a special case for collection-level delegates, where the - // validation should use the collection key as the mint parameter + // There is a special case for collection-level delegates, where the + // validation should use the collection key as the mint parameter. let existing_collection_mint = metadata .collection .as_ref() .map(|Collection { key, .. }| key); - // Check if caller passed in a collection and if so use that. Note that if the - // delegate role from `get_authority_type` comes back as something other than - // `MetadataDelegateRole::Collection` or `MetadataDelegateRole::CollectionItem`, - // then it will fail in `validate_update` because those are the only roles that - // can change collection. - let collection_mint = match &internal_args.collection { - CollectionToggle::Set(Collection { key, .. }) => Some(key), + // Check if caller passed in a collection and if so use that. Note that + // `validate_update` checks that the authority has permission to pass in + // a new collection value. + let collection_mint = match &args { + UpdateArgs::V1 { collection, .. } + | UpdateArgs::AsUpdateAuthorityV2 { collection, .. } + | UpdateArgs::AsCollectionDelegateV2 { collection, .. } + | UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => match collection { + CollectionToggle::Set(Collection { key, .. }) => Some(key), + _ => existing_collection_mint, + }, _ => existing_collection_mint, }; @@ -185,6 +184,13 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // Validate that authority has permission to use the update args that were provided. validate_update(&args, &authority_type, metadata_delegate_role)?; + // See if caller passed in a desired token standard. + let desired_token_standard = match args { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } + | UpdateArgs::AsAuthorityItemDelegateV2 { token_standard, .. } => token_standard, + _ => None, + }; + // Find existing token standard from metadata or infer it. let existing_or_inferred_token_std = if let Some(token_standard) = metadata.token_standard { token_standard @@ -194,7 +200,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // If there is a desired token standard, use it if it passes the check. If there is not a // desired token standard, use the existing or inferred token standard. - let token_standard = match internal_args.token_standard { + let token_standard = match desired_token_standard { Some(desired_token_standard) => { check_desired_token_standard(existing_or_inferred_token_std, desired_token_standard)?; desired_token_standard @@ -223,8 +229,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ctx.accounts.metadata_info, token, token_standard, - authority_type, - metadata_delegate_role, )?; Ok(()) diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index a81c46a574..edcc0dfc82 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -4,10 +4,7 @@ use crate::{ collection::assert_collection_update_is_valid, metadata::assert_data_valid, uses::assert_valid_use, }, - instruction::{ - CollectionDetailsToggle, CollectionToggle, InternalUpdateArgs, MetadataDelegateRole, - RuleSetToggle, UpdateArgs, - }, + instruction::{CollectionDetailsToggle, CollectionToggle, RuleSetToggle, UpdateArgs}, utils::{clean_write_metadata, puff_out_data_fields}, }; @@ -103,97 +100,121 @@ impl Metadata { metadata: &AccountInfo<'a>, token: Option, token_standard: TokenStandard, - authority_type: AuthorityType, - delegate_role: Option, ) -> ProgramResult { - // Convert the args into a struct that contains all available fields. - let args = InternalUpdateArgs::from(args); - // Update the token standard if it is changed. self.token_standard = Some(token_standard); - // Update the Update Authority only sections. - if matches!(authority_type, AuthorityType::Metadata) { - if args.uses.is_some() { - let uses_option = args.uses.to_option(); - // If already None leave it as None. - assert_valid_use(&uses_option, &self.uses)?; - self.uses = uses_option; + // Only the Update Authority can update this section. + match &args { + UpdateArgs::V1 { + uses, + collection_details, + .. } - - if let CollectionDetailsToggle::Set(collection_details) = args.collection_details { - // only unsized collections can have the size set, and only once. - if self.collection_details.is_some() { - return Err(MetadataError::SizedCollection.into()); + | UpdateArgs::AsUpdateAuthorityV2 { + uses, + collection_details, + .. + } => { + if uses.is_some() { + let uses_option = uses.clone().to_option(); + // If already None leave it as None. + assert_valid_use(&uses_option, &self.uses)?; + self.uses = uses_option; } - self.collection_details = Some(collection_details); + if let CollectionDetailsToggle::Set(collection_details) = collection_details { + // only unsized collections can have the size set, and only once. + if self.collection_details.is_some() { + return Err(MetadataError::SizedCollection.into()); + } + + self.collection_details = Some(collection_details.clone()); + } } + _ => (), } - // Update Authority sections. - if matches!(authority_type, AuthorityType::Metadata) - || matches!(delegate_role, Some(MetadataDelegateRole::AuthorityItem)) - { - if let Some(authority) = args.new_update_authority { - self.update_authority = authority; + // Update Authority or Authority Item Delegate can update this section. + match &args { + UpdateArgs::V1 { + new_update_authority, + primary_sale_happened, + is_mutable, + .. + } + | UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + .. } + | UpdateArgs::AsAuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + .. + } => { + if let Some(authority) = new_update_authority { + self.update_authority = *authority; + } - if let Some(primary_sale) = args.primary_sale_happened { - // If received primary_sale is true, flip to true. - if primary_sale || !self.primary_sale_happened { - self.primary_sale_happened = primary_sale - } else { - return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into()); + if let Some(primary_sale) = primary_sale_happened { + // If received primary_sale is true, flip to true. + if *primary_sale || !self.primary_sale_happened { + self.primary_sale_happened = *primary_sale + } else { + return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into()); + } } - } - if let Some(mutable) = args.is_mutable { - // If received value is false, flip to false. - if !mutable || self.is_mutable { - self.is_mutable = mutable - } else { - return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); + if let Some(mutable) = is_mutable { + // If received value is false, flip to false. + if !mutable || self.is_mutable { + self.is_mutable = *mutable + } else { + return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); + } } } + _ => (), } - // Update Data section. - if matches!(authority_type, AuthorityType::Metadata) - || matches!( - delegate_role, - Some(MetadataDelegateRole::Data | MetadataDelegateRole::DataItem) - ) - { - if let Some(data) = args.data { - if !self.is_mutable { - return Err(MetadataError::DataIsImmutable.into()); - } + // Update Authority or Data Delegates can update this section. + match &args { + UpdateArgs::V1 { data, .. } + | UpdateArgs::AsUpdateAuthorityV2 { data, .. } + | UpdateArgs::AsDataDelegateV2 { data, .. } + | UpdateArgs::AsDataItemDelegateV2 { data, .. } => { + if let Some(data) = data { + if !self.is_mutable { + return Err(MetadataError::DataIsImmutable.into()); + } - assert_data_valid( - &data, - update_authority.key, - self, - false, - update_authority.is_signer, - )?; - self.data = data; + assert_data_valid( + data, + update_authority.key, + self, + false, + update_authority.is_signer, + )?; + self.data = data.clone(); + } } + _ => (), } - // Update Collection section. - if matches!(authority_type, AuthorityType::Metadata) - || matches!( - delegate_role, - Some(MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem) - ) - { - // if the Collection data is 'Set', only allow updating if it is unverified - // or if it exactly matches the existing collection info; if the Collection data - // is 'Clear', then only set to 'None' if it is unverified. - match args.collection { + // Update Authority or Collection Delegates can update this section. + match &args { + UpdateArgs::V1 { collection, .. } + | UpdateArgs::AsUpdateAuthorityV2 { collection, .. } + | UpdateArgs::AsCollectionDelegateV2 { collection, .. } + | UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => match collection { + // if the Collection data is 'Set', only allow updating if it is unverified + // or if it exactly matches the existing collection info; if the Collection data + // is 'Clear', then only set to 'None' if it is unverified. CollectionToggle::Set(_) => { - let collection_option = args.collection.to_option(); + let collection_option = collection.clone().to_option(); assert_collection_update_is_valid(false, &self.collection, &collection_option)?; self.collection = collection_option; } @@ -208,42 +229,42 @@ impl Metadata { } } CollectionToggle::None => { /* nothing to do */ } - } - } + }, + _ => (), + }; - // Update Programmable Config section. - if matches!(authority_type, AuthorityType::Metadata) - || matches!( - delegate_role, - Some( - MetadataDelegateRole::ProgrammableConfig - | MetadataDelegateRole::ProgrammableConfigItem - ) - ) - { - // if the rule_set data is either 'Set' or 'Clear', only allow updating if the - // token standard is equal to `ProgrammableNonFungible` and no SPL delegate is set. - if matches!(args.rule_set, RuleSetToggle::Clear | RuleSetToggle::Set(_)) { - if token_standard != TokenStandard::ProgrammableNonFungible { - return Err(MetadataError::InvalidTokenStandard.into()); - } + // Update Authority or Programmable Config Delegates can update this section. + match &args { + UpdateArgs::V1 { rule_set, .. } + | UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } + | UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } + | UpdateArgs::AsProgrammableConfigItemDelegateV2 { rule_set, .. } => { + // if the rule_set data is either 'Set' or 'Clear', only allow updating if the + // token standard is equal to `ProgrammableNonFungible` and no SPL delegate is set. + if matches!(rule_set, RuleSetToggle::Clear | RuleSetToggle::Set(_)) { + if token_standard != TokenStandard::ProgrammableNonFungible { + return Err(MetadataError::InvalidTokenStandard.into()); + } - // Require the token so we can check if it has a token delegate. - let token = token.ok_or(MetadataError::MissingTokenAccount)?; + // Require the token so we can check if it has a token delegate. + let token = token.ok_or(MetadataError::MissingTokenAccount)?; - // If the token has a delegate, we cannot update the rule set. - if token.delegate.is_some() { - return Err(MetadataError::CannotUpdateAssetWithDelegate.into()); - } + // If the token has a delegate, we cannot update the rule set. + if token.delegate.is_some() { + return Err(MetadataError::CannotUpdateAssetWithDelegate.into()); + } - self.programmable_config = - args.rule_set - .to_option() - .map(|rule_set| ProgrammableConfig::V1 { - rule_set: Some(rule_set), - }); + self.programmable_config = + rule_set + .clone() + .to_option() + .map(|rule_set| ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }); + } } - } + _ => (), + }; // Re-serialize metadata. puff_out_data_fields(self); From 540c09389ea25916d6c225036e29beb967b7b9e5 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 27 Apr 2023 02:07:38 -0700 Subject: [PATCH 36/40] Allow passing in current token standard Also make match statements in validate_update more concise. --- .../program/src/processor/metadata/update.rs | 117 ++++++++---------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index df560e443f..c54791b182 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -10,7 +10,10 @@ use spl_token::state::Account; use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, - instruction::{CollectionToggle, Context, MetadataDelegateRole, Update, UpdateArgs}, + instruction::{ + CollectionDetailsToggle, CollectionToggle, Context, MetadataDelegateRole, Update, + UpdateArgs, UsesToggle, + }, pda::{EDITION, PREFIX}, state::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, @@ -109,16 +112,20 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // Validate relationships let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; - // Mint must match metadata mint + // Metadata mint must match mint account key. if metadata.mint != *ctx.accounts.mint_info.key { return Err(MetadataError::MintMismatch.into()); } let (token_pubkey, token) = if let Some(token_info) = ctx.accounts.token_info { - ( - Some(token_info.key), - Some(Account::unpack(&token_info.try_borrow_data()?)?), - ) + let token = Account::unpack(&token_info.try_borrow_data()?)?; + + // Token mint must match mint account key. + if token.mint != *ctx.accounts.mint_info.key { + return Err(MetadataError::MintMismatch.into()); + } + + (Some(token_info.key), Some(token)) } else { (None, None) }; @@ -262,60 +269,45 @@ fn validate_update( // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to if let Some(metadata_delegate_role) = metadata_delegate_role { - match metadata_delegate_role { - MetadataDelegateRole::AuthorityItem => match args { - UpdateArgs::AsAuthorityItemDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - MetadataDelegateRole::Data => match args { - UpdateArgs::AsDataDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - MetadataDelegateRole::DataItem => match args { - UpdateArgs::AsDataItemDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - MetadataDelegateRole::Collection => match args { - UpdateArgs::AsCollectionDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - MetadataDelegateRole::CollectionItem => match args { - UpdateArgs::AsCollectionItemDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - MetadataDelegateRole::ProgrammableConfig => match args { + let valid_delegate_update = match (metadata_delegate_role, args) { + (MetadataDelegateRole::AuthorityItem, UpdateArgs::AsAuthorityItemDelegateV2 { .. }) => { + true + } + (MetadataDelegateRole::Data, UpdateArgs::AsDataDelegateV2 { .. }) => true, + (MetadataDelegateRole::DataItem, UpdateArgs::AsDataItemDelegateV2 { .. }) => true, + (MetadataDelegateRole::Collection, UpdateArgs::AsCollectionDelegateV2 { .. }) => true, + ( + MetadataDelegateRole::CollectionItem, + UpdateArgs::AsCollectionItemDelegateV2 { .. }, + ) => true, + ( // V1 supported Programmable config, leaving here for backwards // compatibility. + MetadataDelegateRole::ProgrammableConfig, UpdateArgs::V1 { - data, - primary_sale_happened, - is_mutable, - collection, - uses, - new_update_authority, - collection_details, + new_update_authority: None, + data: None, + primary_sale_happened: None, + is_mutable: None, + collection: CollectionToggle::None, + collection_details: CollectionDetailsToggle::None, + uses: UsesToggle::None, .. - } => { - // can only update the programmable config - if data.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection.is_some() - || uses.is_some() - || new_update_authority.is_some() - || collection_details.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); - } - } - UpdateArgs::AsProgConfigDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - MetadataDelegateRole::ProgrammableConfigItem => match args { - UpdateArgs::AsProgrammableConfigItemDelegateV2 { .. } => (), - _ => return Err(MetadataError::InvalidUpdateArgs.into()), - }, - _ => return Err(MetadataError::InvalidAuthorityType.into()), + }, + ) => true, + ( + MetadataDelegateRole::ProgrammableConfig, + UpdateArgs::AsProgConfigDelegateV2 { .. }, + ) => true, + ( + MetadataDelegateRole::ProgrammableConfigItem, + UpdateArgs::AsProgrammableConfigItemDelegateV2 { .. }, + ) => true, + _ => false, + }; + + if !valid_delegate_update { + return Err(MetadataError::InvalidUpdateArgs.into()); } } @@ -326,13 +318,12 @@ fn check_desired_token_standard( existing_or_inferred_token_std: TokenStandard, desired_token_standard: TokenStandard, ) -> ProgramResult { - // This function only allows switching between Fungible and FungibleAsset. Mint decimals must - // be zero. - match existing_or_inferred_token_std { - TokenStandard::Fungible | TokenStandard::FungibleAsset => match desired_token_standard { - TokenStandard::Fungible | TokenStandard::FungibleAsset => Ok(()), - _ => Err(MetadataError::InvalidTokenStandard.into()), - }, + match (existing_or_inferred_token_std, desired_token_standard) { + ( + TokenStandard::Fungible | TokenStandard::FungibleAsset, + TokenStandard::Fungible | TokenStandard::FungibleAsset, + ) => Ok(()), + (existing, desired) if existing == desired => Ok(()), _ => Err(MetadataError::InvalidTokenStandard.into()), } } From 8cf70af4e8ca9e83c41188d758902385cae70be9 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 27 Apr 2023 12:02:25 -0700 Subject: [PATCH 37/40] Clarify comments and reorder account validations --- token-metadata/program/src/instruction/mod.rs | 2 +- .../program/src/processor/metadata/update.rs | 69 +++++++++++-------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/token-metadata/program/src/instruction/mod.rs b/token-metadata/program/src/instruction/mod.rs index dec14c4258..ccdd5917dc 100644 --- a/token-metadata/program/src/instruction/mod.rs +++ b/token-metadata/program/src/instruction/mod.rs @@ -728,7 +728,7 @@ pub enum MetadataInstruction { #[account(5, optional, name="edition", desc="Edition account")] #[account(6, signer, writable, name="payer", desc="Payer")] #[account(7, name="system_program", desc="System program")] - #[account(8, name="sysvar_instructions", desc="System program")] + #[account(8, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(9, optional, name="authorization_rules_program", desc="Token Authorization Rules Program")] #[account(10, optional, name="authorization_rules", desc="Token Authorization Rules account")] #[default_optional_accounts] diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index c54791b182..36c4506240 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -50,42 +50,34 @@ pub fn update<'a>( } fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> ProgramResult { - //** Account Validation **/ // Assert signers // This account should always be a signer regardless of the authority type, // because at least one signer is required to update the metadata. assert_signer(ctx.accounts.authority_info)?; - // Note that payer is not checked because it is not used. + assert_signer(ctx.accounts.payer_info)?; // Assert program ownership - assert_owned_by(ctx.accounts.metadata_info, program_id)?; + if let Some(delegate_record_info) = ctx.accounts.delegate_record_info { + assert_owned_by(delegate_record_info, &crate::ID)?; + } + + if let Some(token_info) = ctx.accounts.token_info { + assert_owned_by(token_info, &spl_token::ID)?; + } + assert_owned_by(ctx.accounts.mint_info, &spl_token::ID)?; + assert_owned_by(ctx.accounts.metadata_info, program_id)?; if let Some(edition) = ctx.accounts.edition_info { assert_owned_by(edition, program_id)?; - // checks that we got the correct edition account - assert_derivation( - program_id, - edition, - &[ - PREFIX.as_bytes(), - program_id.as_ref(), - ctx.accounts.mint_info.key.as_ref(), - EDITION.as_bytes(), - ], - )?; } - // token owner - if let Some(holder_token_account) = ctx.accounts.token_info { - assert_owned_by(holder_token_account, &spl_token::ID)?; - } - // delegate - if let Some(delegate_record_info) = ctx.accounts.delegate_record_info { - assert_owned_by(delegate_record_info, &crate::ID)?; - } + // Note that we do NOT check the ownership of authorization rules account here as this allows + // `Update` to be used to correct a previously invalid `RuleSet`. In practice the ownership of + // authorization rules is checked by the Auth Rules program each time the program is invoked to + // validate rules. // Check program IDs @@ -97,13 +89,13 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } // If the current rule set is passed in, also require the mpl-token-auth-rules program - // to be passed in. Note that we do NOT check the ownership of authorization rules - // here as this allows `Update` to be used to correct a previously invalid `RuleSet`. + // to be passed in. if ctx.accounts.authorization_rules_info.is_some() { let authorization_rules_program = ctx .accounts .authorization_rules_program_info .ok_or(MetadataError::MissingAuthorizationRulesProgram)?; + if authorization_rules_program.key != &mpl_token_auth_rules::ID { return Err(ProgramError::IncorrectProgramId); } @@ -111,12 +103,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro // Validate relationships - let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; - // Metadata mint must match mint account key. - if metadata.mint != *ctx.accounts.mint_info.key { - return Err(MetadataError::MintMismatch.into()); - } - + // Token let (token_pubkey, token) = if let Some(token_info) = ctx.accounts.token_info { let token = Account::unpack(&token_info.try_borrow_data()?)?; @@ -130,6 +117,28 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro (None, None) }; + // Metadata + let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; + // Metadata mint must match mint account key. + if metadata.mint != *ctx.accounts.mint_info.key { + return Err(MetadataError::MintMismatch.into()); + } + + // Edition + if let Some(edition) = ctx.accounts.edition_info { + // checks that we got the correct edition account + assert_derivation( + program_id, + edition, + &[ + PREFIX.as_bytes(), + program_id.as_ref(), + ctx.accounts.mint_info.key.as_ref(), + EDITION.as_bytes(), + ], + )?; + } + // There is a special case for collection-level delegates, where the // validation should use the collection key as the mint parameter. let existing_collection_mint = metadata From 08ff9d8d765a26dd3cf309ea288f406e5899a6f6 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 27 Apr 2023 12:31:33 -0700 Subject: [PATCH 38/40] Minor comment update --- token-metadata/program/src/processor/metadata/update.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 36c4506240..1364bc6ed2 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -52,7 +52,7 @@ pub fn update<'a>( fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> ProgramResult { // Assert signers - // This account should always be a signer regardless of the authority type, + // Authority should always be a signer regardless of the authority type, // because at least one signer is required to update the metadata. assert_signer(ctx.accounts.authority_info)?; assert_signer(ctx.accounts.payer_info)?; @@ -89,7 +89,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } // If the current rule set is passed in, also require the mpl-token-auth-rules program - // to be passed in. + // to be passed in (and check its program ID). if ctx.accounts.authorization_rules_info.is_some() { let authorization_rules_program = ctx .accounts @@ -139,6 +139,8 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro )?; } + // Check authority. + // There is a special case for collection-level delegates, where the // validation should use the collection key as the mint parameter. let existing_collection_mint = metadata From 27bf163e6e314c272a6cac4706c53b79fd0a128b Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 27 Apr 2023 12:32:32 -0700 Subject: [PATCH 39/40] Minor description change to generated IDL/SDK --- token-metadata/js/idl/mpl_token_metadata.json | 2 +- token-metadata/js/src/generated/instructions/Update.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index b8e85ffde6..6512e18f8a 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -3353,7 +3353,7 @@ "name": "sysvarInstructions", "isMut": false, "isSigner": false, - "desc": "System program" + "desc": "Instructions sysvar account" }, { "name": "authorizationRulesProgram", diff --git a/token-metadata/js/src/generated/instructions/Update.ts b/token-metadata/js/src/generated/instructions/Update.ts index 568103ff5a..0bd5759368 100644 --- a/token-metadata/js/src/generated/instructions/Update.ts +++ b/token-metadata/js/src/generated/instructions/Update.ts @@ -43,7 +43,7 @@ export const UpdateStruct = new beet.FixableBeetArgsStruct< * @property [_writable_] metadata Metadata account * @property [] edition (optional) Edition account * @property [_writable_, **signer**] payer Payer - * @property [] sysvarInstructions System program + * @property [] sysvarInstructions Instructions sysvar account * @property [] authorizationRulesProgram (optional) Token Authorization Rules Program * @property [] authorizationRules (optional) Token Authorization Rules account * @category Instructions From e586ea05e3d20b32f36f892ecf87988dba079c93 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 27 Apr 2023 15:51:24 -0700 Subject: [PATCH 40/40] Add more BPF tests * Holder authority test. * Pass with same token standard being passed in. * Token doesn't match mint test. * Metadata doesn't match mint test. * Wrong edition test. * Some failure cases around collection-level delegates. --- token-metadata/program/tests/update.rs | 520 ++++++++++++++++++++++++- 1 file changed, 517 insertions(+), 3 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 6497efb78e..02dcfe0fd1 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -5,7 +5,7 @@ use mpl_token_metadata::{ error::MetadataError, instruction::{ builders::UpdateBuilder, CollectionToggle, DelegateArgs, InstructionBuilder, RuleSetToggle, - UpdateArgs, + TransferArgs, UpdateArgs, }, state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard}, state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, @@ -103,7 +103,7 @@ mod update { } #[tokio::test] - async fn success_update_by_authority_item_delegate() { + async fn success_update_by_items_authority_item_delegate() { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -474,6 +474,306 @@ mod update { assert_eq!(metadata.programmable_config, None); } + #[tokio::test] + async fn fail_update_pfnt_config_token_and_mint_mismatch() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data.clone()), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create second digital asset. + let mut second_da = DigitalAsset::new(); + second_da + .create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Trying to remove the RuleSet from first digital asset. + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + // Send the wrong token. + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(second_da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::MintMismatch); + + // `RuleSet` should not have changed on first asset. + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + } + + #[tokio::test] + async fn fail_update_pfnt_config_metadata_and_mint_mismatch() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data.clone()), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create second digital asset. + let mut second_da = DigitalAsset::new(); + second_da + .create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Trying to remove the RuleSet from first digital asset. + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + // Send the wrong mint and wrong token so that we are checking mint against the metadata. + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(second_da.mint.pubkey()) + .token(second_da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::MintMismatch); + + // `RuleSet` should not have changed onf first asset. + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + } + + #[tokio::test] + async fn fail_update_pfnt_config_by_update_authority_wrong_edition() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data.clone()), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create second digital asset. + let mut second_da = DigitalAsset::new(); + second_da + .create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Trying to remove the RuleSet from first digital asset. + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + // Send the wrong edition. + if let Some(edition) = second_da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::DerivedKeyInvalid); + + // `RuleSet` should not have changed onf first asset. + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + } + #[tokio::test] async fn success_update_pnft_by_items_programmable_config_delegate() { let delegate_args = DelegateArgs::ProgrammableConfigV1 { @@ -585,7 +885,7 @@ mod update { } #[tokio::test] - async fn fail_update_by_items_authority_delegate() { + async fn fail_update_by_items_authority_item_delegate() { let args = DelegateArgs::AuthorityItemV1 { authorization_data: None, }; @@ -789,6 +1089,85 @@ mod update { assert_custom_error!(err, MetadataError::InvalidAuthorityType); } + #[tokio::test] + async fn fail_update_by_holder() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + ) + .await + .unwrap(); + + // Transfer to a new holder. + let holder = Keypair::new(); + holder.airdrop(context, 1_000_000_000).await.unwrap(); + + let args = TransferArgs::V1 { + authorization_data: None, + amount: 1, + }; + + da.transfer(TransferParams { + context, + authority: &update_authority, + source_owner: &update_authority.pubkey(), + destination_owner: holder.pubkey(), + destination_token: None, // fn will create the ATA + payer: &update_authority, + authorization_rules: None, + args, + }) + .await + .unwrap(); + + // Attempt to update. There are no `AsHolder` update args available but + // we expect to fail before we get to the point of checking args anyways. + let update_args = UpdateArgs::default_as_update_authority(); + match update_args { + UpdateArgs::AsUpdateAuthorityV2 { .. } => (), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(holder.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .payer(holder.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&holder.pubkey()), + &[&holder], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::FeatureNotSupported); + } + #[tokio::test] async fn success_update_token_standard() { let context = &mut program_test().start_with_context().await; @@ -821,6 +1200,38 @@ mod update { assert_eq!(metadata.token_standard, Some(TokenStandard::Fungible)); } + #[tokio::test] + async fn success_update_token_standard_to_same() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::NonFungible)); + + // Update token standard + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } => { + *token_standard = Some(TokenStandard::NonFungible) + } + _ => panic!("Unexpected enum variant"), + } + + da.update(context, update_authority.dirty_clone(), args) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::NonFungible)); + } + #[tokio::test] async fn fail_invalid_update_token_standard() { let context = &mut program_test().start_with_context().await; @@ -1313,6 +1724,109 @@ mod update { assert_eq!(metadata.collection, None); } + #[tokio::test] + async fn fail_update_collection_by_collections_programmable_config_item_delegate() { + let delegate_args = DelegateArgs::ProgrammableConfigItemV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_item_delegate(delegate_args).await + } + + #[tokio::test] + async fn fail_update_collection_by_collections_data_item_delegate() { + let delegate_args = DelegateArgs::DataItemV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_item_delegate(delegate_args).await + } + + async fn fail_update_collection_by_collections_item_delegate(delegate_args: DelegateArgs) { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + let mut args = UpdateArgs::default_as_collection_delegate(); + match &mut args { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + #[tokio::test] async fn success_update_prog_config_by_collections_prog_config_delegate_v2_args() { // Change programmable config, removing the RuleSet.