Skip to content

Commit

Permalink
Use Data delegate (#107)
Browse files Browse the repository at this point in the history
* Require TM collection data delegate for update metadata

* Add program test

* Add js tests
  • Loading branch information
danenbm authored Sep 5, 2024
1 parent d560dd1 commit 3a05c49
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 6 deletions.
272 changes: 271 additions & 1 deletion clients/js/test/updateMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { createNft } from '@metaplex-foundation/mpl-token-metadata';
import {
createNft,
delegateDataV1,
TokenStandard,
findMetadataDelegateRecordPda,
MetadataDelegateRole,
delegateCollectionV1,
approveCollectionAuthority,
findCollectionAuthorityRecordPda,
} from '@metaplex-foundation/mpl-token-metadata';
import {
defaultPublicKey,
generateSigner,
Expand Down Expand Up @@ -307,6 +316,267 @@ test('it can update metadata using collection update authority when collection i
t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf));
});

test('it can update metadata using old collection authority when collection is verified', 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);

// And a Collection NFT.
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);

// 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,
collectionAuthority,
}).sendAndConfirm(umi);

// When we approve a collection authority record.
const newCollectionAuthority = generateSigner(umi);
let collectionAuthorityRecordPda = findCollectionAuthorityRecordPda(umi, {
mint: collectionMint.publicKey,
collectionAuthority: newCollectionAuthority.publicKey,
});

await approveCollectionAuthority(umi, {
collectionAuthorityRecord: collectionAuthorityRecordPda,
newCollectionAuthority: newCollectionAuthority.publicKey,
updateAuthority: collectionAuthority,
mint: collectionMint.publicKey,
}).sendAndConfirm(umi);

// And when metadata is updated.
const updateArgs: UpdateArgsArgs = {
name: some('New name'),
uri: some('https://updated-example.com/my-nft.json'),
};
await updateMetadata(umi, {
leafOwner,
merkleTree,
root: getCurrentRoot(merkleTreeAccount.tree),
nonce: 0,
index: 0,
currentMetadata: metadata,
proof: [],
updateArgs,
authority: newCollectionAuthority,
collectionMint: collectionMint.publicKey,
collectionAuthorityRecordPda,
}).sendAndConfirm(umi);

// Then the leaf was updated in the merkle tree.
const updatedLeaf = hashLeaf(umi, {
merkleTree,
owner: leafOwner,
leafIndex: 0,
metadata: {
...metadata,
name: 'New name',
uri: 'https://updated-example.com/my-nft.json',
},
});
merkleTreeAccount = await fetchMerkleTree(umi, merkleTree);
t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf));
});

test('it can update metadata using collection data delegate when collection is verified', 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);

// And a Collection NFT.
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);

// 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,
collectionAuthority,
}).sendAndConfirm(umi);

// When we approve a data delegate.
const dataDelegate = generateSigner(umi);
await delegateDataV1(umi, {
mint: collectionMint.publicKey,
authority: collectionAuthority,
delegate: dataDelegate.publicKey,
tokenStandard: TokenStandard.NonFungible,
}).sendAndConfirm(umi);

let delegateRecordPda = findMetadataDelegateRecordPda(umi, {
mint: collectionMint.publicKey,
delegateRole: MetadataDelegateRole.Data,
delegate: dataDelegate.publicKey,
updateAuthority: collectionAuthority.publicKey,
});

// And when metadata is updated.
const updateArgs: UpdateArgsArgs = {
name: some('New name'),
uri: some('https://updated-example.com/my-nft.json'),
};
await updateMetadata(umi, {
leafOwner,
merkleTree,
root: getCurrentRoot(merkleTreeAccount.tree),
nonce: 0,
index: 0,
currentMetadata: metadata,
proof: [],
updateArgs,
authority: dataDelegate,
collectionMint: collectionMint.publicKey,
collectionAuthorityRecordPda: delegateRecordPda,
}).sendAndConfirm(umi);

