Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bubblegum: Refactor MetadataArgs to be wrapper around Metadata #1037

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bubblegum/program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ pub enum BubblegumError {
LeafAuthorityMustSign,
#[msg("Collection Not Compatable with Compression, Must be Sized")]
CollectionMustBeSized,
#[msg("Token Standard Not Supported")]
TokenStandardNotSupported,
}
51 changes: 14 additions & 37 deletions bubblegum/program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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::<Result<Vec<_>>>()?;

// Calculate creator hash.
let creator_hash = keccak::hashv(
creator_data
.iter()
.map(|c| c.as_slice())
.collect::<Vec<&[u8]>>()
.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.get_metadata_auth_for_v0(),
)?;
let leaf: LeafSchema = metadata.to_leaf_schema_v0()?;
wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?;

append_leaf(
Expand Down
140 changes: 140 additions & 0 deletions bubblegum/program/src/state/metadata_model.rs
Original file line number Diff line number Diff line change
@@ -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<LeafSchema> {
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<MetadataArgs> {
let token_standard: Result<Option<TokenStandard>> = 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<Self> {
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::<Vec<_>>();

// Calculate new creator hash.
keccak::hashv(
creator_data
.iter()
.map(|c| c.as_slice())
.collect::<Vec<&[u8]>>()
.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())
}
}
86 changes: 86 additions & 0 deletions bubblegum/program/src/state/metaplex_adapter.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)]
Expand Down Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -102,3 +132,59 @@ pub struct MetadataArgs {
pub token_program_version: TokenProgramVersion,
pub creators: Vec<Creator>,
}

impl MetadataArgs {
/// Also performs validation
pub fn to_metadata(
self,
metadata_auth: &Pubkey,
) -> std::result::Result<Metadata, BubblegumError> {
let creators = match self.creators {
creators if creators.is_empty() => None,
creators => Some(
creators
.iter()
.map(|c| c.adapt())
.collect::<Vec<mpl_token_metadata::state::Creator>>(),
),
};
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,
})
}
}
9 changes: 9 additions & 0 deletions bubblegum/program/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod leaf_schema;
pub mod metadata_model;
pub mod metaplex_adapter;
pub mod metaplex_anchor;

Expand Down Expand Up @@ -31,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]
Expand Down
6 changes: 4 additions & 2 deletions bubblegum/program/tests/utils/context.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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,
Expand Down