diff --git a/clients/js/test/printV1.test.ts b/clients/js/test/printV1.test.ts index 44d0d8ad..c0349334 100644 --- a/clients/js/test/printV1.test.ts +++ b/clients/js/test/printV1.test.ts @@ -1,10 +1,12 @@ import { createMintWithAssociatedToken, + findAssociatedTokenPda, setComputeUnitLimit, } from '@metaplex-foundation/mpl-toolbox'; import { generateSigner, percentAmount, + publicKey, some, transactionBuilder, } from '@metaplex-foundation/umi'; @@ -13,11 +15,14 @@ import { DigitalAsset, DigitalAssetWithToken, TokenStandard, + delegateSaleV1, fetchDigitalAsset, fetchDigitalAssetWithAssociatedToken, findMasterEditionPda, + findTokenRecordPda, printSupply, printV1, + thawDelegatedAccount, } from '../src'; import { createDigitalAssetWithToken, createUmi } from './_setup'; @@ -252,3 +257,64 @@ test('it cannot print a new edition if the initialized edition mint account has // Then we expect a program error. await t.throwsAsync(promise, { name: 'InvalidMintForTokenStandard' }); }); + +test('it cannot thaw the token on a pNFT Edition', async (t) => { + // Given an existing master edition PNFT. + const umi = await createUmi(); + const originalOwner = generateSigner(umi); + const originalMint = await createDigitalAssetWithToken(umi, { + name: 'My PNFT', + symbol: 'MPNFT', + uri: 'https://example.com/pnft.json', + sellerFeeBasisPoints: percentAmount(5.42), + tokenOwner: originalOwner.publicKey, + printSupply: printSupply('Limited', [10]), + tokenStandard: TokenStandard.ProgrammableNonFungible, + }); + const saleDelegate = generateSigner(umi); + + // When we print a new edition of the asset. + const editionMint = generateSigner(umi); + const editionOwner = generateSigner(umi); + const editionTokenAccount = findAssociatedTokenPda(umi, { + mint: editionMint.publicKey, + owner: editionOwner.publicKey, + }); + const editionTokenRecord = findTokenRecordPda(umi, { + mint: editionMint.publicKey, + token: publicKey(editionTokenAccount), + }); + await transactionBuilder() + .add(setComputeUnitLimit(umi, { units: 400_000 })) + .add( + printV1(umi, { + masterTokenAccountOwner: originalOwner, + masterEditionMint: originalMint.publicKey, + editionMint, + editionTokenAccountOwner: editionOwner.publicKey, + editionNumber: 1, + tokenStandard: TokenStandard.ProgrammableNonFungible, + }) + ) + .add( + delegateSaleV1(umi, { + delegate: saleDelegate.publicKey, + mint: editionMint.publicKey, + tokenOwner: editionOwner.publicKey, + authority: editionOwner, + tokenStandard: TokenStandard.ProgrammableNonFungibleEdition, + tokenRecord: editionTokenRecord, + }) + ) + .sendAndConfirm(umi); + + // Try to thaw the token. + const result = thawDelegatedAccount(umi, { + delegate: saleDelegate, + tokenAccount: editionTokenAccount, + mint: editionMint.publicKey, + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(result, { name: 'InvalidTokenStandard' }); +}); diff --git a/clients/js/test/printV2.test.ts b/clients/js/test/printV2.test.ts index 925cd150..1232f81b 100644 --- a/clients/js/test/printV2.test.ts +++ b/clients/js/test/printV2.test.ts @@ -1,10 +1,12 @@ import { createMintWithAssociatedToken, + findAssociatedTokenPda, setComputeUnitLimit, } from '@metaplex-foundation/mpl-toolbox'; import { generateSigner, percentAmount, + publicKey, sol, some, transactionBuilder, @@ -15,12 +17,15 @@ import { DigitalAssetWithToken, TokenStandard, delegatePrintDelegateV1, + delegateSaleV1, fetchDigitalAsset, fetchDigitalAssetWithAssociatedToken, findHolderDelegateRecordPda, findMasterEditionPda, + findTokenRecordPda, printSupply, printV2, + thawDelegatedAccount, } from '../src'; import { createDigitalAssetWithToken, createUmi } from './_setup'; @@ -657,3 +662,64 @@ test('it can delegate the authority to print a new edition with a separate payer }, }); }); + +test('it cannot thaw the token on a pNFT Edition', async (t) => { + // Given an existing master edition PNFT. + const umi = await createUmi(); + const originalOwner = generateSigner(umi); + const originalMint = await createDigitalAssetWithToken(umi, { + name: 'My PNFT', + symbol: 'MPNFT', + uri: 'https://example.com/pnft.json', + sellerFeeBasisPoints: percentAmount(5.42), + tokenOwner: originalOwner.publicKey, + printSupply: printSupply('Limited', [10]), + tokenStandard: TokenStandard.ProgrammableNonFungible, + }); + const saleDelegate = generateSigner(umi); + + // When we print a new edition of the asset. + const editionMint = generateSigner(umi); + const editionOwner = generateSigner(umi); + const editionTokenAccount = findAssociatedTokenPda(umi, { + mint: editionMint.publicKey, + owner: editionOwner.publicKey, + }); + const editionTokenRecord = findTokenRecordPda(umi, { + mint: editionMint.publicKey, + token: publicKey(editionTokenAccount), + }); + await transactionBuilder() + .add(setComputeUnitLimit(umi, { units: 400_000 })) + .add( + printV2(umi, { + masterTokenAccountOwner: originalOwner, + masterEditionMint: originalMint.publicKey, + editionMint, + editionTokenAccountOwner: editionOwner.publicKey, + editionNumber: 1, + tokenStandard: TokenStandard.ProgrammableNonFungible, + }) + ) + .add( + delegateSaleV1(umi, { + delegate: saleDelegate.publicKey, + mint: editionMint.publicKey, + tokenOwner: editionOwner.publicKey, + authority: editionOwner, + tokenStandard: TokenStandard.ProgrammableNonFungibleEdition, + tokenRecord: editionTokenRecord, + }) + ) + .sendAndConfirm(umi); + + // Try to thaw the token. + const result = thawDelegatedAccount(umi, { + delegate: saleDelegate, + tokenAccount: editionTokenAccount, + mint: editionMint.publicKey, + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(result, { name: 'InvalidTokenStandard' }); +}); diff --git a/programs/token-metadata/program/src/assertions/edition.rs b/programs/token-metadata/program/src/assertions/edition.rs index b8ab5129..be4c7b48 100644 --- a/programs/token-metadata/program/src/assertions/edition.rs +++ b/programs/token-metadata/program/src/assertions/edition.rs @@ -6,7 +6,9 @@ use spl_token_2022::state::Mint; use crate::{ error::MetadataError, pda::find_master_edition_account, - state::{TokenStandard, EDITION, PREFIX, TOKEN_STANDARD_INDEX}, + state::{ + Key, TokenStandard, EDITION, PREFIX, TOKEN_STANDARD_INDEX, TOKEN_STANDARD_INDEX_EDITION, + }, utils::unpack, }; @@ -22,12 +24,19 @@ pub fn assert_edition_is_not_mint_authority(mint_account_info: &AccountInfo) -> Ok(()) } -/// Checks that the `master_edition` is not a pNFT master edition. -pub fn assert_edition_is_not_programmable(master_edition_info: &AccountInfo) -> ProgramResult { - let edition_data = master_edition_info.data.borrow(); +/// Checks that the `edition` is not a pNFT master edition or edition. +pub fn assert_edition_is_not_programmable(edition_info: &AccountInfo) -> ProgramResult { + let edition_data = edition_info.data.borrow(); - if edition_data.len() > TOKEN_STANDARD_INDEX - && edition_data[TOKEN_STANDARD_INDEX] == TokenStandard::ProgrammableNonFungible as u8 + // Check if it's a master edition of a pNFT + if (edition_data.len() > TOKEN_STANDARD_INDEX + && edition_data[0] == Key::MasterEditionV2 as u8 + // Check if it's an edition of a pNFT + && (edition_data[TOKEN_STANDARD_INDEX] == TokenStandard::ProgrammableNonFungible as u8)) + || (edition_data.len() > TOKEN_STANDARD_INDEX_EDITION + && edition_data[0] == Key::EditionV1 as u8 + && edition_data[TOKEN_STANDARD_INDEX_EDITION] + == TokenStandard::ProgrammableNonFungible as u8) { return Err(MetadataError::InvalidTokenStandard.into()); }