#![cfg(feature = "test-bpf")] pub mod utils; use mpl_token_metadata::{ error::MetadataError, id, instruction, state::{ Collection, Creator, DataV2, Key, UseMethod, Uses, MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH, }, utils::puffed_out_string, }; use num_traits::FromPrimitive; use solana_program_test::*; use solana_sdk::{ instruction::InstructionError, signature::{Keypair, Signer}, transaction::{Transaction, TransactionError}, transport::TransportError, }; use utils::*; mod update_metadata_account_v2 { use mpl_token_metadata::pda::find_collection_authority_account; use super::*; #[tokio::test] async fn success_compatible() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); let name = "Test".to_string(); let symbol = "TST".to_string(); let uri = "uri".to_string(); let puffed_symbol = puffed_out_string(&symbol, MAX_SYMBOL_LENGTH); let puffed_uri = puffed_out_string(&uri, MAX_URI_LENGTH); test_metadata .create( &mut context, name, symbol.clone(), uri.clone(), None, 10, true, ) .await .unwrap(); let updated_name = "New Name".to_string(); let puffed_updated_name = puffed_out_string(&updated_name, MAX_NAME_LENGTH); test_metadata .update_v2( &mut context, updated_name, symbol, uri, None, 10, false, Some(Collection { key: test_metadata.pubkey, verified: false, }), Some(Uses { use_method: UseMethod::Multiple, remaining: 5, total: 10, }), ) .await .unwrap(); let metadata = test_metadata.get_data(&mut context).await; assert_eq!(metadata.data.name, puffed_updated_name,); assert_eq!(metadata.data.symbol, puffed_symbol); assert_eq!(metadata.data.uri, puffed_uri); assert_eq!(metadata.data.seller_fee_basis_points, 10); assert_eq!(metadata.data.creators, None); assert!(!metadata.primary_sale_happened); assert!(!metadata.is_mutable); assert_eq!(metadata.mint, test_metadata.mint.pubkey()); assert_eq!(metadata.update_authority, context.payer.pubkey()); assert_eq!(metadata.key, Key::MetadataV1); assert_eq!(metadata.collection.unwrap().key, test_metadata.pubkey); assert_eq!(metadata.uses.unwrap().use_method, UseMethod::Multiple) } #[tokio::test] async fn success() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); let name = "Test".to_string(); let symbol = "TST".to_string(); let uri = "uri".to_string(); let puffed_symbol = puffed_out_string(&symbol, MAX_SYMBOL_LENGTH); let puffed_uri = puffed_out_string(&uri, MAX_URI_LENGTH); test_metadata .create_v2( &mut context, name, symbol.clone(), uri.clone(), None, 10, true, None, None, None, ) .await .unwrap(); let updated_name = "New Name".to_string(); let puffed_updated_name = puffed_out_string(&updated_name, MAX_NAME_LENGTH); test_metadata .update_v2( &mut context, updated_name, symbol, uri, None, 10, false, Some(Collection { verified: false, key: test_metadata.pubkey, }), Some(Uses { use_method: UseMethod::Multiple, remaining: 5, total: 15, }), ) .await .unwrap(); let metadata = test_metadata.get_data(&mut context).await; assert_eq!(metadata.data.name, puffed_updated_name); assert_eq!(metadata.data.symbol, puffed_symbol); assert_eq!(metadata.data.uri, puffed_uri); assert_eq!(metadata.data.seller_fee_basis_points, 10); assert_eq!(metadata.data.creators, None); assert!(!metadata.primary_sale_happened); assert!(!metadata.is_mutable); assert_eq!(metadata.mint, test_metadata.mint.pubkey()); assert_eq!(metadata.update_authority, context.payer.pubkey()); assert_eq!(metadata.key, Key::MetadataV1); assert!(!metadata.collection.unwrap().verified); assert_eq!(metadata.uses.unwrap().total, 15); } #[tokio::test] async fn success_update_metadata_when_collection_is_verified() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); let name = "Test".to_string(); let symbol = "TST".to_string(); let uri = "uri".to_string(); let puffed_symbol = puffed_out_string(&symbol, MAX_SYMBOL_LENGTH); let puffed_uri = puffed_out_string(&uri, MAX_URI_LENGTH); test_metadata .create_v2( &mut context, name, symbol.clone(), uri.clone(), None, 10, true, None, None, None, ) .await .unwrap(); let new_collection_authority = Keypair::new(); let test_collection = Metadata::new(); test_collection .create_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), None, 10, false, None, None, None, ) .await .unwrap(); let collection_master_edition_account = MasterEditionV2::new(&test_collection); collection_master_edition_account .create_v3(&mut context, Some(0)) .await .unwrap(); let update_authority = context.payer.pubkey(); let (record, _) = find_collection_authority_account( &test_collection.mint.pubkey(), &new_collection_authority.pubkey(), ); let ix = mpl_token_metadata::instruction::approve_collection_authority( mpl_token_metadata::id(), record, new_collection_authority.pubkey(), update_authority, context.payer.pubkey(), test_collection.pubkey, test_collection.mint.pubkey(), ); let tx1 = Transaction::new_signed_with_payer( &[ix], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); context.banks_client.process_transaction(tx1).await.unwrap(); test_metadata .set_and_verify_collection( &mut context, test_collection.pubkey, &new_collection_authority, update_authority, test_collection.mint.pubkey(), collection_master_edition_account.pubkey, Some(record), ) .await .unwrap(); let updated_name = "New Name".to_string(); let puffed_updated_name = puffed_out_string(&updated_name, MAX_NAME_LENGTH); let tx2 = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, Some(DataV2 { name: updated_name, symbol: symbol.clone(), uri: uri.clone(), creators: None, seller_fee_basis_points: 10, collection: Some(Collection { key: test_collection.mint.pubkey(), verified: true, }), uses: None, }), None, Some(false), )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); context.banks_client.process_transaction(tx2).await.unwrap(); let metadata = test_metadata.get_data(&mut context).await; assert_eq!(metadata.data.name, puffed_updated_name); assert_eq!(metadata.data.symbol, puffed_symbol); assert_eq!(metadata.data.uri, puffed_uri); assert_eq!(metadata.data.seller_fee_basis_points, 10); assert_eq!(metadata.data.creators, None); assert!(!metadata.primary_sale_happened); assert!(!metadata.is_mutable); assert_eq!(metadata.mint, test_metadata.mint.pubkey()); assert_eq!(metadata.update_authority, context.payer.pubkey()); assert_eq!(metadata.key, Key::MetadataV1); assert_eq!( metadata.collection.unwrap().key, test_collection.mint.pubkey() ); } #[tokio::test] async fn fail_invalid_update_authority() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); let fake_update_authority = Keypair::new(); test_metadata .create( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), None, 10, true, ) .await .unwrap(); let tx = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, fake_update_authority.pubkey(), None, None, None, None, )], Some(&context.payer.pubkey()), &[&context.payer, &fake_update_authority], context.last_blockhash, ); let result = context .banks_client .process_transaction(tx) .await .unwrap_err(); assert_custom_error!(result, MetadataError::UpdateAuthorityIncorrect); } #[tokio::test] async fn cannot_flip_primary_sale_happened_from_true_to_false() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); let name = "Test".to_string(); let symbol = "TST".to_string(); let uri = "uri".to_string(); // Primary sale happened created as false by default. test_metadata .create( &mut context, name, symbol.clone(), uri.clone(), None, 10, true, ) .await .unwrap(); // Flip true. let tx = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, None, Some(true), None, )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); context.banks_client.process_transaction(tx).await.unwrap(); // Try to flip back to false; this should fail. let tx = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, None, Some(false), None, )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); let result = context .banks_client .process_transaction(tx) .await .unwrap_err(); // We should not be able to make an immutable NFT mutable again. assert_custom_error!(result, MetadataError::PrimarySaleCanOnlyBeFlippedToTrue); } #[tokio::test] async fn cannot_flip_is_mutable_from_false_to_true() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); let name = "Test".to_string(); let symbol = "TST".to_string(); let uri = "uri".to_string(); // Start with NFT immutable. let is_mutable = false; test_metadata .create( &mut context, name, symbol.clone(), uri.clone(), None, 10, is_mutable, ) .await .unwrap(); let tx = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, None, None, // Try to flip to be mutable. Some(true), )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); let result = context .banks_client .process_transaction(tx) .await .unwrap_err(); // We should not be able to make an immutable NFT mutable again. assert_custom_error!(result, MetadataError::IsMutableCanOnlyBeFlippedToFalse); } #[tokio::test] async fn fail_cannot_verify_collection() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); test_metadata .create_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), None, 10, true, None, None, None, ) .await .unwrap(); let test_collection = Metadata::new(); test_collection .create_v2( &mut context, "Test Col".to_string(), "TSTCOL".to_string(), "uricol".to_string(), None, 10, false, None, None, None, ) .await .unwrap(); let collection_master_edition_account = MasterEditionV2::new(&test_collection); collection_master_edition_account .create_v3(&mut context, Some(1)) .await .unwrap(); let tx = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, Some(DataV2 { name: "Test".to_string(), symbol: "TST".to_string(), uri: "uri".to_string(), creators: None, seller_fee_basis_points: 10, collection: Some(Collection { key: test_collection.pubkey, verified: true, }), uses: None, }), None, None, )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); let result = context .banks_client .process_transaction(tx) .await .unwrap_err(); assert_custom_error!( result, MetadataError::CollectionCannotBeVerifiedInThisInstruction ); } #[tokio::test] async fn fail_cannot_change_collection_key_when_verified() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); test_metadata .create_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), None, 10, true, None, None, None, ) .await .unwrap(); let new_collection_authority = Keypair::new(); let test_collection = Metadata::new(); test_collection .create_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), None, 10, false, None, None, None, ) .await .unwrap(); let collection_master_edition_account = MasterEditionV2::new(&test_collection); collection_master_edition_account .create_v3(&mut context, Some(0)) .await .unwrap(); let update_authority = context.payer.pubkey(); let (record, _) = find_collection_authority_account( &test_collection.mint.pubkey(), &new_collection_authority.pubkey(), ); let ix = mpl_token_metadata::instruction::approve_collection_authority( mpl_token_metadata::id(), record, new_collection_authority.pubkey(), update_authority, context.payer.pubkey(), test_collection.pubkey, test_collection.mint.pubkey(), ); let tx1 = Transaction::new_signed_with_payer( &[ix], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); context.banks_client.process_transaction(tx1).await.unwrap(); test_metadata .set_and_verify_collection( &mut context, test_collection.pubkey, &new_collection_authority, update_authority, test_collection.mint.pubkey(), collection_master_edition_account.pubkey, Some(record), ) .await .unwrap(); let fake_collection_pubkey = collection_master_edition_account.pubkey; let tx2 = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, Some(DataV2 { name: "Test".to_string(), symbol: "TST".to_string(), uri: "uri".to_string(), creators: None, seller_fee_basis_points: 10, collection: Some(Collection { key: fake_collection_pubkey, verified: true, }), uses: None, }), None, None, )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); let result = context .banks_client .process_transaction(tx2) .await .unwrap_err(); assert_custom_error!( result, MetadataError::CollectionCannotBeVerifiedInThisInstruction ); } #[tokio::test] async fn extra_data_zeroed() { let mut context = program_test().start_with_context().await; /* Create a metadata account with five creators. Update it to have only one and ensure that all data after uses struct is zeroed out. */ let creator1 = Keypair::new(); let creator2 = Keypair::new(); let creator3 = Keypair::new(); let creator4 = Keypair::new(); let creators = vec![ Creator { address: creator1.pubkey(), verified: false, share: 20, }, Creator { address: creator2.pubkey(), verified: false, share: 20, }, Creator { address: creator3.pubkey(), verified: false, share: 20, }, Creator { address: creator4.pubkey(), verified: false, share: 20, }, // Context key must be in array or we get an error. Creator { address: context.payer.pubkey(), verified: false, share: 20, }, ]; let test_metadata = Metadata::new(); test_metadata .create_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), Some(creators), 10, true, None, None, None, ) .await .unwrap(); test_metadata .update_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), Some(vec![Creator { address: creator1.pubkey(), verified: false, share: 100, }]), 10, true, None, None, ) .await .unwrap(); let data = get_account(&mut context, &test_metadata.pubkey).await.data; let padding_index = 1 + 32 + 32 + 36 + 14 + 204 + 7 + 34 + 1 + 1 + 2 + 2 + 1 + 1; let zeros_len = data.len() - padding_index; let zeros = vec![0u8; zeros_len]; assert_eq!(data[padding_index..], zeros[..]); } #[tokio::test] async fn fail_invalid_use_method() { let mut context = program_test().start_with_context().await; let test_metadata = Metadata::new(); test_metadata .create_v2( &mut context, "Test".to_string(), "TST".to_string(), "uri".to_string(), None, 10, true, None, None, Some(Uses { use_method: UseMethod::Single, remaining: 1, total: 1, }), ) .await .unwrap(); let tx = Transaction::new_signed_with_payer( &[instruction::update_metadata_accounts_v2( id(), test_metadata.pubkey, context.payer.pubkey(), None, Some(DataV2 { name: "Test".to_string(), symbol: "TST".to_string(), uri: "uri".to_string(), creators: None, seller_fee_basis_points: 10, collection: None, uses: Some(Uses { use_method: UseMethod::Multiple, remaining: 1, total: 1, }), }), None, None, )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, ); let result = context .banks_client .process_transaction(tx) .await .unwrap_err(); assert_custom_error!(result, MetadataError::InvalidUseMethod); } }