// Then the leaf was updated in the merkle tree.
const updatedLeaf = hashLeaf(umi, {
merkleTree,
owner: leafOwner,
leafIndex: 0,
metadata: {
...metadata,
name: 'New name',
uri: 'https://updated-example.com/my-nft.json',
},
});
merkleTreeAccount = await fetchMerkleTree(umi, merkleTree);
t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf));
});

test('it cannot update metadata using collection collection delegate when collection is verified', 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);

// And a Collection NFT.
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);

// 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,
collectionAuthority,
}).sendAndConfirm(umi);

// When we approve a data delegate.
const collectionDelegate = generateSigner(umi);
await delegateCollectionV1(umi, {
mint: collectionMint.publicKey,
authority: collectionAuthority,
delegate: collectionDelegate.publicKey,
tokenStandard: TokenStandard.NonFungible,
}).sendAndConfirm(umi);

let delegate_record_pda = findMetadataDelegateRecordPda(umi, {
mint: collectionMint.publicKey,
delegateRole: MetadataDelegateRole.Collection,
delegate: collectionDelegate.publicKey,
updateAuthority: collectionAuthority.publicKey,
});

// And when metadata is updated.
const updateArgs: UpdateArgsArgs = {
name: some('New name'),
uri: some('https://updated-example.com/my-nft.json'),
};
const promise = updateMetadata(umi, {
leafOwner,
merkleTree,
root: getCurrentRoot(merkleTreeAccount.tree),
nonce: 0,
index: 0,
currentMetadata: metadata,
proof: [],
updateArgs,
authority: collectionDelegate,
collectionMint: collectionMint.publicKey,
collectionAuthorityRecordPda: delegate_record_pda,
}).sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidDelegateRecord' });

// And the leaf was not updated in the merkle tree.
const notUpdatedLeaf = hashLeaf(umi, {
merkleTree,
owner: leafOwner,
leafIndex: 0,
metadata,
});
merkleTreeAccount = await fetchMerkleTree(umi, merkleTree);
t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(notUpdatedLeaf));
});

test('it can update metadata using the getAssetWithProof helper with verified collection', async (t) => {
// Given we increase the timeout for this test.
t.timeout(20000);
Expand Down
3 changes: 2 additions & 1 deletion programs/bubblegum/program/src/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub fn assert_has_collection_authority(
mint: &Pubkey,
collection_authority: &Pubkey,
delegate_record: Option<&AccountInfo>,
metadata_delegate_role: MetadataDelegateRole,
) -> Result<()> {
// Mint is the correct one for the metadata account.
if collection_data.mint != *mint {
Expand All @@ -112,7 +113,7 @@ pub fn assert_has_collection_authority(
let (ca_pda, ca_bump) = CollectionAuthorityRecord::find_pda(mint, collection_authority);
let (md_pda, md_bump) = MetadataDelegateRecord::find_pda(
mint,
MetadataDelegateRole::Collection,
metadata_delegate_role,
&collection_data.update_authority,
collection_authority,
);
Expand Down
2 changes: 2 additions & 0 deletions programs/bubblegum/program/src/processor/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anchor_lang::prelude::*;
use mpl_token_metadata::types::MetadataDelegateRole;
use solana_program::{account_info::AccountInfo, pubkey::Pubkey};
use spl_account_compression::wrap_application_data_v1;

Expand Down Expand Up @@ -197,6 +198,7 @@ fn process_collection_verification_mpl_only<'info>(
collection_mint.key,
collection_authority.key,
collection_authority_record,
MetadataDelegateRole::Collection,
)?;

// Update collection in metadata args. Note since this is a mutable reference,
Expand Down
2 changes: 2 additions & 0 deletions programs/bubblegum/program/src/processor/update_metadata.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anchor_lang::prelude::*;
use mpl_token_metadata::types::MetadataDelegateRole;
use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop};

use crate::{
Expand Down Expand Up @@ -85,6 +86,7 @@ fn assert_authority_matches_collection<'info>(
collection_mint.key,
collection_authority.key,
collection_authority_record.as_ref(),
MetadataDelegateRole::Data,
)?;

Ok(())
Expand Down
Loading

0 comments on commit 3a05c49

Please sign in to comment.