diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts index 683840d9..fdd71827 100644 --- a/clients/js/test/_setup.ts +++ b/clients/js/test/_setup.ts @@ -5,6 +5,7 @@ import { Pda, PublicKey, SolAmount, + TransactionWithMeta, generateSigner, none, publicKey, @@ -101,3 +102,23 @@ export const mint = async ( ), }; }; + +// TransactionWithMeta doesn't have ReturnData field that is discribed in +// https://solana.com/docs/rpc/http/gettransaction#result +// so ugly log parsing is provided +export function getReturnLog( + transaction: TransactionWithMeta | null +): null | [string, string, Buffer] { + if (transaction === null) { + return null; + } + const prefix = 'Program return: '; + let log = transaction.meta.logs.find((logs) => logs.startsWith(prefix)); + if (log === undefined) { + return null; + } + log = log.slice(prefix.length); + const [key, data] = log.split(' ', 2); + const buffer = Buffer.from(data, 'base64'); + return [key, data, buffer]; +} diff --git a/clients/js/test/mintToCollectionV1.test.ts b/clients/js/test/mintToCollectionV1.test.ts index a4fb9e97..07db8706 100644 --- a/clients/js/test/mintToCollectionV1.test.ts +++ b/clients/js/test/mintToCollectionV1.test.ts @@ -18,10 +18,12 @@ import test from 'ava'; import { MetadataArgsArgs, fetchMerkleTree, + findLeafAssetIdPda, + getLeafSchemaSerializer, hashLeaf, mintToCollectionV1, } from '../src'; -import { createTree, createUmi } from './_setup'; +import { createTree, createUmi, getReturnLog } from './_setup'; test('it can mint an NFT from a collection (collection unverified in passed-in metadata)', async (t) => { // Given an empty Bubblegum tree. @@ -279,3 +281,53 @@ test('it can mint an NFT from a collection using a legacy collection delegate', }); t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf)); }); + +test('it can mint an NFT from a collection (collection verified in passed-in metadata) && can get created LeafSchema from returnedValue ', async (t) => { + // Given an empty Bubblegum tree. + const umi = await createUmi(); + const merkleTree = await createTree(umi); + const leafOwner = generateSigner(umi).publicKey; + + // And a Collection NFT. + const collectionMint = generateSigner(umi); + await createNft(umi, { + mint: collectionMint, + name: 'My Collection', + uri: 'https://example.com/my-collection.json', + sellerFeeBasisPoints: percentAmount(5.5), // 5.5% + isCollection: true, + }).sendAndConfirm(umi); + + // When we mint a new NFT from the tree using the following metadata, with collection verified. + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 550, // 5.5% + collection: { + key: collectionMint.publicKey, + verified: true, + }, + creators: [], + }; + + const transactionResult = await mintToCollectionV1(umi, { + leafOwner, + merkleTree, + metadata, + collectionMint: collectionMint.publicKey, + }).sendAndConfirm(umi); + const transaction = await umi.rpc.getTransaction(transactionResult.signature); + + const unparsedLogs = getReturnLog(transaction); + if (unparsedLogs != null) { + const buffer = unparsedLogs[2]; + const leaf = getLeafSchemaSerializer().deserialize(buffer)[0]; + const assetId = findLeafAssetIdPda(umi, { merkleTree, leafIndex: 0 }); + + t.is(leafOwner, leaf.owner); + t.is(Number(leaf.nonce), 0); + t.is(leaf.id, assetId[0]); + } else { + t.fail(); + } +}); diff --git a/clients/js/test/mintV1.test.ts b/clients/js/test/mintV1.test.ts index 7aa87127..d88c392f 100644 --- a/clients/js/test/mintV1.test.ts +++ b/clients/js/test/mintV1.test.ts @@ -7,13 +7,15 @@ import { } from '@metaplex-foundation/umi'; import test from 'ava'; import { - MetadataArgsArgs, TokenStandard, + MetadataArgsArgs, fetchMerkleTree, + findLeafAssetIdPda, + getLeafSchemaSerializer, hashLeaf, mintV1, } from '../src'; -import { createTree, createUmi } from './_setup'; +import { createTree, createUmi, getReturnLog } from './_setup'; test('it can mint an NFT from a Bubblegum tree', async (t) => { // Given an empty Bubblegum tree. @@ -101,3 +103,39 @@ test('it cannot mint an NFT from a Bubblegum tree because token standard is wron // Then we expect a program error because metadata's token standard is FungibleAsset which is wrong. await t.throwsAsync(promise, { name: 'InvalidTokenStandard' }); }); + +test('it can get LeafSchema from mint without CPI', async (t) => { + // Given an empty Bubblegum tree. + const umi = await createUmi(); + + const merkleTree = await createTree(umi); + const leafOwner = generateSigner(umi).publicKey; + + // When we mint a new NFT from the tree using the following metadata. + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 500, // 5% + collection: none(), + creators: [], + }; + const transactionResult = await mintV1(umi, { + leafOwner, + merkleTree, + metadata, + }).sendAndConfirm(umi); + const transaction = await umi.rpc.getTransaction(transactionResult.signature); + + const unparsedLogs = getReturnLog(transaction); + if (unparsedLogs != null) { + const buffer = unparsedLogs[2]; + const leaf = getLeafSchemaSerializer().deserialize(buffer)[0]; + const assetId = findLeafAssetIdPda(umi, { merkleTree, leafIndex: 0 }); + + t.is(leafOwner, leaf.owner); + t.is(Number(leaf.nonce), 0); + t.is(leaf.id, assetId[0]); + } else { + t.fail(); + } +}); diff --git a/clients/rust/tests/mint.rs b/clients/rust/tests/mint.rs index 76ab48d6..72915143 100644 --- a/clients/rust/tests/mint.rs +++ b/clients/rust/tests/mint.rs @@ -2,7 +2,11 @@ pub mod setup; pub use setup::*; -use mpl_bubblegum::types::{Creator, MetadataArgs, TokenProgramVersion, TokenStandard}; +use mpl_bubblegum::{ + hash::{hash_creators, hash_metadata}, + types::{Creator, MetadataArgs, TokenProgramVersion, TokenStandard}, + utils::get_asset_id, +}; use solana_program_test::tokio; use solana_sdk::signature::Keypair; use solana_sdk::signature::Signer; @@ -117,4 +121,59 @@ mod mint { tree_manager.assert_root(&mut context).await; } + + #[tokio::test] + async fn recieve_leaf_schema() { + let mut program_test = create_program_test(); + program_test.set_compute_max_units(400_000); + let mut context = program_test.start_with_context().await; + + // Given a new merkle tree. + + let mut tree_manager = TreeManager::<5, 8>::default(); + tree_manager.create(&mut context).await.unwrap(); + + assert!(find_account(&mut context, &tree_manager.tree.pubkey()) + .await + .is_some()); + + // When minting a new cNFT. + + let owner = Keypair::new(); + + let metadata = MetadataArgs { + name: String::from("cNFT"), + uri: String::from("https://c.nft"), + symbol: String::from("cNFT"), + creators: vec![Creator { + address: context.payer.pubkey(), + share: 100, + verified: false, + }], + edition_nonce: None, + is_mutable: true, + primary_sale_happened: true, + seller_fee_basis_points: 500, + token_program_version: TokenProgramVersion::Original, + token_standard: Some(TokenStandard::NonFungible), + collection: None, + uses: None, + }; + + let leaf = tree_manager + .mint(&mut context, owner.pubkey(), metadata.clone()) + .await + .unwrap(); + + // Then LeafSchema returned and valid. + assert_eq!( + leaf.id(), + get_asset_id(&tree_manager.tree.pubkey(), tree_manager.minted() - 1) + ); + assert_eq!(leaf.owner(), owner.pubkey()); + assert_eq!(leaf.delegate(), owner.pubkey()); + assert_eq!(leaf.nonce(), tree_manager.minted() - 1); + assert_eq!(leaf.data_hash(), hash_metadata(&metadata).unwrap()); + assert_eq!(leaf.creator_hash(), hash_creators(&metadata.creators)); + } } diff --git a/idls/bubblegum.json b/idls/bubblegum.json index ba94b021..43a837a2 100644 --- a/idls/bubblegum.json +++ b/idls/bubblegum.json @@ -538,7 +538,10 @@ "defined": "MetadataArgs" } } - ] + ], + "returns": { + "defined": "LeafSchema" + } }, { "name": "mintV1", @@ -599,7 +602,10 @@ "defined": "MetadataArgs" } } - ] + ], + "returns": { + "defined": "LeafSchema" + } }, { "name": "redeem", diff --git a/programs/bubblegum/program/src/lib.rs b/programs/bubblegum/program/src/lib.rs index f0d468c0..fc73b650 100644 --- a/programs/bubblegum/program/src/lib.rs +++ b/programs/bubblegum/program/src/lib.rs @@ -11,6 +11,7 @@ pub mod utils; use processor::*; use state::{ + leaf_schema::LeafSchema, metaplex_adapter::{MetadataArgs, UpdateArgs}, DecompressibleState, }; @@ -70,7 +71,6 @@ pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { #[program] pub mod bubblegum { - use super::*; /// Burns a leaf node from the tree. @@ -129,12 +129,12 @@ pub mod bubblegum { pub fn mint_to_collection_v1( ctx: Context, metadata_args: MetadataArgs, - ) -> Result<()> { + ) -> Result { processor::mint_to_collection_v1(ctx, metadata_args) } /// Mints a new asset. - pub fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> { + pub fn mint_v1(ctx: Context, message: MetadataArgs) -> Result { processor::mint_v1(ctx, message) } diff --git a/programs/bubblegum/program/src/processor/mint.rs b/programs/bubblegum/program/src/processor/mint.rs index 3ec28885..f40bb1ad 100644 --- a/programs/bubblegum/program/src/processor/mint.rs +++ b/programs/bubblegum/program/src/processor/mint.rs @@ -32,7 +32,7 @@ pub struct MintV1<'info> { pub system_program: Program<'info, System>, } -pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> { +pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result { // TODO -> Separate V1 / V1 into seperate instructions let payer = ctx.accounts.payer.key(); let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); @@ -68,7 +68,7 @@ pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> .map(|a| a.key()), ); - process_mint_v1( + let leaf = process_mint_v1( message, owner, delegate, @@ -83,7 +83,7 @@ pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> authority.increment_mint_count(); - Ok(()) + Ok(leaf) } pub(crate) fn process_mint_v1<'info>( @@ -97,7 +97,7 @@ pub(crate) fn process_mint_v1<'info>( wrapper: &Program<'info, Noop>, compression_program: &AccountInfo<'info>, allow_verified_collection: bool, -) -> Result<()> { +) -> Result { assert_metadata_is_mpl_compatible(&message)?; if !allow_verified_collection { if let Some(collection) = &message.collection { @@ -160,5 +160,7 @@ pub(crate) fn process_mint_v1<'info>( &merkle_tree.to_account_info(), &wrapper.to_account_info(), leaf.to_node(), - ) + )?; + + Ok(leaf) } diff --git a/programs/bubblegum/program/src/processor/mint_to_collection.rs b/programs/bubblegum/program/src/processor/mint_to_collection.rs index 97334108..fd835233 100644 --- a/programs/bubblegum/program/src/processor/mint_to_collection.rs +++ b/programs/bubblegum/program/src/processor/mint_to_collection.rs @@ -6,6 +6,7 @@ use spl_account_compression::{program::SplAccountCompression, Noop}; use crate::{ error::BubblegumError, state::{ + leaf_schema::LeafSchema, metaplex_adapter::MetadataArgs, metaplex_anchor::{MplTokenMetadata, TokenMetadata}, TreeConfig, COLLECTION_CPI_PREFIX, @@ -57,7 +58,7 @@ pub struct MintToCollectionV1<'info> { pub(crate) fn mint_to_collection_v1( ctx: Context, metadata_args: MetadataArgs, -) -> Result<()> { +) -> Result { let mut message = metadata_args; // TODO -> Separate V1 / V1 into seperate instructions let payer = ctx.accounts.payer.key(); @@ -118,7 +119,7 @@ pub(crate) fn mint_to_collection_v1( true, )?; - process_mint_v1( + let leaf = process_mint_v1( message, owner, delegate, @@ -133,5 +134,5 @@ pub(crate) fn mint_to_collection_v1( authority.increment_mint_count(); - Ok(()) + Ok(leaf) }