From 677a49b9914c2ba273b8290aae5eb95be496abf3 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 24 Feb 2023 12:02:39 -0500 Subject: [PATCH 1/3] Rework the metadata args into a wrapper around actual Metadata --- bubblegum/program/src/error.rs | 2 + bubblegum/program/src/lib.rs | 51 ++----- bubblegum/program/src/state/metadata_model.rs | 140 ++++++++++++++++++ .../program/src/state/metaplex_adapter.rs | 86 +++++++++++ bubblegum/program/src/state/mod.rs | 1 + 5 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 bubblegum/program/src/state/metadata_model.rs diff --git a/bubblegum/program/src/error.rs b/bubblegum/program/src/error.rs index 74cc570c98..5ffd53687b 100644 --- a/bubblegum/program/src/error.rs +++ b/bubblegum/program/src/error.rs @@ -56,4 +56,6 @@ pub enum BubblegumError { LeafAuthorityMustSign, #[msg("Collection Not Compatable with Compression, Must be Sized")] CollectionMustBeSized, + #[msg("Token Standard Not Supported")] + TokenStandardNotSupported, } diff --git a/bubblegum/program/src/lib.rs b/bubblegum/program/src/lib.rs index 0defea828b..01416fb1a0 100644 --- a/bubblegum/program/src/lib.rs +++ b/bubblegum/program/src/lib.rs @@ -4,6 +4,7 @@ use crate::{ error::BubblegumError, state::{ leaf_schema::LeafSchema, + metadata_model::MetaplexMetadata, metaplex_adapter::{self, Creator, MetadataArgs, TokenProgramVersion}, metaplex_anchor::{MasterEdition, MplTokenMetadata, TokenMetadata}, TreeConfig, Voucher, ASSET_PREFIX, COLLECTION_CPI_PREFIX, TREE_AUTHORITY_SIZE, @@ -518,47 +519,23 @@ fn process_mint_v1<'info>( } } - // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace - // instructions, without passing the entire, un-hashed MetadataArgs struct - let metadata_args_hash = keccak::hashv(&[message.try_to_vec()?.as_slice()]); - let data_hash = keccak::hashv(&[ - &metadata_args_hash.to_bytes(), - &message.seller_fee_basis_points.to_le_bytes(), - ]); - // Use the metadata auth to check whether we can allow `verified` to be set to true in the // creator Vec. - let creator_data = message - .creators - .iter() - .map(|c| { - if c.verified && !metadata_auth.contains(&c.address) { - Err(BubblegumError::CreatorDidNotVerify.into()) - } else { - Ok([c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) - } - }) - .collect::>>()?; - - // Calculate creator hash. - let creator_hash = keccak::hashv( - creator_data - .iter() - .map(|c| c.as_slice()) - .collect::>() - .as_ref(), - ); + for creator in message.creators.iter() { + if creator.verified && !metadata_auth.contains(&creator.address) { + return Err(BubblegumError::CreatorDidNotVerify.into()); + } + } - let asset_id = get_asset_id(&merkle_tree.key(), authority.num_minted); - let leaf = LeafSchema::new_v0( - asset_id, - owner, - delegate, + let metadata = MetaplexMetadata::from_metadata_args( + &message, + &owner, + &delegate, authority.num_minted, - data_hash.to_bytes(), - creator_hash.to_bytes(), - ); - + &merkle_tree.key(), + &authority.tree_creator, + )?; + let leaf: LeafSchema = metadata.to_leaf_schema_v0()?; wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; append_leaf( diff --git a/bubblegum/program/src/state/metadata_model.rs b/bubblegum/program/src/state/metadata_model.rs new file mode 100644 index 0000000000..f7aca80e23 --- /dev/null +++ b/bubblegum/program/src/state/metadata_model.rs @@ -0,0 +1,140 @@ +use anchor_lang::prelude::*; +use mpl_token_metadata::state::Metadata; +use solana_program::keccak; + +use crate::{error::BubblegumError, utils::get_asset_id}; + +use super::{ + leaf_schema::LeafSchema, + metaplex_adapter::{ + Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard, Uses, + }, +}; + +pub struct MetaplexMetadata { + pub metadata: Metadata, + pub owner: Pubkey, + pub delegate: Pubkey, + pub leaf_index: u64, + pub merkle_tree: Pubkey, +} + +impl MetaplexMetadata { + pub fn to_leaf_schema_v0(&self) -> Result { + let data_hash = self.hash_metadata()?; + let creator_hash = self.hash_creators(); + let asset_id = get_asset_id(&self.merkle_tree, self.leaf_index); + Ok(LeafSchema::new_v0( + asset_id, + self.owner.clone(), + self.delegate.clone(), + self.leaf_index, + data_hash, + creator_hash, + )) + } + + pub fn to_metadata_args(&self) -> Result { + let token_standard: Result> = match self.metadata.token_standard { + Some(mpl_token_metadata::state::TokenStandard::NonFungible) => { + Ok(Some(TokenStandard::NonFungible)) + } + Some(mpl_token_metadata::state::TokenStandard::FungibleAsset) => { + Ok(Some(TokenStandard::FungibleAsset)) + } + Some(mpl_token_metadata::state::TokenStandard::Fungible) => { + Ok(Some(TokenStandard::Fungible)) + } + Some(mpl_token_metadata::state::TokenStandard::NonFungibleEdition) => { + Ok(Some(TokenStandard::NonFungibleEdition)) + } + Some(mpl_token_metadata::state::TokenStandard::ProgrammableNonFungible) => { + Err(BubblegumError::TokenStandardNotSupported.into()) + } + None => Ok(None), + }; + let token_standard = token_standard?; + + Ok(MetadataArgs { + name: self.metadata.data.name.clone(), + symbol: self.metadata.data.symbol.clone(), + uri: self.metadata.data.uri.clone(), + seller_fee_basis_points: self.metadata.data.seller_fee_basis_points, + creators: self + .metadata + .data + .creators + .as_ref() + .map_or(Vec::<_>::new(), |creators| { + creators + .into_iter() + .map(|c| Creator::from(c.clone())) + .collect() + }), + token_standard, + is_mutable: self.metadata.is_mutable, + primary_sale_happened: self.metadata.primary_sale_happened, + uses: self.metadata.uses.as_ref().map(|uses| Uses::from(&uses)), + collection: self + .metadata + .collection + .as_ref() + .map(|c| Collection::from(&c)), + edition_nonce: self.metadata.edition_nonce, + token_program_version: TokenProgramVersion::Original, + }) + } + + pub fn from_metadata_args( + args: &MetadataArgs, + owner: &Pubkey, + delegate: &Pubkey, + leaf_index: u64, + merkle_tree: &Pubkey, + metadata_authority: &Pubkey, + ) -> Result { + let metadata: Metadata = args.clone().to_metadata(metadata_authority)?; + Ok(Self { + metadata, + owner: owner.clone(), + delegate: delegate.clone(), + leaf_index, + merkle_tree: merkle_tree.clone(), + }) + } + + pub fn hash_creators(&self) -> [u8; 32] { + // Convert creator Vec to bytes Vec. + let creator_data = self + .metadata + .data + .creators + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|c| [c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) + .collect::>(); + + // Calculate new creator hash. + keccak::hashv( + creator_data + .iter() + .map(|c| c.as_slice()) + .collect::>() + .as_ref(), + ) + .to_bytes() + } + + pub fn hash_metadata(&self) -> Result<[u8; 32]> { + let metadata: MetadataArgs = self.to_metadata_args()?; + // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace + // instructions, without passing the entire, un-hashed MetadataArgs struct + let metadata_args_hash = keccak::hashv(&[metadata.try_to_vec()?.as_slice()]); + Ok(keccak::hashv(&[ + &metadata_args_hash.to_bytes(), + &metadata.seller_fee_basis_points.to_le_bytes(), + ]) + .to_bytes()) + } +} diff --git a/bubblegum/program/src/state/metaplex_adapter.rs b/bubblegum/program/src/state/metaplex_adapter.rs index 39ff6ee803..ec23318872 100644 --- a/bubblegum/program/src/state/metaplex_adapter.rs +++ b/bubblegum/program/src/state/metaplex_adapter.rs @@ -1,4 +1,7 @@ use anchor_lang::prelude::*; +use mpl_token_metadata::state::{Data, Metadata}; + +use crate::error::BubblegumError; #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, Clone)] pub enum TokenProgramVersion { @@ -22,6 +25,14 @@ impl Creator { share: self.share, } } + + pub fn from(args: mpl_token_metadata::state::Creator) -> Self { + Creator { + address: args.address, + verified: args.verified, + share: args.share, + } + } } #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, Clone)] @@ -59,6 +70,18 @@ impl Uses { total: self.total, } } + + pub fn from(args: &mpl_token_metadata::state::Uses) -> Self { + Uses { + use_method: match args.use_method { + mpl_token_metadata::state::UseMethod::Burn => UseMethod::Burn, + mpl_token_metadata::state::UseMethod::Multiple => UseMethod::Multiple, + mpl_token_metadata::state::UseMethod::Single => UseMethod::Single, + }, + remaining: args.remaining, + total: args.total, + } + } } #[repr(C)] @@ -75,6 +98,13 @@ impl Collection { key: self.key, } } + + pub fn from(args: &mpl_token_metadata::state::Collection) -> Self { + Collection { + verified: args.verified, + key: args.key, + } + } } #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug, Clone)] @@ -102,3 +132,59 @@ pub struct MetadataArgs { pub token_program_version: TokenProgramVersion, pub creators: Vec, } + +impl MetadataArgs { + /// Also performs validation + pub fn to_metadata( + self, + metadata_auth: &Pubkey, + ) -> std::result::Result { + let creators = match self.creators { + creators if creators.is_empty() => None, + creators => Some( + creators + .iter() + .map(|c| c.adapt()) + .collect::>(), + ), + }; + let data = Data { + name: self.name, + symbol: self.symbol, + uri: self.uri, + seller_fee_basis_points: self.seller_fee_basis_points, + creators, + }; + let token_standard = match self.token_standard { + Some(TokenStandard::NonFungible) => { + Ok(Some(mpl_token_metadata::state::TokenStandard::NonFungible)) + } + Some(TokenStandard::FungibleAsset) => Err(BubblegumError::TokenStandardNotSupported), + Some(TokenStandard::Fungible) => Err(BubblegumError::TokenStandardNotSupported), + Some(TokenStandard::NonFungibleEdition) => { + Err(BubblegumError::TokenStandardNotSupported) + } + None => Err(BubblegumError::TokenStandardNotSupported), + }?; + Ok(Metadata { + key: mpl_token_metadata::state::Key::MetadataV1, + update_authority: metadata_auth.clone(), + mint: Pubkey::default(), + data, + primary_sale_happened: self.primary_sale_happened, + is_mutable: self.is_mutable, + edition_nonce: self.edition_nonce, + token_standard, + collection: match self.collection { + Some(c) => Some(c.adapt()), + None => None, + }, + uses: match self.uses { + Some(u) => Some(u.adapt()), + None => None, + }, + collection_details: None, + programmable_config: None, + }) + } +} diff --git a/bubblegum/program/src/state/mod.rs b/bubblegum/program/src/state/mod.rs index 512d086271..68570143fc 100644 --- a/bubblegum/program/src/state/mod.rs +++ b/bubblegum/program/src/state/mod.rs @@ -1,4 +1,5 @@ pub mod leaf_schema; +pub mod metadata_model; pub mod metaplex_adapter; pub mod metaplex_anchor; From 1ea6540bab6d914bc41cb2e555749b139bdc6bc8 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 24 Feb 2023 15:51:56 -0500 Subject: [PATCH 2/3] Fix tests, only non-fungible token standard is supported, bc that's it's only standard that can be decompressed --- bubblegum/program/tests/utils/context.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bubblegum/program/tests/utils/context.rs b/bubblegum/program/tests/utils/context.rs index b437ddcf03..4af113a161 100644 --- a/bubblegum/program/tests/utils/context.rs +++ b/bubblegum/program/tests/utils/context.rs @@ -1,6 +1,8 @@ use std::fmt::Display; -use mpl_bubblegum::state::metaplex_adapter::{Creator, MetadataArgs, TokenProgramVersion}; +use mpl_bubblegum::state::metaplex_adapter::{ + Creator, MetadataArgs, TokenProgramVersion, TokenStandard, +}; use solana_program::pubkey::Pubkey; use solana_program_test::{BanksClient, ProgramTestContext}; use solana_sdk::{ @@ -96,7 +98,7 @@ impl BubblegumTestContext { primary_sale_happened: false, is_mutable: false, edition_nonce: None, - token_standard: None, + token_standard: Some(TokenStandard::NonFungible), token_program_version: TokenProgramVersion::Original, collection: None, uses: None, From 1114ec251c3b3d8a0577de38efb0bc098880dcb2 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 24 Feb 2023 16:26:25 -0500 Subject: [PATCH 3/3] metadata authority for v0 NFTs is tree creator if tree is private, otw frozen --- bubblegum/program/src/lib.rs | 2 +- bubblegum/program/src/state/mod.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bubblegum/program/src/lib.rs b/bubblegum/program/src/lib.rs index 01416fb1a0..5dac050bbc 100644 --- a/bubblegum/program/src/lib.rs +++ b/bubblegum/program/src/lib.rs @@ -533,7 +533,7 @@ fn process_mint_v1<'info>( &delegate, authority.num_minted, &merkle_tree.key(), - &authority.tree_creator, + &authority.get_metadata_auth_for_v0(), )?; let leaf: LeafSchema = metadata.to_leaf_schema_v0()?; wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; diff --git a/bubblegum/program/src/state/mod.rs b/bubblegum/program/src/state/mod.rs index 68570143fc..6b45deff6b 100644 --- a/bubblegum/program/src/state/mod.rs +++ b/bubblegum/program/src/state/mod.rs @@ -32,6 +32,14 @@ impl TreeConfig { let remaining_mints = self.total_mint_capacity.saturating_sub(self.num_minted); requested_capacity <= remaining_mints } + + pub fn get_metadata_auth_for_v0(&self) -> Pubkey { + if !self.is_public { + self.tree_creator.clone() + } else { + Pubkey::default() + } + } } #[account]