From fe5dae5f513b91079e4f0510b383c08fe7b1a850 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:20:55 -0700 Subject: [PATCH] Fixes to `set_and_verify_collection` (#43) * Removing deploy capabilities * Adding back program deploy. * Update collection verification checks * Add Umi tests around collection verification * Add deprecated log message (#2) * Set decompressible on tree creation (#3) * Fix binary name * Set decompressible on tree creation * Fix binary name * Revert "Removing deploy capabilities" This reverts commit c203069e3b99c30c5e6a68176cca2c47bd59b101. --------- Co-authored-by: blockiosaurus Co-authored-by: Fernando Otero --- .github/workflows/deploy-program.yml | 2 +- clients/js-solita/package.json | 2 +- clients/js-solita/tests/main.test.ts | 127 ++++++++++++------ clients/js/test/mintToCollectionV1.test.ts | 64 ++++++++- .../js/test/setAndVerifyCollection.test.ts | 83 ++++++++++++ clients/js/test/unverifyCollection.test.ts | 62 +++++++++ clients/js/test/verifyCollection.test.ts | 65 +++++++++ programs/bubblegum/program/src/lib.rs | 1 + .../src/processor/mint_to_collection.rs | 1 - .../bubblegum/program/src/processor/mod.rs | 38 +++--- 10 files changed, 380 insertions(+), 65 deletions(-) diff --git a/.github/workflows/deploy-program.yml b/.github/workflows/deploy-program.yml index bee294a3..e2943e82 100644 --- a/.github/workflows/deploy-program.yml +++ b/.github/workflows/deploy-program.yml @@ -114,7 +114,7 @@ jobs: if [ "${{ inputs.program }}" == "bubblegum" ]; then echo ${{ secrets.BUBBLEGUM_DEPLOY_KEY }} > ./deploy-key.json echo ${{ secrets.BUBBLEGUM_ID }} > ./program-id.json - echo PROGRAM_NAME="mpl_bubblegum" >> $GITHUB_ENV + echo PROGRAM_NAME="bubblegum" >> $GITHUB_ENV fi - name: Bump program version diff --git a/clients/js-solita/package.json b/clients/js-solita/package.json index 16c4d418..a1de41b3 100644 --- a/clients/js-solita/package.json +++ b/clients/js-solita/package.json @@ -13,7 +13,7 @@ "postpublish": "git push origin && git push origin --tags", "build:docs": "typedoc", "build": "rimraf dist && tsc -p tsconfig.json", - "start-validator": "solana-test-validator -ud --quiet --reset -c cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK -c 4VTQredsAmr1yzRJugLV6Mt6eu6XMeCwdkZ73wwVMWHv -c noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV -c 3RHkdjCwWyK2firrwFQGvXCxbUpBky1GTmb9EDK9hUnX -c metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s -c PwDiXFxQsGra4sFFTT8r1QWRMd4vfumiWC1jfWNfdYT --bpf-program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY ../../programs/.bin/mpl_bubblegum.so", + "start-validator": "solana-test-validator -ud --quiet --reset -c cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK -c 4VTQredsAmr1yzRJugLV6Mt6eu6XMeCwdkZ73wwVMWHv -c noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV -c 3RHkdjCwWyK2firrwFQGvXCxbUpBky1GTmb9EDK9hUnX -c metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s -c PwDiXFxQsGra4sFFTT8r1QWRMd4vfumiWC1jfWNfdYT --bpf-program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY ../../programs/.bin/bubblegum.so", "run-tests": "jest tests --detectOpenHandles", "test": "start-server-and-test start-validator http://localhost:8899/health run-tests", "api:gen": "DEBUG='(solita|rustbin):(info|error)' solita", diff --git a/clients/js-solita/tests/main.test.ts b/clients/js-solita/tests/main.test.ts index c70c4074..269f4ba7 100644 --- a/clients/js-solita/tests/main.test.ts +++ b/clients/js-solita/tests/main.test.ts @@ -15,7 +15,7 @@ import { ConcurrentMerkleTreeAccount, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, - ValidDepthSizePair + ValidDepthSizePair, } from '@solana/spl-account-compression'; import { @@ -30,19 +30,21 @@ import { TokenProgramVersion, TokenStandard, Creator, + createSetDecompressibleStateInstruction, + DecompressibleState, } from '../src/generated'; -import { getLeafAssetId, computeDataHash, computeCreatorHash, computeCompressedNFTHash } from '../src/mpl-bubblegum'; -import { BN } from 'bn.js'; -import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"; import { - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID -} from "@solana/spl-token"; + getLeafAssetId, + computeDataHash, + computeCreatorHash, + computeCompressedNFTHash, +} from '../src/mpl-bubblegum'; +import { BN } from 'bn.js'; +import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from '@metaplex-foundation/mpl-token-metadata'; +import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; function keypairFromSeed(seed: string) { - const expandedSeed = Uint8Array.from( - Buffer.from(`${seed}`), - ); + const expandedSeed = Uint8Array.from(Buffer.from(`${seed}`)); return Keypair.fromSeed(expandedSeed.slice(0, 32)); } @@ -69,8 +71,8 @@ async function setupTreeWithCompressedNFT( compressedNFT: MetadataArgs, depthSizePair: ValidDepthSizePair = { maxDepth: 14, - maxBufferSize: 64 - } + maxBufferSize: 64, + }, ): Promise<{ merkleTree: PublicKey; }> { @@ -78,7 +80,10 @@ async function setupTreeWithCompressedNFT( const merkleTreeKeypair = Keypair.generate(); const merkleTree = merkleTreeKeypair.publicKey; - const space = getConcurrentMerkleTreeAccountSize(depthSizePair.maxDepth, depthSizePair.maxBufferSize); + const space = getConcurrentMerkleTreeAccountSize( + depthSizePair.maxDepth, + depthSizePair.maxBufferSize, + ); const allocTreeIx = SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: merkleTree, @@ -107,6 +112,17 @@ async function setupTreeWithCompressedNFT( BUBBLEGUM_PROGRAM_ID, ); + const setDecompressibleStateIx = createSetDecompressibleStateInstruction( + { + treeAuthority, + treeCreator: payer, + }, + { + decompressableState: DecompressibleState.Enabled, + }, + BUBBLEGUM_PROGRAM_ID, + ); + const mintIx = createMintV1Instruction( { merkleTree, @@ -123,7 +139,11 @@ async function setupTreeWithCompressedNFT( }, ); - const tx = new Transaction().add(allocTreeIx).add(createTreeIx).add(mintIx); + const tx = new Transaction() + .add(allocTreeIx) + .add(createTreeIx) + .add(setDecompressibleStateIx) + .add(mintIx); tx.feePayer = payer; await sendAndConfirmTransaction(connection, tx, [merkleTreeKeypair, payerKeypair], { commitment: 'confirmed', @@ -158,7 +178,10 @@ describe('Bubblegum tests', () => { sellerFeeBasisPoints: 0, isMutable: false, }; - await setupTreeWithCompressedNFT(connection, payerKeypair, compressedNFT, { maxDepth: 14, maxBufferSize: 64 }); + await setupTreeWithCompressedNFT(connection, payerKeypair, compressedNFT, { + maxDepth: 14, + maxBufferSize: 64, + }); }); describe('Unit test compressed NFT instructions', () => { @@ -174,7 +197,7 @@ describe('Bubblegum tests', () => { share: 45, verified: false, }, - ] + ]; const originalCompressedNFT = makeCompressedNFT('test', 'TST', creators); beforeEach(async () => { await connection.requestAirdrop(payer, LAMPORTS_PER_SOL); @@ -185,7 +208,7 @@ describe('Bubblegum tests', () => { { maxDepth: 14, maxBufferSize: 64, - } + }, ); merkleTree = result.merkleTree; }); @@ -197,15 +220,12 @@ describe('Bubblegum tests', () => { // Verify leaf exists. const leafIndex = new BN.BN(0); const assetId = await getLeafAssetId(merkleTree, leafIndex); - const verifyLeafIx = createVerifyLeafIx( - merkleTree, - { - root: account.getCurrentRoot(), - leaf: computeCompressedNFTHash(assetId, payer, payer, leafIndex, originalCompressedNFT), - leafIndex: 0, - proof: [], - } - ); + const verifyLeafIx = createVerifyLeafIx(merkleTree, { + root: account.getCurrentRoot(), + leaf: computeCompressedNFTHash(assetId, payer, payer, leafIndex, originalCompressedNFT), + leafIndex: 0, + proof: [], + }); const tx = new Transaction().add(verifyLeafIx); const txId = await sendAndConfirmTransaction(connection, tx, [payerKeypair], { commitment: 'confirmed', @@ -240,7 +260,7 @@ describe('Bubblegum tests', () => { dataHash: Array.from(computeDataHash(originalCompressedNFT)), creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)), nonce: 0, - index: 0 + index: 0, }, ); @@ -268,16 +288,21 @@ describe('Bubblegum tests', () => { dataHash: Array.from(computeDataHash(originalCompressedNFT)), creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)), nonce: 0, - index: 0 + index: 0, }, ); const burnTx = new Transaction().add(burnIx); burnTx.feePayer = payer; - const burnTxId = await sendAndConfirmTransaction(connection, burnTx, [payerKeypair, newLeafOwnerKeypair], { - commitment: 'confirmed', - skipPreflight: true, - }); + const burnTxId = await sendAndConfirmTransaction( + connection, + burnTx, + [payerKeypair, newLeafOwnerKeypair], + { + commitment: 'confirmed', + skipPreflight: true, + }, + ); console.log('NFT burn tx:', burnTxId); }); @@ -292,7 +317,11 @@ describe('Bubblegum tests', () => { ); const nonce = new BN.BN(0); const [voucher] = PublicKey.findProgramAddressSync( - [Buffer.from('voucher', 'utf8'), merkleTree.toBuffer(), Uint8Array.from(nonce.toArray('le', 8))], + [ + Buffer.from('voucher', 'utf8'), + merkleTree.toBuffer(), + Uint8Array.from(nonce.toArray('le', 8)), + ], BUBBLEGUM_PROGRAM_ID, ); @@ -311,7 +340,7 @@ describe('Bubblegum tests', () => { dataHash: Array.from(computeDataHash(originalCompressedNFT)), creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)), nonce, - index: 0 + index: 0, }, ); @@ -326,7 +355,11 @@ describe('Bubblegum tests', () => { // Decompress. const [mint] = PublicKey.findProgramAddressSync( - [Buffer.from('asset', 'utf8'), merkleTree.toBuffer(), Uint8Array.from(nonce.toArray('le', 8))], + [ + Buffer.from('asset', 'utf8'), + merkleTree.toBuffer(), + Uint8Array.from(nonce.toArray('le', 8)), + ], BUBBLEGUM_PROGRAM_ID, ); const [tokenAccount] = PublicKey.findProgramAddressSync( @@ -335,14 +368,19 @@ describe('Bubblegum tests', () => { ); const [mintAuthority] = PublicKey.findProgramAddressSync( [mint.toBuffer()], - BUBBLEGUM_PROGRAM_ID + BUBBLEGUM_PROGRAM_ID, ); const [metadata] = PublicKey.findProgramAddressSync( [Buffer.from('metadata', 'utf8'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()], TOKEN_METADATA_PROGRAM_ID, ); const [masterEdition] = PublicKey.findProgramAddressSync( - [Buffer.from('metadata', 'utf8'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('edition', 'utf8')], + [ + Buffer.from('metadata', 'utf8'), + TOKEN_METADATA_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + Buffer.from('edition', 'utf8'), + ], TOKEN_METADATA_PROGRAM_ID, ); @@ -361,16 +399,21 @@ describe('Bubblegum tests', () => { logWrapper: SPL_NOOP_PROGRAM_ID, }, { - metadata: originalCompressedNFT + metadata: originalCompressedNFT, }, ); const decompressTx = new Transaction().add(decompressIx); decompressTx.feePayer = payer; - const decompressTxId = await sendAndConfirmTransaction(connection, decompressTx, [payerKeypair], { - commitment: 'confirmed', - skipPreflight: true, - }); + const decompressTxId = await sendAndConfirmTransaction( + connection, + decompressTx, + [payerKeypair], + { + commitment: 'confirmed', + skipPreflight: true, + }, + ); console.log('NFT decompress tx:', decompressTxId); }); diff --git a/clients/js/test/mintToCollectionV1.test.ts b/clients/js/test/mintToCollectionV1.test.ts index a0d43123..a4fb9e97 100644 --- a/clients/js/test/mintToCollectionV1.test.ts +++ b/clients/js/test/mintToCollectionV1.test.ts @@ -23,7 +23,7 @@ import { } from '../src'; import { createTree, createUmi } from './_setup'; -test('it can mint an NFT from a collection', async (t) => { +test('it can mint an NFT from a collection (collection unverified in passed-in metadata)', async (t) => { // Given an empty Bubblegum tree. const umi = await createUmi(); const merkleTree = await createTree(umi); @@ -45,7 +45,7 @@ test('it can mint an NFT from a collection', async (t) => { isCollection: true, }).sendAndConfirm(umi); - // When we mint a new NFT from the tree using the following metadata. + // When we mint a new NFT from the tree using the following metadata, with collection unverified. const metadata: MetadataArgsArgs = { name: 'My NFT', uri: 'https://example.com/my-nft.json', @@ -83,6 +83,66 @@ test('it can mint an NFT from a collection', async (t) => { t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf)); }); +test('it can mint an NFT from a collection (collection verified in passed-in metadata)', async (t) => { + // Given an empty Bubblegum tree. + const umi = await createUmi(); + const merkleTree = await createTree(umi); + const leafOwner = generateSigner(umi).publicKey; + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.sequenceNumber, 0n); + t.is(merkleTreeAccount.tree.activeIndex, 0n); + t.is(merkleTreeAccount.tree.bufferSize, 1n); + t.is(merkleTreeAccount.tree.rightMostPath.index, 0); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, defaultPublicKey()); + + // 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: [], + }; + await mintToCollectionV1(umi, { + leafOwner, + merkleTree, + metadata, + collectionMint: collectionMint.publicKey, + }).sendAndConfirm(umi); + + // Then a new leaf was added to the merkle tree. + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.sequenceNumber, 1n); + t.is(merkleTreeAccount.tree.activeIndex, 1n); + t.is(merkleTreeAccount.tree.bufferSize, 2n); + t.is(merkleTreeAccount.tree.rightMostPath.index, 1); + + // And the hash of the metadata matches the new leaf. + const leaf = hashLeaf(umi, { + merkleTree, + owner: leafOwner, + leafIndex: 0, + metadata: { + ...metadata, + collection: some({ key: collectionMint.publicKey, verified: true }), + }, + }); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf)); +}); + test('it can mint an NFT from a collection using a collection delegate', async (t) => { // Given an empty Bubblegum tree. const umi = await createUmi(); diff --git a/clients/js/test/setAndVerifyCollection.test.ts b/clients/js/test/setAndVerifyCollection.test.ts index d8e618ac..7aa54559 100644 --- a/clients/js/test/setAndVerifyCollection.test.ts +++ b/clients/js/test/setAndVerifyCollection.test.ts @@ -113,3 +113,86 @@ test('it cannot set and verify the collection if the tree creator or delegate do // Then we expect a program error. await t.throwsAsync(promise, { name: 'UpdateAuthorityIncorrect' }); }); + +test('it cannot set and verify the collection if there is already a verified collection', async (t) => { + // Given a first Collection NFT. + const umi = await createUmi(); + const firstCollectionMint = generateSigner(umi); + const firstCollectionAuthority = generateSigner(umi); + await createNft(umi, { + mint: firstCollectionMint, + authority: firstCollectionAuthority, + name: 'My Collection 1', + uri: 'https://example.com/my-collection-1.json', + sellerFeeBasisPoints: percentAmount(5.5), // 5.5% + isCollection: true, + }).sendAndConfirm(umi); + + // And a tree with a minted NFT that has a verified collection of that first Collection NFT. + const treeCreator = await generateSignerWithSol(umi); + const merkleTree = await createTree(umi, { treeCreator }); + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + const leafOwner = generateSigner(umi).publicKey; + const { metadata, leafIndex } = await mint(umi, { + merkleTree, + treeCreatorOrDelegate: treeCreator, + leafOwner, + }); + await setAndVerifyCollection(umi, { + leafOwner, + treeCreatorOrDelegate: treeCreator, + collectionMint: firstCollectionMint.publicKey, + collectionAuthority: firstCollectionAuthority, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + nonce: leafIndex, + index: leafIndex, + metadata, + }).sendAndConfirm(umi); + const firstCollectionVerifiedMetadata = { + ...metadata, + collection: { + key: firstCollectionMint.publicKey, + verified: true, + }, + }; + + // And then given a second Collection NFT. + const secondCollectionMint = generateSigner(umi); + const secondCollectionAuthority = generateSigner(umi); + await createNft(umi, { + mint: secondCollectionMint, + authority: secondCollectionAuthority, + name: 'My Collection 2', + uri: 'https://example.com/my-collection-2.json', + sellerFeeBasisPoints: percentAmount(5.5), // 5.5% + isCollection: true, + }).sendAndConfirm(umi); + + // When the second collection authority attempts to set and verify the second collection. + let promise = setAndVerifyCollection(umi, { + leafOwner, + treeCreatorOrDelegate: treeCreator, + collectionMint: secondCollectionMint.publicKey, + collectionAuthority: secondCollectionAuthority, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + nonce: leafIndex, + index: leafIndex, + metadata: firstCollectionVerifiedMetadata, + proof: [], + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'AlreadyVerified' }); + + // And the leaf was not updated in the merkle tree. + const notUpdatedLeaf = hashLeaf(umi, { + merkleTree, + owner: leafOwner, + leafIndex, + metadata: firstCollectionVerifiedMetadata, + }); + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(notUpdatedLeaf)); +}); diff --git a/clients/js/test/unverifyCollection.test.ts b/clients/js/test/unverifyCollection.test.ts index 32e76c4d..f122f0ee 100644 --- a/clients/js/test/unverifyCollection.test.ts +++ b/clients/js/test/unverifyCollection.test.ts @@ -80,3 +80,65 @@ test('it can unverify the collection of a minted compressed NFT', async (t) => { merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf)); }); + +test('it cannot unverify the collection if it is already unverified', async (t) => { + // Given a Collection NFT. + const umi = await createUmi(); + const collectionMint = generateSigner(umi); + const collectionAuthority = generateSigner(umi); + await createNft(umi, { + mint: collectionMint, + authority: collectionAuthority, + name: 'My Collection', + uri: 'https://example.com/my-collection.json', + sellerFeeBasisPoints: percentAmount(5.5), // 5.5% + isCollection: true, + }).sendAndConfirm(umi); + + // And a tree with a minted NFT that has an unverified collection. + const merkleTree = await createTree(umi); + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + const leafOwner = generateSigner(umi).publicKey; + const { metadata, leafIndex } = await mint(umi, { + merkleTree, + leafOwner, + metadata: { + collection: { + key: collectionMint.publicKey, + verified: false, + }, + }, + }); + + // When the collection authority attempts to unverify the collection. + const promise = unverifyCollection(umi, { + leafOwner, + collectionMint: collectionMint.publicKey, + collectionAuthority, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + nonce: leafIndex, + index: leafIndex, + metadata, + proof: [], + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'AlreadyUnverified' }); + + // And the leaf was not updated in the merkle tree. + const notUpdatedLeaf = hashLeaf(umi, { + merkleTree, + owner: leafOwner, + leafIndex, + metadata: { + ...metadata, + collection: { + key: collectionMint.publicKey, + verified: false, + }, + }, + }); + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(notUpdatedLeaf)); +}); diff --git a/clients/js/test/verifyCollection.test.ts b/clients/js/test/verifyCollection.test.ts index 8acfa919..d43caafb 100644 --- a/clients/js/test/verifyCollection.test.ts +++ b/clients/js/test/verifyCollection.test.ts @@ -9,6 +9,7 @@ import { fetchMerkleTree, getCurrentRoot, hashLeaf, + setAndVerifyCollection, verifyCollection, } from '../src'; import { createTree, createUmi, mint } from './_setup'; @@ -71,3 +72,67 @@ test('it can verify the collection of a minted compressed NFT', async (t) => { merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf)); }); + +test('it cannot verify the collection if it is already verified', async (t) => { + // Given a Collection NFT. + const umi = await createUmi(); + const collectionMint = generateSigner(umi); + const collectionAuthority = generateSigner(umi); + await createNft(umi, { + mint: collectionMint, + authority: collectionAuthority, + name: 'My Collection', + uri: 'https://example.com/my-collection.json', + sellerFeeBasisPoints: percentAmount(5.5), // 5.5% + isCollection: true, + }).sendAndConfirm(umi); + + // And a tree with a minted NFT that has a verified collection. + const merkleTree = await createTree(umi); + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + const leafOwner = generateSigner(umi).publicKey; + const { metadata, leafIndex } = await mint(umi, { merkleTree, leafOwner }); + await setAndVerifyCollection(umi, { + leafOwner, + collectionMint: collectionMint.publicKey, + collectionAuthority, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + nonce: leafIndex, + index: leafIndex, + metadata, + }).sendAndConfirm(umi); + const verifiedMetadata = { + ...metadata, + collection: { + key: collectionMint.publicKey, + verified: true, + }, + }; + + // When the collection authority attempts to verify the collection. + const promise = verifyCollection(umi, { + leafOwner, + collectionMint: collectionMint.publicKey, + collectionAuthority, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + nonce: leafIndex, + index: leafIndex, + metadata: verifiedMetadata, + proof: [], + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'AlreadyVerified' }); + + // And the leaf was not updated in the merkle tree. + const notUpdatedLeaf = hashLeaf(umi, { + merkleTree, + owner: leafOwner, + leafIndex, + metadata: verifiedMetadata, + }); + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(notUpdatedLeaf)); +}); diff --git a/programs/bubblegum/program/src/lib.rs b/programs/bubblegum/program/src/lib.rs index 4883a79e..03bc3690 100644 --- a/programs/bubblegum/program/src/lib.rs +++ b/programs/bubblegum/program/src/lib.rs @@ -180,6 +180,7 @@ pub mod bubblegum { ctx: Context, decompressable_state: DecompressibleState, ) -> Result<()> { + msg!("Deprecated: please use `set_decompressible_state` instead"); processor::set_decompressible_state(ctx, decompressable_state) } diff --git a/programs/bubblegum/program/src/processor/mint_to_collection.rs b/programs/bubblegum/program/src/processor/mint_to_collection.rs index ea9a3ca9..97334108 100644 --- a/programs/bubblegum/program/src/processor/mint_to_collection.rs +++ b/programs/bubblegum/program/src/processor/mint_to_collection.rs @@ -116,7 +116,6 @@ pub(crate) fn mint_to_collection_v1( &token_metadata_program, &mut message, true, - None, )?; process_mint_v1( diff --git a/programs/bubblegum/program/src/processor/mod.rs b/programs/bubblegum/program/src/processor/mod.rs index 57b2b040..76cc8bf0 100644 --- a/programs/bubblegum/program/src/processor/mod.rs +++ b/programs/bubblegum/program/src/processor/mod.rs @@ -166,7 +166,6 @@ fn process_collection_verification_mpl_only<'info>( token_metadata_program: &AccountInfo<'info>, message: &mut MetadataArgs, verify: bool, - new_collection: Option, ) -> Result<()> { // See if a collection authority record PDA was provided. let collection_authority_record = if collection_authority_record_pda.key() == crate::id() { @@ -189,24 +188,8 @@ fn process_collection_verification_mpl_only<'info>( BubblegumError::IncorrectOwner ); - // If new collection was provided, set it in the NFT metadata. - if new_collection.is_some() { - message.collection = new_collection.map(|key| metaplex_adapter::Collection { - verified: false, // Set to true below. - key, - }); - } - // If the NFT has collection data, we set it to the correct value after doing some validation. if let Some(collection) = &mut message.collection { - // Don't verify already verified items, or unverify unverified items, otherwise for sized - // collections we end up with invalid size data. - if verify && collection.verified { - return Err(BubblegumError::AlreadyVerified.into()); - } else if !verify && !collection.verified { - return Err(BubblegumError::AlreadyUnverified.into()); - } - // Collection verify assert from token-metadata program. assert_collection_verify_is_valid( &Some(collection.adapt()), @@ -308,6 +291,26 @@ fn process_collection_verification<'info>( return Err(BubblegumError::DataHashMismatch.into()); } + // Check existing collection. Don't verify already-verified items, or unverify unverified + // items, otherwise for sized collections we end up with invalid size data. Also, we don't + // allow a new collection (via `set_and_verify_collection`) to overwrite an already-verified + // item. + if let Some(collection) = &message.collection { + if verify && collection.verified { + return Err(BubblegumError::AlreadyVerified.into()); + } else if !verify && !collection.verified { + return Err(BubblegumError::AlreadyUnverified.into()); + } + } + + // If new collection was provided (via `set_and_verify_collection`), set it in the metadata. + if new_collection.is_some() { + message.collection = new_collection.map(|key| metaplex_adapter::Collection { + verified: false, // Will be set to true later. + key, + }); + } + // Note this call mutates message. process_collection_verification_mpl_only( collection_metadata, @@ -320,7 +323,6 @@ fn process_collection_verification<'info>( &token_metadata_program, &mut message, verify, - new_collection, )?; // Calculate new data hash.