Skip to content

Commit

Permalink
Use token metadata client crate (#60)
Browse files Browse the repository at this point in the history
* Use token metadata client crate

* Update instructions and types

* Update token metadata crate version

* Bump dependency version
  • Loading branch information
febo authored Nov 29, 2023
1 parent 1f5efaf commit cea5f00
Show file tree
Hide file tree
Showing 15 changed files with 452 additions and 564 deletions.
227 changes: 100 additions & 127 deletions programs/bubblegum/Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions programs/bubblegum/program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ default = []
anchor-lang = { version = "0.28.0", features = ["init-if-needed"] }
anchor-spl = "0.28.0"
bytemuck = "1.13.0"
mpl-token-metadata = { version = "2.0.0-beta.1", features = ["no-entrypoint"] }
mpl-token-metadata = "3.2.3"
num-traits = "0.2.15"
solana-program = "~1.16.5"
solana-program = "~1.16.8"
spl-account-compression = { version="0.2.0", features = ["cpi"] }
spl-associated-token-account = { version = ">= 1.1.3, < 3.0", features = ["no-entrypoint"] }
spl-token = { version = ">= 3.5.0, < 5.0", features = ["no-entrypoint"] }
Expand Down
55 changes: 46 additions & 9 deletions programs/bubblegum/program/src/asserts.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
use crate::{error::BubblegumError, state::metaplex_adapter::MetadataArgs, utils::cmp_pubkeys};
use anchor_lang::prelude::*;
use mpl_token_metadata::{
instruction::MetadataDelegateRole,
pda::{find_collection_authority_account, find_metadata_delegate_record_account},
state::{CollectionAuthorityRecord, Metadata, MetadataDelegateRecord, TokenMetadataAccount},
accounts::{CollectionAuthorityRecord, Metadata, MetadataDelegateRecord},
types::{Collection, MetadataDelegateRole, TokenStandard},
};

/// Assert that the provided MetadataArgs are compatible with MPL `Data`
pub fn assert_metadata_is_mpl_compatible(metadata: &MetadataArgs) -> Result<()> {
if metadata.name.len() > mpl_token_metadata::state::MAX_NAME_LENGTH {
if metadata.name.len() > mpl_token_metadata::MAX_NAME_LENGTH {
return Err(BubblegumError::MetadataNameTooLong.into());
}

if metadata.symbol.len() > mpl_token_metadata::state::MAX_SYMBOL_LENGTH {
if metadata.symbol.len() > mpl_token_metadata::MAX_SYMBOL_LENGTH {
return Err(BubblegumError::MetadataSymbolTooLong.into());
}

if metadata.uri.len() > mpl_token_metadata::state::MAX_URI_LENGTH {
if metadata.uri.len() > mpl_token_metadata::MAX_URI_LENGTH {
return Err(BubblegumError::MetadataUriTooLong.into());
}

if metadata.seller_fee_basis_points > 10000 {
return Err(BubblegumError::MetadataBasisPointsTooHigh.into());
}
if !metadata.creators.is_empty() {
if metadata.creators.len() > mpl_token_metadata::state::MAX_CREATOR_LIMIT {
if metadata.creators.len() > mpl_token_metadata::MAX_CREATOR_LIMIT {
return Err(BubblegumError::CreatorsTooLong.into());
}

Expand Down Expand Up @@ -106,8 +105,8 @@ pub fn assert_has_collection_authority(
}

if let Some(record_info) = delegate_record {
let (ca_pda, ca_bump) = find_collection_authority_account(mint, collection_authority);
let (md_pda, md_bump) = find_metadata_delegate_record_account(
let (ca_pda, ca_bump) = CollectionAuthorityRecord::find_pda(mint, collection_authority);
let (md_pda, md_bump) = MetadataDelegateRecord::find_pda(
mint,
MetadataDelegateRole::Collection,
&collection_data.update_authority,
Expand Down Expand Up @@ -150,3 +149,41 @@ pub fn assert_has_collection_authority(
}
Ok(())
}

pub fn assert_collection_membership(
membership: &Option<Collection>,
collection_metadata: &Metadata,
collection_mint: &Pubkey,
collection_edition: &AccountInfo,
) -> Result<()> {
match membership {
Some(collection) => {
if collection.key != *collection_mint || collection_metadata.mint != *collection_mint {
return Err(BubblegumError::CollectionNotFound.into());
}
}
None => {
return Err(BubblegumError::CollectionNotFound.into());
}
}

let (expected, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(collection_mint);

if collection_edition.key != &expected {
return Err(BubblegumError::CollectionMasterEditionAccountInvalid.into());
}

let edition = mpl_token_metadata::accounts::MasterEdition::try_from(collection_edition)
.map_err(|_err| BubblegumError::CollectionMustBeAUniqueMasterEdition)?;

match collection_metadata.token_standard {
Some(TokenStandard::NonFungible) | Some(TokenStandard::ProgrammableNonFungible) => (),
_ => return Err(BubblegumError::CollectionMustBeAUniqueMasterEdition.into()),
}

if edition.max_supply != Some(0) {
return Err(BubblegumError::CollectionMustBeAUniqueMasterEdition.into());
}

Ok(())
}
8 changes: 4 additions & 4 deletions programs/bubblegum/program/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anchor_lang::prelude::*;
use mpl_token_metadata::error::MetadataError;
use mpl_token_metadata::errors::MplTokenMetadataError;
use num_traits::FromPrimitive;

#[error_code]
Expand Down Expand Up @@ -82,12 +82,12 @@ pub fn metadata_error_into_bubblegum(error: ProgramError) -> BubblegumError {
FromPrimitive::from_u32(e).expect("Unknown error code from token-metadata");

match metadata_error {
MetadataError::CollectionNotFound => BubblegumError::CollectionNotFound,
MetadataError::CollectionMustBeAUniqueMasterEdition => {
MplTokenMetadataError::CollectionNotFound => BubblegumError::CollectionNotFound,
MplTokenMetadataError::CollectionMustBeAUniqueMasterEdition => {
BubblegumError::CollectionMustBeAUniqueMasterEdition
}

MetadataError::CollectionMasterEditionAccountInvalid => {
MplTokenMetadataError::CollectionMasterEditionAccountInvalid => {
BubblegumError::CollectionMasterEditionAccountInvalid
}

Expand Down
111 changes: 44 additions & 67 deletions programs/bubblegum/program/src/processor/decompress.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use anchor_lang::prelude::*;
use anchor_spl::{associated_token::AssociatedToken, token::Token};
use mpl_token_metadata::{
instructions::{CreateMasterEditionV3CpiBuilder, CreateMetadataAccountV3CpiBuilder},
types::DataV2,
};
use solana_program::{
program::{invoke, invoke_signed},
program_pack::Pack,
Expand Down Expand Up @@ -71,12 +75,14 @@ pub struct DecompressV1<'info> {
}

pub(crate) fn decompress_v1(ctx: Context<DecompressV1>, metadata: MetadataArgs) -> Result<()> {
// Allocate and create mint
let incoming_data_hash = hash_metadata(&metadata)?;
// Validate the incoming metadata

match ctx.accounts.voucher.leaf_schema {
LeafSchema::V1 {
owner, data_hash, ..
} => {
let incoming_data_hash = hash_metadata(&metadata)?;

if !cmp_bytes(&data_hash, &incoming_data_hash, 32) {
return Err(BubblegumError::HashingMismatch.into());
}
Expand Down Expand Up @@ -180,82 +186,53 @@ pub(crate) fn decompress_v1(ctx: Context<DecompressV1>, metadata: MetadataArgs)
]],
)?;

let metadata_infos = vec![
ctx.accounts.metadata.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.leaf_owner.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.sysvar_rent.to_account_info(),
];

let master_edition_infos = vec![
ctx.accounts.master_edition.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.leaf_owner.to_account_info(),
ctx.accounts.metadata.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.sysvar_rent.to_account_info(),
];

msg!("Creating metadata!");
invoke_signed(
&mpl_token_metadata::instruction::create_metadata_accounts_v3(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.metadata.key(),
ctx.accounts.mint.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.leaf_owner.key(),
ctx.accounts.mint_authority.key(),
metadata.name.clone(),
metadata.symbol.clone(),
metadata.uri.clone(),
if !metadata.creators.is_empty() {
Some(metadata.creators.iter().map(|c| c.adapt()).collect())
} else {
msg!("Creating metadata");
CreateMetadataAccountV3CpiBuilder::new(&ctx.accounts.token_metadata_program)
.metadata(&ctx.accounts.metadata)
.mint(&ctx.accounts.mint)
.mint_authority(&ctx.accounts.mint_authority)
.payer(&ctx.accounts.leaf_owner)
.update_authority(&ctx.accounts.mint_authority, true)
.system_program(&ctx.accounts.system_program)
.data(DataV2 {
name: metadata.name.clone(),
symbol: metadata.symbol.clone(),
uri: metadata.uri.clone(),
creators: if metadata.creators.is_empty() {
None
} else {
Some(metadata.creators.iter().map(|c| c.adapt()).collect())
},
metadata.seller_fee_basis_points,
true,
metadata.is_mutable,
metadata.collection.map(|c| c.adapt()),
metadata.uses.map(|u| u.adapt()),
None,
),
metadata_infos.as_slice(),
&[&[
collection: metadata.collection.map(|c| c.adapt()),
seller_fee_basis_points: metadata.seller_fee_basis_points,
uses: metadata.uses.map(|u| u.adapt()),
})
.is_mutable(metadata.is_mutable)
.invoke_signed(&[&[
ctx.accounts.mint.key().as_ref(),
&[ctx.bumps["mint_authority"]],
]],
)?;
]])?;

msg!("Creating master edition!");
invoke_signed(
&mpl_token_metadata::instruction::create_master_edition_v3(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.master_edition.key(),
ctx.accounts.mint.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.metadata.key(),
ctx.accounts.leaf_owner.key(),
Some(0),
),
master_edition_infos.as_slice(),
&[&[
msg!("Creating master edition");
CreateMasterEditionV3CpiBuilder::new(&ctx.accounts.token_metadata_program)
.edition(&ctx.accounts.master_edition)
.mint(&ctx.accounts.mint)
.mint_authority(&ctx.accounts.mint_authority)
.update_authority(&ctx.accounts.mint_authority)
.metadata(&ctx.accounts.metadata)
.payer(&ctx.accounts.leaf_owner)
.system_program(&ctx.accounts.system_program)
.token_program(&ctx.accounts.token_program)
.max_supply(0)
.invoke_signed(&[&[
ctx.accounts.mint.key().as_ref(),
&[ctx.bumps["mint_authority"]],
]],
)?;
]])?;

ctx.accounts
.mint_authority
.to_account_info()
.assign(&System::id());

Ok(())
}
50 changes: 17 additions & 33 deletions programs/bubblegum/program/src/processor/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use anchor_lang::prelude::*;
use mpl_token_metadata::{
assertions::collection::assert_collection_verify_is_valid, state::CollectionDetails,
instructions::BubblegumSetCollectionSizeCpiBuilder,
types::{CollectionDetails, SetCollectionSizeArgs},
};
use solana_program::{account_info::AccountInfo, program::invoke_signed, pubkey::Pubkey};
use solana_program::{account_info::AccountInfo, pubkey::Pubkey};
use spl_account_compression::wrap_application_data_v1;

use crate::{
asserts::assert_has_collection_authority,
error::{metadata_error_into_bubblegum, BubblegumError},
asserts::{assert_collection_membership, assert_has_collection_authority},
error::BubblegumError,
state::{
leaf_schema::LeafSchema,
metaplex_adapter::{self, Creator, MetadataArgs},
Expand Down Expand Up @@ -190,14 +191,12 @@ fn process_collection_verification_mpl_only<'info>(

// If the NFT has collection data, we set it to the correct value after doing some validation.
if let Some(collection) = &mut message.collection {
// Collection verify assert from token-metadata program.
assert_collection_verify_is_valid(
assert_collection_membership(
&Some(collection.adapt()),
collection_metadata,
collection_mint,
collection_mint.key,
edition_account,
)
.map_err(metadata_error_into_bubblegum)?;
)?;

assert_has_collection_authority(
collection_metadata,
Expand Down Expand Up @@ -228,31 +227,16 @@ fn process_collection_verification_mpl_only<'info>(
}
};

// CPI into to token-metadata program to change the collection size.
let mut bubblegum_set_collection_size_infos = vec![
collection_metadata.to_account_info(),
collection_authority.clone(),
collection_mint.clone(),
bubblegum_signer.clone(),
];
// CPI into Token Metadata program to change the collection size.

if let Some(record) = collection_authority_record {
bubblegum_set_collection_size_infos.push(record.clone());
}

invoke_signed(
&mpl_token_metadata::instruction::bubblegum_set_collection_size(
token_metadata_program.key(),
collection_metadata.to_account_info().key(),
collection_authority.key(),
collection_mint.key(),
bubblegum_signer.key(),
collection_authority_record.map(|r| r.key()),
new_size,
),
bubblegum_set_collection_size_infos.as_slice(),
&[&[COLLECTION_CPI_PREFIX.as_bytes(), &[bubblegum_bump]]],
)?;
BubblegumSetCollectionSizeCpiBuilder::new(token_metadata_program)
.collection_metadata(&collection_metadata.to_account_info())
.collection_authority(collection_authority)
.collection_mint(collection_mint)
.bubblegum_signer(bubblegum_signer)
.collection_authority_record(collection_authority_record)
.set_collection_size_args(SetCollectionSizeArgs { size: new_size })
.invoke_signed(&[&[COLLECTION_CPI_PREFIX.as_bytes(), &[bubblegum_bump]]])?;
} else {
return Err(BubblegumError::CollectionMustBeSized.into());
}
Expand Down
18 changes: 9 additions & 9 deletions programs/bubblegum/program/src/state/metaplex_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ pub struct Creator {
}

impl Creator {
pub fn adapt(&self) -> mpl_token_metadata::state::Creator {
mpl_token_metadata::state::Creator {
pub fn adapt(&self) -> mpl_token_metadata::types::Creator {
mpl_token_metadata::types::Creator {
address: self.address,
verified: self.verified,
share: self.share,
Expand Down Expand Up @@ -50,12 +50,12 @@ pub struct Uses {
}

impl Uses {
pub fn adapt(&self) -> mpl_token_metadata::state::Uses {
mpl_token_metadata::state::Uses {
pub fn adapt(&self) -> mpl_token_metadata::types::Uses {
mpl_token_metadata::types::Uses {
use_method: match self.use_method {
UseMethod::Burn => mpl_token_metadata::state::UseMethod::Burn,
UseMethod::Multiple => mpl_token_metadata::state::UseMethod::Multiple,
UseMethod::Single => mpl_token_metadata::state::UseMethod::Single,
UseMethod::Burn => mpl_token_metadata::types::UseMethod::Burn,
UseMethod::Multiple => mpl_token_metadata::types::UseMethod::Multiple,
UseMethod::Single => mpl_token_metadata::types::UseMethod::Single,
},
remaining: self.remaining,
total: self.total,
Expand All @@ -71,8 +71,8 @@ pub struct Collection {
}

impl Collection {
pub fn adapt(&self) -> mpl_token_metadata::state::Collection {
mpl_token_metadata::state::Collection {
pub fn adapt(&self) -> mpl_token_metadata::types::Collection {
mpl_token_metadata::types::Collection {
verified: self.verified,
key: self.key,
}
Expand Down
Loading

0 comments on commit cea5f00

Please sign in to comment.