From 989a88539a026a109f08df6acf08bac14eb08735 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 26 May 2023 08:54:38 +0100 Subject: [PATCH] Integrate the new Burn and Verify instructions (#519) --- .changeset/ninety-ladybugs-obey.md | 5 + .../plugins/nftModule/operations/deleteNft.ts | 115 +++++++++++++-- .../operations/unverifyNftCollection.ts | 135 ++++++++++++------ .../operations/unverifyNftCreator.ts | 15 +- .../operations/verifyNftCollection.ts | 126 +++++++++++----- .../nftModule/operations/verifyNftCreator.ts | 15 +- .../test/plugins/nftModule/deleteNft.test.ts | 2 +- .../nftModule/unverifyNftCollection.test.ts | 53 +++++++ .../nftModule/verifyNftCollection.test.ts | 52 +++++++ 9 files changed, 418 insertions(+), 100 deletions(-) create mode 100644 .changeset/ninety-ladybugs-obey.md diff --git a/.changeset/ninety-ladybugs-obey.md b/.changeset/ninety-ladybugs-obey.md new file mode 100644 index 000000000..90f7cf627 --- /dev/null +++ b/.changeset/ninety-ladybugs-obey.md @@ -0,0 +1,5 @@ +--- +'@metaplex-foundation/js': patch +--- + +Integrate the new Burn and Verify instructions diff --git a/packages/js/src/plugins/nftModule/operations/deleteNft.ts b/packages/js/src/plugins/nftModule/operations/deleteNft.ts index 94d20dbd0..550b37e93 100644 --- a/packages/js/src/plugins/nftModule/operations/deleteNft.ts +++ b/packages/js/src/plugins/nftModule/operations/deleteNft.ts @@ -1,15 +1,23 @@ -import { createBurnNftInstruction } from '@metaplex-foundation/mpl-token-metadata'; -import { PublicKey } from '@solana/web3.js'; +import { createBurnInstruction } from '@metaplex-foundation/mpl-token-metadata'; +import { PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js'; import { SendAndConfirmTransactionResponse } from '../../rpcModule'; -import { Metaplex } from '@/Metaplex'; +import { + TokenMetadataAuthorityHolder, + TokenMetadataAuthorityTokenDelegate, + getSignerFromTokenMetadataAuthority, + parseTokenMetadataAuthorization, +} from '../Authorization'; +import { TransactionBuilder, TransactionBuilderOptions } from '@/utils'; import { Operation, OperationHandler, OperationScope, Signer, + SplTokenAmount, + token, useOperation, } from '@/types'; -import { TransactionBuilder, TransactionBuilderOptions } from '@/utils'; +import { Metaplex } from '@/Metaplex'; // ----------------- // Operation @@ -50,12 +58,51 @@ export type DeleteNftInput = { mintAddress: PublicKey; /** - * The owner of the NFT as a Signer. + * An authority allowed to burn the asset. * + * Note that Metadata authorities are + * not supported for this instruction. + * + * If a `Signer` is provided directly, + * it will be used as an Holder authority. + * + * @see {@link TokenMetadataAuthority} * @defaultValue `metaplex.identity()` */ + authority?: + | Signer + | TokenMetadataAuthorityTokenDelegate + | TokenMetadataAuthorityHolder; + + /** + * Alias of `authority` for backwards compatibility. + * + * @deprecated Use `authority` instead. + * @see {@link DeleteNftInput.authority} + */ owner?: Signer; + /** + * The mint of the parent edition when the asset is a printed edition. + * + * @defaultValue Defaults to not providing a parent edition to the program. + */ + parentEditionMint?: PublicKey; + + /** + * The token account of the parent edition when the asset is a printed edition. + * + * @defaultValue Defaults to not providing a parent edition to the program. + */ + parentEditionToken?: PublicKey; + + /** + * The edition marker of the asset if it is a printed edition. + * + * @defaultValue Defaults to not providing the edition marker to the program. + */ + editionMarker?: PublicKey; + /** * The explicit token account linking the provided mint and owner * accounts, if that account is not their associated token account. @@ -74,6 +121,13 @@ export type DeleteNftInput = { * Size Collection NFT. */ collection?: PublicKey; + + /** + * The amount of tokens to burn. + * + * @defaultValue `token(1)` + */ + amount?: SplTokenAmount; }; /** @@ -136,14 +190,22 @@ export const deleteNftBuilder = ( const { programs, payer = metaplex.rpc().getDefaultFeePayer() } = options; const { mintAddress, - owner = metaplex.identity(), ownerTokenAccount, collection, + parentEditionMint, + parentEditionToken, + editionMarker, + amount = token(1), } = params; + const authority = + params.authority ?? params.owner ?? (metaplex.identity() as Signer); + + const systemProgram = metaplex.programs().getSystem(programs); const tokenProgram = metaplex.programs().getToken(programs); const tokenMetadataProgram = metaplex.programs().getTokenMetadata(programs); + const owner = getSignerFromTokenMetadataAuthority(authority).publicKey; const metadata = metaplex.nfts().pdas().metadata({ mint: mintAddress, programs, @@ -156,28 +218,51 @@ export const deleteNftBuilder = ( ownerTokenAccount ?? metaplex.tokens().pdas().associatedTokenAccount({ mint: mintAddress, - owner: owner.publicKey, + owner, programs, }); + // Auth. + const auth = parseTokenMetadataAuthorization(metaplex, { + mint: mintAddress, + authority: + '__kind' in authority + ? authority + : { __kind: 'holder', owner: authority, token: tokenAddress }, + programs, + }); + return TransactionBuilder.make() .setFeePayer(payer) .add({ - instruction: createBurnNftInstruction( + instruction: createBurnInstruction( { - metadata, - owner: owner.publicKey, - mint: mintAddress, - tokenAccount: tokenAddress, - masterEditionAccount: edition, - splTokenProgram: tokenProgram.address, + authority: auth.accounts.authority, collectionMetadata: collection ? metaplex.nfts().pdas().metadata({ mint: collection, programs }) : undefined, + metadata, + edition, + mint: mintAddress, + token: auth.accounts.token!, + masterEdition: parentEditionMint + ? metaplex.nfts().pdas().metadata({ + mint: parentEditionMint, + programs, + }) + : undefined, + masterEditionMint: parentEditionMint, + masterEditionToken: parentEditionToken, + editionMarker, + tokenRecord: auth.accounts.delegateRecord, + systemProgram: systemProgram.address, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + splTokenProgram: tokenProgram.address, }, + { burnArgs: { __kind: 'V1', amount: amount.basisPoints } }, tokenMetadataProgram.address ), - signers: [owner], + signers: auth.signers, key: params.instructionKey ?? 'deleteNft', }); }; diff --git a/packages/js/src/plugins/nftModule/operations/unverifyNftCollection.ts b/packages/js/src/plugins/nftModule/operations/unverifyNftCollection.ts index 3a7156ea2..28f490500 100644 --- a/packages/js/src/plugins/nftModule/operations/unverifyNftCollection.ts +++ b/packages/js/src/plugins/nftModule/operations/unverifyNftCollection.ts @@ -1,8 +1,10 @@ import { + VerificationArgs, createUnverifyCollectionInstruction, + createUnverifyInstruction, createUnverifySizedCollectionItemInstruction, } from '@metaplex-foundation/mpl-token-metadata'; -import { PublicKey } from '@solana/web3.js'; +import { PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js'; import { SendAndConfirmTransactionResponse } from '../../rpcModule'; import { Metaplex } from '@/Metaplex'; import { @@ -74,12 +76,28 @@ export type UnverifyNftCollectionInput = { /** * Whether or not the provided `collectionAuthority` is a delegated - * collection authority, i.e. it was approved by the update authority - * using `metaplex.nfts().approveCollectionAuthority()`. + * collection authority, i.e. it was approved by the update authority. + * + * - `false` means the collection authority is the update authority of the collection. + * - `legacyDelegate` means the collection authority is a delegate that was approved + * using the legacy `metaplex.nfts().approveCollectionAuthority()` operation. + * - `metadataDelegate` means the collection authority is a delegate that was approved + * using the new `metaplex.nfts().delegate()` operation. + * - `true` is equivalent to `legacyDelegate` for backwards compatibility. * * @defaultValue `false` */ - isDelegated?: boolean; + isDelegated?: boolean | 'legacyDelegate' | 'metadataDelegate'; + + /** + * The update authority of the Collection NFT. + * + * This is used to compute the metadata delegate record when + * `isDelegated` is equal to `"metadataDelegate"`. + * + * @defaultValue `metaplex.identity().publicKey` + */ + collectionUpdateAuthority?: PublicKey; }; /** @@ -151,55 +169,92 @@ export const unverifyNftCollectionBuilder = ( isSizedCollection = true, isDelegated = false, collectionAuthority = metaplex.identity(), + collectionUpdateAuthority = metaplex.identity().publicKey, } = params; // Programs. + const systemProgram = metaplex.programs().getSystem(programs); const tokenMetadataProgram = metaplex.programs().getTokenMetadata(programs); - const accounts = { - metadata: metaplex.nfts().pdas().metadata({ - mint: mintAddress, - programs, - }), - collectionAuthority: collectionAuthority.publicKey, - payer: payer.publicKey, - collectionMint: collectionMintAddress, - collection: metaplex.nfts().pdas().metadata({ - mint: collectionMintAddress, - programs, - }), - collectionMasterEditionAccount: metaplex.nfts().pdas().masterEdition({ - mint: collectionMintAddress, - programs, - }), - collectionAuthorityRecord: isDelegated - ? metaplex.nfts().pdas().collectionAuthorityRecord({ + // Accounts. + const metadata = metaplex.nfts().pdas().metadata({ + mint: mintAddress, + programs, + }); + const collectionMetadata = metaplex.nfts().pdas().metadata({ + mint: collectionMintAddress, + programs, + }); + const collectionEdition = metaplex.nfts().pdas().masterEdition({ + mint: collectionMintAddress, + programs, + }); + + if (isDelegated === 'legacyDelegate' || isDelegated === true) { + const accounts = { + metadata, + collectionAuthority: collectionAuthority.publicKey, + payer: payer.publicKey, + collectionMint: collectionMintAddress, + collection: collectionMetadata, + collectionMasterEditionAccount: collectionEdition, + collectionAuthorityRecord: metaplex + .nfts() + .pdas() + .collectionAuthorityRecord({ mint: collectionMintAddress, collectionAuthority: collectionAuthority.publicKey, programs, - }) - : undefined, - }; + }), + }; - const instruction = isSizedCollection - ? createUnverifySizedCollectionItemInstruction( - accounts, - tokenMetadataProgram.address - ) - : createUnverifyCollectionInstruction( - accounts, - tokenMetadataProgram.address - ); + const instruction = isSizedCollection + ? createUnverifySizedCollectionItemInstruction( + accounts, + tokenMetadataProgram.address + ) + : createUnverifyCollectionInstruction( + accounts, + tokenMetadataProgram.address + ); - return ( - TransactionBuilder.make() + return TransactionBuilder.make() .setFeePayer(payer) - - // Unverify the collection. .add({ instruction, signers: [payer, collectionAuthority], key: params.instructionKey ?? 'unverifyCollection', - }) - ); + }); + } + + const delegateRecord = + isDelegated === 'metadataDelegate' + ? metaplex.nfts().pdas().metadataDelegateRecord({ + mint: collectionMintAddress, + type: 'CollectionV1', + updateAuthority: collectionUpdateAuthority, + delegate: collectionAuthority.publicKey, + programs, + }) + : undefined; + + return TransactionBuilder.make() + .setFeePayer(payer) + .add({ + instruction: createUnverifyInstruction( + { + authority: collectionAuthority.publicKey, + delegateRecord, + metadata, + collectionMint: collectionMintAddress, + collectionMetadata, + systemProgram: systemProgram.address, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + }, + { verificationArgs: VerificationArgs.CollectionV1 }, + tokenMetadataProgram.address + ), + signers: [collectionAuthority], + key: params.instructionKey ?? 'unverifyCollection', + }); }; diff --git a/packages/js/src/plugins/nftModule/operations/unverifyNftCreator.ts b/packages/js/src/plugins/nftModule/operations/unverifyNftCreator.ts index 4dd40034c..f8e170e02 100644 --- a/packages/js/src/plugins/nftModule/operations/unverifyNftCreator.ts +++ b/packages/js/src/plugins/nftModule/operations/unverifyNftCreator.ts @@ -1,5 +1,8 @@ -import { createRemoveCreatorVerificationInstruction } from '@metaplex-foundation/mpl-token-metadata'; -import { PublicKey } from '@solana/web3.js'; +import { + VerificationArgs, + createUnverifyInstruction, +} from '@metaplex-foundation/mpl-token-metadata'; +import { PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js'; import { SendAndConfirmTransactionResponse } from '../../rpcModule'; import { Metaplex } from '@/Metaplex'; import { @@ -124,6 +127,7 @@ export const unverifyNftCreatorBuilder = ( const { mintAddress, creator = metaplex.identity() } = params; // Programs. + const systemProgram = metaplex.programs().getSystem(programs); const tokenMetadataProgram = metaplex.programs().getTokenMetadata(programs); return ( @@ -132,14 +136,17 @@ export const unverifyNftCreatorBuilder = ( // Verify the creator. .add({ - instruction: createRemoveCreatorVerificationInstruction( + instruction: createUnverifyInstruction( { + authority: creator.publicKey, metadata: metaplex.nfts().pdas().metadata({ mint: mintAddress, programs, }), - creator: creator.publicKey, + systemProgram: systemProgram.address, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, }, + { verificationArgs: VerificationArgs.CreatorV1 }, tokenMetadataProgram.address ), signers: [creator], diff --git a/packages/js/src/plugins/nftModule/operations/verifyNftCollection.ts b/packages/js/src/plugins/nftModule/operations/verifyNftCollection.ts index 3dcbafced..38508fcac 100644 --- a/packages/js/src/plugins/nftModule/operations/verifyNftCollection.ts +++ b/packages/js/src/plugins/nftModule/operations/verifyNftCollection.ts @@ -1,8 +1,10 @@ import { + VerificationArgs, createVerifyCollectionInstruction, + createVerifyInstruction, createVerifySizedCollectionItemInstruction, } from '@metaplex-foundation/mpl-token-metadata'; -import { PublicKey } from '@solana/web3.js'; +import { PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js'; import { SendAndConfirmTransactionResponse } from '../../rpcModule'; import { Metaplex } from '@/Metaplex'; import { @@ -74,12 +76,28 @@ export type VerifyNftCollectionInput = { /** * Whether or not the provided `collectionAuthority` is a delegated - * collection authority, i.e. it was approved by the update authority - * using `metaplex.nfts().approveCollectionAuthority()`. + * collection authority, i.e. it was approved by the update authority. + * + * - `false` means the collection authority is the update authority of the collection. + * - `legacyDelegate` means the collection authority is a delegate that was approved + * using the legacy `metaplex.nfts().approveCollectionAuthority()` operation. + * - `metadataDelegate` means the collection authority is a delegate that was approved + * using the new `metaplex.nfts().delegate()` operation. + * - `true` is equivalent to `legacyDelegate` for backwards compatibility. * * @defaultValue `false` */ - isDelegated?: boolean; + isDelegated?: boolean | 'legacyDelegate' | 'metadataDelegate'; + + /** + * The update authority of the Collection NFT. + * + * This is used to compute the metadata delegate record when + * `isDelegated` is equal to `"metadataDelegate"`. + * + * @defaultValue `metaplex.identity().publicKey` + */ + collectionUpdateAuthority?: PublicKey; }; /** @@ -151,37 +169,45 @@ export const verifyNftCollectionBuilder = ( isSizedCollection = true, isDelegated = false, collectionAuthority = metaplex.identity(), + collectionUpdateAuthority = metaplex.identity().publicKey, } = params; // Programs. + const systemProgram = metaplex.programs().getSystem(programs); const tokenMetadataProgram = metaplex.programs().getTokenMetadata(programs); - const accounts = { - metadata: metaplex.nfts().pdas().metadata({ - mint: mintAddress, - programs, - }), - collectionAuthority: collectionAuthority.publicKey, - payer: payer.publicKey, - collectionMint: collectionMintAddress, - collection: metaplex.nfts().pdas().metadata({ - mint: collectionMintAddress, - programs, - }), - collectionMasterEditionAccount: metaplex.nfts().pdas().masterEdition({ - mint: collectionMintAddress, - programs, - }), - }; - - const instruction = isSizedCollection - ? createVerifySizedCollectionItemInstruction( - accounts, - tokenMetadataProgram.address - ) - : createVerifyCollectionInstruction(accounts, tokenMetadataProgram.address); + // Accounts. + const metadata = metaplex.nfts().pdas().metadata({ + mint: mintAddress, + programs, + }); + const collectionMetadata = metaplex.nfts().pdas().metadata({ + mint: collectionMintAddress, + programs, + }); + const collectionEdition = metaplex.nfts().pdas().masterEdition({ + mint: collectionMintAddress, + programs, + }); - if (isDelegated) { + if (isDelegated === 'legacyDelegate' || isDelegated === true) { + const accounts = { + metadata, + collectionAuthority: collectionAuthority.publicKey, + payer: payer.publicKey, + collectionMint: collectionMintAddress, + collection: collectionMetadata, + collectionMasterEditionAccount: collectionEdition, + }; + const instruction = isSizedCollection + ? createVerifySizedCollectionItemInstruction( + accounts, + tokenMetadataProgram.address + ) + : createVerifyCollectionInstruction( + accounts, + tokenMetadataProgram.address + ); instruction.keys.push({ pubkey: metaplex.nfts().pdas().collectionAuthorityRecord({ mint: collectionMintAddress, @@ -191,17 +217,45 @@ export const verifyNftCollectionBuilder = ( isWritable: false, isSigner: false, }); - } - return ( - TransactionBuilder.make() + return TransactionBuilder.make() .setFeePayer(payer) - - // Verify the collection. .add({ instruction, signers: [payer, collectionAuthority], key: params.instructionKey ?? 'verifyCollection', - }) - ); + }); + } + + const delegateRecord = + isDelegated === 'metadataDelegate' + ? metaplex.nfts().pdas().metadataDelegateRecord({ + mint: collectionMintAddress, + type: 'CollectionV1', + updateAuthority: collectionUpdateAuthority, + delegate: collectionAuthority.publicKey, + programs, + }) + : undefined; + + return TransactionBuilder.make() + .setFeePayer(payer) + .add({ + instruction: createVerifyInstruction( + { + authority: collectionAuthority.publicKey, + delegateRecord, + metadata, + collectionMint: collectionMintAddress, + collectionMetadata, + collectionMasterEdition: collectionEdition, + systemProgram: systemProgram.address, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + }, + { verificationArgs: VerificationArgs.CollectionV1 }, + tokenMetadataProgram.address + ), + signers: [collectionAuthority], + key: params.instructionKey ?? 'verifyCollection', + }); }; diff --git a/packages/js/src/plugins/nftModule/operations/verifyNftCreator.ts b/packages/js/src/plugins/nftModule/operations/verifyNftCreator.ts index 1a8a44244..d0f0be533 100644 --- a/packages/js/src/plugins/nftModule/operations/verifyNftCreator.ts +++ b/packages/js/src/plugins/nftModule/operations/verifyNftCreator.ts @@ -1,5 +1,8 @@ -import { createSignMetadataInstruction } from '@metaplex-foundation/mpl-token-metadata'; -import { PublicKey } from '@solana/web3.js'; +import { + VerificationArgs, + createVerifyInstruction, +} from '@metaplex-foundation/mpl-token-metadata'; +import { PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js'; import { SendAndConfirmTransactionResponse } from '../../rpcModule'; import { Metaplex } from '@/Metaplex'; import { @@ -124,6 +127,7 @@ export const verifyNftCreatorBuilder = ( const { mintAddress, creator = metaplex.identity() } = params; // Programs. + const systemProgram = metaplex.programs().getSystem(programs); const tokenMetadataProgram = metaplex.programs().getTokenMetadata(programs); return ( @@ -132,14 +136,17 @@ export const verifyNftCreatorBuilder = ( // Verify the creator. .add({ - instruction: createSignMetadataInstruction( + instruction: createVerifyInstruction( { + authority: creator.publicKey, metadata: metaplex.nfts().pdas().metadata({ mint: mintAddress, programs, }), - creator: creator.publicKey, + systemProgram: systemProgram.address, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, }, + { verificationArgs: VerificationArgs.CreatorV1 }, tokenMetadataProgram.address ), signers: [creator], diff --git a/packages/js/test/plugins/nftModule/deleteNft.test.ts b/packages/js/test/plugins/nftModule/deleteNft.test.ts index aebd5e730..67c61ef58 100644 --- a/packages/js/test/plugins/nftModule/deleteNft.test.ts +++ b/packages/js/test/plugins/nftModule/deleteNft.test.ts @@ -93,7 +93,7 @@ test('[nftModule] the update authority of an NFT cannot delete it', async (t: Te }); // Then we expect an error. - await assertThrows(t, promise, /InvalidOwner: Invalid Owner/); + await assertThrows(t, promise, /Invalid authority type/); // And the NFT accounts still exist. const accounts = await mx diff --git a/packages/js/test/plugins/nftModule/unverifyNftCollection.test.ts b/packages/js/test/plugins/nftModule/unverifyNftCollection.test.ts index d7eb761e0..6f85657bf 100644 --- a/packages/js/test/plugins/nftModule/unverifyNftCollection.test.ts +++ b/packages/js/test/plugins/nftModule/unverifyNftCollection.test.ts @@ -89,3 +89,56 @@ test('[nftModule] it can unverify the legacy collection of an NFT item', async ( }, } as unknown as Specifications); }); + +test('[nftModule] it can unverify the collection of an NFT item as a metadata delegate', async (t: Test) => { + // Given a Metaplex instance. + const mx = await metaplex(); + + // And an existing NFT with an verified collection. + const collectionAuthority = Keypair.generate(); + const collection = await createCollectionNft(mx, { + updateAuthority: collectionAuthority, + }); + const nft = await createNft(mx, { + collection: collection.address, + collectionAuthority, + }); + t.true(nft.collection, 'nft has a collection'); + t.true(nft.collection?.verified, 'nft collection is verified'); + await assertRefreshedCollectionHasSize(t, mx, collection, 1); + + // And a metadata delegate approved by the collection's update authority. + const collectionDelegate = Keypair.generate(); + await mx.nfts().delegate({ + nftOrSft: collection, + authority: collectionAuthority, + delegate: { + type: 'CollectionV1', + delegate: collectionDelegate.publicKey, + updateAuthority: collection.updateAuthorityAddress, + }, + }); + + // When the metadata delegate unverifies the collection. + await mx.nfts().unverifyCollection({ + mintAddress: nft.address, + collectionMintAddress: nft.collection!.address, + collectionAuthority: collectionDelegate, + collectionUpdateAuthority: collectionAuthority.publicKey, + isDelegated: 'metadataDelegate', + }); + + // Then the NFT collection should be unverified. + const updatedNft = await mx.nfts().refresh(nft); + spok(t, updatedNft, { + $topic: 'Updated Nft', + model: 'nft', + collection: { + address: spokSamePubkey(collection.address), + verified: false, + }, + } as unknown as Specifications); + + // And the collection should have the updated size. + await assertRefreshedCollectionHasSize(t, mx, collection, 0); +}); diff --git a/packages/js/test/plugins/nftModule/verifyNftCollection.test.ts b/packages/js/test/plugins/nftModule/verifyNftCollection.test.ts index e61c2a2f5..c0c338758 100644 --- a/packages/js/test/plugins/nftModule/verifyNftCollection.test.ts +++ b/packages/js/test/plugins/nftModule/verifyNftCollection.test.ts @@ -86,3 +86,55 @@ test('[nftModule] it can verify the legacy collection of an NFT item', async (t: }, } as unknown as Specifications); }); + +test('[nftModule] it can verify the collection of an NFT item as a metadata delegate', async (t: Test) => { + // Given a Metaplex instance. + const mx = await metaplex(); + + // And an existing NFT with an unverified collection. + const collectionAuthority = Keypair.generate(); + const collection = await createCollectionNft(mx, { + updateAuthority: collectionAuthority, + }); + const nft = await createNft(mx, { + collection: collection.address, + }); + t.true(nft.collection, 'nft has a collection'); + t.false(nft.collection?.verified, 'nft collection is not verified'); + await assertRefreshedCollectionHasSize(t, mx, collection, 0); + + // And a metadata delegate approved by the collection's update authority. + const collectionDelegate = Keypair.generate(); + await mx.nfts().delegate({ + nftOrSft: collection, + authority: collectionAuthority, + delegate: { + type: 'CollectionV1', + delegate: collectionDelegate.publicKey, + updateAuthority: collection.updateAuthorityAddress, + }, + }); + + // When the metadata delegate verifies the collection. + await mx.nfts().verifyCollection({ + mintAddress: nft.address, + collectionMintAddress: nft.collection!.address, + collectionAuthority: collectionDelegate, + collectionUpdateAuthority: collectionAuthority.publicKey, + isDelegated: 'metadataDelegate', + }); + + // Then the NFT collection should be verified. + const updatedNft = await mx.nfts().refresh(nft); + spok(t, updatedNft, { + $topic: 'Updated Nft', + model: 'nft', + collection: { + address: spokSamePubkey(collection.address), + verified: true, + }, + } as unknown as Specifications); + + // And the collection should have the updated size. + await assertRefreshedCollectionHasSize(t, mx, collection, 1); +});