From 8d0ac5baad4c78ab2bd82d51f91e4fa1dc156c09 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 26 Aug 2022 18:13:47 -0700 Subject: [PATCH] Adding `set_and_verify_collection` instruction to Bubblegum - Require that either the tree authority signed this transaction, or the tree authority is the collection update authority. - Add `payer` to both creator and collection verification instructions. - Regenerating API. - Renaming `bubblegum_program_authority` to `bubblegum_signer` and adding a PDA seed. - Some additional minor changes to existing logic. --- bubblegum/js/idl/bubblegum.json | 187 +++++++++++++- bubblegum/js/src/generated/errors/index.ts | 58 ++++- .../js/src/generated/instructions/index.ts | 1 + .../instructions/setAndVerifyCollection.ts | 188 ++++++++++++++ .../instructions/unverifyCollection.ts | 20 +- .../generated/instructions/unverifyCreator.ts | 7 + .../instructions/verifyCollection.ts | 20 +- .../generated/instructions/verifyCreator.ts | 7 + bubblegum/program/src/error.rs | 2 + bubblegum/program/src/lib.rs | 241 ++++++++++++------ bubblegum/program/src/state/mod.rs | 2 + token-metadata/program/src/instruction.rs | 4 +- token-metadata/program/src/processor.rs | 6 +- 13 files changed, 637 insertions(+), 106 deletions(-) create mode 100644 bubblegum/js/src/generated/instructions/setAndVerifyCollection.ts diff --git a/bubblegum/js/idl/bubblegum.json b/bubblegum/js/idl/bubblegum.json index fbe34e2e91..04b0772210 100644 --- a/bubblegum/js/idl/bubblegum.json +++ b/bubblegum/js/idl/bubblegum.json @@ -292,6 +292,11 @@ "isMut": false, "isSigner": false }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, { "name": "creator", "isMut": false, @@ -375,6 +380,11 @@ "isMut": false, "isSigner": false }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, { "name": "creator", "isMut": false, @@ -458,6 +468,20 @@ "isMut": false, "isSigner": false }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "treeDelegate", + "isMut": false, + "isSigner": false, + "docs": [ + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] + }, { "name": "collectionAuthority", "isMut": false, @@ -479,7 +503,7 @@ "isSigner": false }, { - "name": "bubblegumProgramAuthority", + "name": "bubblegumSigner", "isMut": false, "isSigner": false }, @@ -566,6 +590,20 @@ "isMut": false, "isSigner": false }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "treeDelegate", + "isMut": false, + "isSigner": false, + "docs": [ + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] + }, { "name": "collectionAuthority", "isMut": false, @@ -587,7 +625,7 @@ "isSigner": false }, { - "name": "bubblegumProgramAuthority", + "name": "bubblegumSigner", "isMut": false, "isSigner": false }, @@ -656,6 +694,137 @@ } ] }, + { + "name": "setAndVerifyCollection", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": false + }, + { + "name": "delegate", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "treeDelegate", + "isMut": false, + "isSigner": false, + "docs": [ + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] + }, + { + "name": "collectionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "collectionMint", + "isMut": false, + "isSigner": false + }, + { + "name": "collectionMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "editionAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "bubblegumSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "candyWrapper", + "isMut": false, + "isSigner": false + }, + { + "name": "gummyrollProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "merkleSlab", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMetadataProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "message", + "type": { + "defined": "MetadataArgs" + } + }, + { + "name": "collection", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, { "name": "transfer", "accounts": [ @@ -1727,17 +1896,27 @@ { "code": 6021, "name": "CollectionCannotBeVerifiedInThisInstruction", - "msg": "Cannont Verify Collection in this Instruction" + "msg": "Cannot Verify Collection in this Instruction" }, { "code": 6022, + "name": "CollectionNotFound", + "msg": "Collection Not Found on Metadata" + }, + { + "code": 6023, "name": "AlreadyVerified", "msg": "Collection item is already verified." }, { - "code": 6023, + "code": 6024, "name": "AlreadyUnverified", "msg": "Collection item is already unverified." + }, + { + "code": 6025, + "name": "UpdateAuthorityIncorrect", + "msg": "Incorrect leaf metadata update authority." } ], "metadata": { diff --git a/bubblegum/js/src/generated/errors/index.ts b/bubblegum/js/src/generated/errors/index.ts index f8cc935a59..8e76588aec 100644 --- a/bubblegum/js/src/generated/errors/index.ts +++ b/bubblegum/js/src/generated/errors/index.ts @@ -501,7 +501,7 @@ createErrorFromCodeLookup.set(0x1784, () => new IncorrectOwnerError()) createErrorFromNameLookup.set('IncorrectOwner', () => new IncorrectOwnerError()) /** - * CollectionCannotBeVerifiedInThisInstruction: 'Cannont Verify Collection in this Instruction' + * CollectionCannotBeVerifiedInThisInstruction: 'Cannot Verify Collection in this Instruction' * * @category Errors * @category generated @@ -510,7 +510,7 @@ export class CollectionCannotBeVerifiedInThisInstructionError extends Error { readonly code: number = 0x1785 readonly name: string = 'CollectionCannotBeVerifiedInThisInstruction' constructor() { - super('Cannont Verify Collection in this Instruction') + super('Cannot Verify Collection in this Instruction') if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace( this, @@ -529,6 +529,29 @@ createErrorFromNameLookup.set( () => new CollectionCannotBeVerifiedInThisInstructionError() ) +/** + * CollectionNotFound: 'Collection Not Found on Metadata' + * + * @category Errors + * @category generated + */ +export class CollectionNotFoundError extends Error { + readonly code: number = 0x1786 + readonly name: string = 'CollectionNotFound' + constructor() { + super('Collection Not Found on Metadata') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, CollectionNotFoundError) + } + } +} + +createErrorFromCodeLookup.set(0x1786, () => new CollectionNotFoundError()) +createErrorFromNameLookup.set( + 'CollectionNotFound', + () => new CollectionNotFoundError() +) + /** * AlreadyVerified: 'Collection item is already verified.' * @@ -536,7 +559,7 @@ createErrorFromNameLookup.set( * @category generated */ export class AlreadyVerifiedError extends Error { - readonly code: number = 0x1786 + readonly code: number = 0x1787 readonly name: string = 'AlreadyVerified' constructor() { super('Collection item is already verified.') @@ -546,7 +569,7 @@ export class AlreadyVerifiedError extends Error { } } -createErrorFromCodeLookup.set(0x1786, () => new AlreadyVerifiedError()) +createErrorFromCodeLookup.set(0x1787, () => new AlreadyVerifiedError()) createErrorFromNameLookup.set( 'AlreadyVerified', () => new AlreadyVerifiedError() @@ -559,7 +582,7 @@ createErrorFromNameLookup.set( * @category generated */ export class AlreadyUnverifiedError extends Error { - readonly code: number = 0x1787 + readonly code: number = 0x1788 readonly name: string = 'AlreadyUnverified' constructor() { super('Collection item is already unverified.') @@ -569,12 +592,35 @@ export class AlreadyUnverifiedError extends Error { } } -createErrorFromCodeLookup.set(0x1787, () => new AlreadyUnverifiedError()) +createErrorFromCodeLookup.set(0x1788, () => new AlreadyUnverifiedError()) createErrorFromNameLookup.set( 'AlreadyUnverified', () => new AlreadyUnverifiedError() ) +/** + * UpdateAuthorityIncorrect: 'Incorrect leaf metadata update authority.' + * + * @category Errors + * @category generated + */ +export class UpdateAuthorityIncorrectError extends Error { + readonly code: number = 0x1789 + readonly name: string = 'UpdateAuthorityIncorrect' + constructor() { + super('Incorrect leaf metadata update authority.') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, UpdateAuthorityIncorrectError) + } + } +} + +createErrorFromCodeLookup.set(0x1789, () => new UpdateAuthorityIncorrectError()) +createErrorFromNameLookup.set( + 'UpdateAuthorityIncorrect', + () => new UpdateAuthorityIncorrectError() +) + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/bubblegum/js/src/generated/instructions/index.ts b/bubblegum/js/src/generated/instructions/index.ts index f621527646..913e16e822 100644 --- a/bubblegum/js/src/generated/instructions/index.ts +++ b/bubblegum/js/src/generated/instructions/index.ts @@ -10,6 +10,7 @@ export * from './delegate' export * from './mintV1' export * from './redeem' export * from './requestMintAuthority' +export * from './setAndVerifyCollection' export * from './setTreeDelegate' export * from './transfer' export * from './unverifyCollection' diff --git a/bubblegum/js/src/generated/instructions/setAndVerifyCollection.ts b/bubblegum/js/src/generated/instructions/setAndVerifyCollection.ts new file mode 100644 index 0000000000..9bfe713f77 --- /dev/null +++ b/bubblegum/js/src/generated/instructions/setAndVerifyCollection.ts @@ -0,0 +1,188 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { MetadataArgs, metadataArgsBeet } from '../types/MetadataArgs' + +/** + * @category Instructions + * @category SetAndVerifyCollection + * @category generated + */ +export type SetAndVerifyCollectionInstructionArgs = { + root: number[] /* size: 32 */ + dataHash: number[] /* size: 32 */ + creatorHash: number[] /* size: 32 */ + nonce: beet.bignum + index: number + message: MetadataArgs + collection: number[] /* size: 32 */ +} +/** + * @category Instructions + * @category SetAndVerifyCollection + * @category generated + */ +export const setAndVerifyCollectionStruct = new beet.FixableBeetArgsStruct< + SetAndVerifyCollectionInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['root', beet.uniformFixedSizeArray(beet.u8, 32)], + ['dataHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['creatorHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['nonce', beet.u64], + ['index', beet.u32], + ['message', metadataArgsBeet], + ['collection', beet.uniformFixedSizeArray(beet.u8, 32)], + ], + 'SetAndVerifyCollectionInstructionArgs' +) +/** + * Accounts required by the _setAndVerifyCollection_ instruction + * + * @property [] authority + * @property [] owner + * @property [] delegate + * @property [**signer**] payer + * @property [] treeDelegate + * @property [**signer**] collectionAuthority + * @property [] collectionMint + * @property [] collectionMetadata + * @property [] editionAccount + * @property [] bubblegumSigner + * @property [] candyWrapper + * @property [] gummyrollProgram + * @property [_writable_] merkleSlab + * @property [] tokenMetadataProgram + * @category Instructions + * @category SetAndVerifyCollection + * @category generated + */ +export type SetAndVerifyCollectionInstructionAccounts = { + authority: web3.PublicKey + owner: web3.PublicKey + delegate: web3.PublicKey + payer: web3.PublicKey + treeDelegate: web3.PublicKey + collectionAuthority: web3.PublicKey + collectionMint: web3.PublicKey + collectionMetadata: web3.PublicKey + editionAccount: web3.PublicKey + bubblegumSigner: web3.PublicKey + candyWrapper: web3.PublicKey + gummyrollProgram: web3.PublicKey + merkleSlab: web3.PublicKey + tokenMetadataProgram: web3.PublicKey +} + +export const setAndVerifyCollectionInstructionDiscriminator = [ + 235, 242, 121, 216, 158, 234, 180, 234, +] + +/** + * Creates a _SetAndVerifyCollection_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category SetAndVerifyCollection + * @category generated + */ +export function createSetAndVerifyCollectionInstruction( + accounts: SetAndVerifyCollectionInstructionAccounts, + args: SetAndVerifyCollectionInstructionArgs, + programId = new web3.PublicKey('BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY') +) { + const [data] = setAndVerifyCollectionStruct.serialize({ + instructionDiscriminator: setAndVerifyCollectionInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.authority, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.owner, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.delegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.payer, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.treeDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.collectionAuthority, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.collectionMint, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.collectionMetadata, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.editionAccount, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.bubblegumSigner, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.candyWrapper, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.gummyrollProgram, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.merkleSlab, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.tokenMetadataProgram, + isWritable: false, + isSigner: false, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/bubblegum/js/src/generated/instructions/unverifyCollection.ts b/bubblegum/js/src/generated/instructions/unverifyCollection.ts index f83b0d531a..b7d2760039 100644 --- a/bubblegum/js/src/generated/instructions/unverifyCollection.ts +++ b/bubblegum/js/src/generated/instructions/unverifyCollection.ts @@ -49,11 +49,13 @@ export const unverifyCollectionStruct = new beet.FixableBeetArgsStruct< * @property [] authority * @property [] owner * @property [] delegate + * @property [**signer**] payer + * @property [] treeDelegate * @property [**signer**] collectionAuthority * @property [] collectionMint * @property [] collectionMetadata * @property [] editionAccount - * @property [] bubblegumProgramAuthority + * @property [] bubblegumSigner * @property [] candyWrapper * @property [] gummyrollProgram * @property [_writable_] merkleSlab @@ -66,11 +68,13 @@ export type UnverifyCollectionInstructionAccounts = { authority: web3.PublicKey owner: web3.PublicKey delegate: web3.PublicKey + payer: web3.PublicKey + treeDelegate: web3.PublicKey collectionAuthority: web3.PublicKey collectionMint: web3.PublicKey collectionMetadata: web3.PublicKey editionAccount: web3.PublicKey - bubblegumProgramAuthority: web3.PublicKey + bubblegumSigner: web3.PublicKey candyWrapper: web3.PublicKey gummyrollProgram: web3.PublicKey merkleSlab: web3.PublicKey @@ -116,6 +120,16 @@ export function createUnverifyCollectionInstruction( isWritable: false, isSigner: false, }, + { + pubkey: accounts.payer, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.treeDelegate, + isWritable: false, + isSigner: false, + }, { pubkey: accounts.collectionAuthority, isWritable: false, @@ -137,7 +151,7 @@ export function createUnverifyCollectionInstruction( isSigner: false, }, { - pubkey: accounts.bubblegumProgramAuthority, + pubkey: accounts.bubblegumSigner, isWritable: false, isSigner: false, }, diff --git a/bubblegum/js/src/generated/instructions/unverifyCreator.ts b/bubblegum/js/src/generated/instructions/unverifyCreator.ts index 9bd36311ab..995470a742 100644 --- a/bubblegum/js/src/generated/instructions/unverifyCreator.ts +++ b/bubblegum/js/src/generated/instructions/unverifyCreator.ts @@ -49,6 +49,7 @@ export const unverifyCreatorStruct = new beet.FixableBeetArgsStruct< * @property [] authority * @property [] owner * @property [] delegate + * @property [**signer**] payer * @property [**signer**] creator * @property [] candyWrapper * @property [] gummyrollProgram @@ -61,6 +62,7 @@ export type UnverifyCreatorInstructionAccounts = { authority: web3.PublicKey owner: web3.PublicKey delegate: web3.PublicKey + payer: web3.PublicKey creator: web3.PublicKey candyWrapper: web3.PublicKey gummyrollProgram: web3.PublicKey @@ -106,6 +108,11 @@ export function createUnverifyCreatorInstruction( isWritable: false, isSigner: false, }, + { + pubkey: accounts.payer, + isWritable: false, + isSigner: true, + }, { pubkey: accounts.creator, isWritable: false, diff --git a/bubblegum/js/src/generated/instructions/verifyCollection.ts b/bubblegum/js/src/generated/instructions/verifyCollection.ts index c207646476..cf6e2683a9 100644 --- a/bubblegum/js/src/generated/instructions/verifyCollection.ts +++ b/bubblegum/js/src/generated/instructions/verifyCollection.ts @@ -49,11 +49,13 @@ export const verifyCollectionStruct = new beet.FixableBeetArgsStruct< * @property [] authority * @property [] owner * @property [] delegate + * @property [**signer**] payer + * @property [] treeDelegate * @property [**signer**] collectionAuthority * @property [] collectionMint * @property [] collectionMetadata * @property [] editionAccount - * @property [] bubblegumProgramAuthority + * @property [] bubblegumSigner * @property [] candyWrapper * @property [] gummyrollProgram * @property [_writable_] merkleSlab @@ -66,11 +68,13 @@ export type VerifyCollectionInstructionAccounts = { authority: web3.PublicKey owner: web3.PublicKey delegate: web3.PublicKey + payer: web3.PublicKey + treeDelegate: web3.PublicKey collectionAuthority: web3.PublicKey collectionMint: web3.PublicKey collectionMetadata: web3.PublicKey editionAccount: web3.PublicKey - bubblegumProgramAuthority: web3.PublicKey + bubblegumSigner: web3.PublicKey candyWrapper: web3.PublicKey gummyrollProgram: web3.PublicKey merkleSlab: web3.PublicKey @@ -116,6 +120,16 @@ export function createVerifyCollectionInstruction( isWritable: false, isSigner: false, }, + { + pubkey: accounts.payer, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.treeDelegate, + isWritable: false, + isSigner: false, + }, { pubkey: accounts.collectionAuthority, isWritable: false, @@ -137,7 +151,7 @@ export function createVerifyCollectionInstruction( isSigner: false, }, { - pubkey: accounts.bubblegumProgramAuthority, + pubkey: accounts.bubblegumSigner, isWritable: false, isSigner: false, }, diff --git a/bubblegum/js/src/generated/instructions/verifyCreator.ts b/bubblegum/js/src/generated/instructions/verifyCreator.ts index c17d3b6b72..b681ad1dc2 100644 --- a/bubblegum/js/src/generated/instructions/verifyCreator.ts +++ b/bubblegum/js/src/generated/instructions/verifyCreator.ts @@ -49,6 +49,7 @@ export const verifyCreatorStruct = new beet.FixableBeetArgsStruct< * @property [] authority * @property [] owner * @property [] delegate + * @property [**signer**] payer * @property [**signer**] creator * @property [] candyWrapper * @property [] gummyrollProgram @@ -61,6 +62,7 @@ export type VerifyCreatorInstructionAccounts = { authority: web3.PublicKey owner: web3.PublicKey delegate: web3.PublicKey + payer: web3.PublicKey creator: web3.PublicKey candyWrapper: web3.PublicKey gummyrollProgram: web3.PublicKey @@ -106,6 +108,11 @@ export function createVerifyCreatorInstruction( isWritable: false, isSigner: false, }, + { + pubkey: accounts.payer, + isWritable: false, + isSigner: true, + }, { pubkey: accounts.creator, isWritable: false, diff --git a/bubblegum/program/src/error.rs b/bubblegum/program/src/error.rs index 3bae82ce21..a19808ba58 100644 --- a/bubblegum/program/src/error.rs +++ b/bubblegum/program/src/error.rs @@ -52,4 +52,6 @@ pub enum BubblegumError { AlreadyVerified, #[msg("Collection item is already unverified.")] AlreadyUnverified, + #[msg("Incorrect leaf metadata update authority.")] + UpdateAuthorityIncorrect, } diff --git a/bubblegum/program/src/lib.rs b/bubblegum/program/src/lib.rs index d63fbbd2fe..b3fa882edd 100644 --- a/bubblegum/program/src/lib.rs +++ b/bubblegum/program/src/lib.rs @@ -3,11 +3,11 @@ use { crate::state::metaplex_anchor::MplTokenMetadata, crate::state::{ leaf_schema::{LeafSchema, Version}, - metaplex_adapter::{Creator, MetadataArgs, TokenProgramVersion}, + metaplex_adapter::{self, Creator, MetadataArgs, TokenProgramVersion}, metaplex_anchor::{MasterEdition, TokenMetadata}, request::{MintRequest, MINT_REQUEST_SIZE}, - NFTDecompressionEvent, NewNFTEvent, TreeConfig, Voucher, ASSET_PREFIX, TREE_AUTHORITY_SIZE, - VOUCHER_PREFIX, VOUCHER_SIZE, + NFTDecompressionEvent, NewNFTEvent, TreeConfig, Voucher, ASSET_PREFIX, + COLLECTION_CPI_PREFIX, TREE_AUTHORITY_SIZE, VOUCHER_PREFIX, VOUCHER_SIZE, }, crate::utils::{ append_leaf, assert_metadata_is_mpl_compatible, assert_pubkey_equal, cmp_bytes, @@ -117,6 +117,7 @@ pub struct CreatorVerification<'info> { pub owner: UncheckedAccount<'info>, /// CHECK: This account is chekced in the instruction pub delegate: UncheckedAccount<'info>, + pub payer: Signer<'info>, pub creator: Signer<'info>, pub candy_wrapper: Program<'info, CandyWrapper>, pub gummyroll_program: Program<'info, Gummyroll>, @@ -136,17 +137,23 @@ pub struct CollectionVerification<'info> { pub owner: UncheckedAccount<'info>, /// CHECK: This account is checked in the instruction pub delegate: UncheckedAccount<'info>, + pub payer: Signer<'info>, + /// CHECK: This account is checked to be a signer in + /// the case of `set_and_verify_collection` where + /// we are actually changing the NFT metadata. + pub tree_delegate: UncheckedAccount<'info>, pub collection_authority: Signer<'info>, /// CHECK: This account is checked in the instruction pub collection_mint: UncheckedAccount<'info>, pub collection_metadata: Box>, - pub edition_account: Box>, + /// CHECK: This account is checked in the instruction + pub edition_account: UncheckedAccount<'info>, /// CHECK: This is just used as a signing PDA. #[account( - seeds = [], + seeds = [COLLECTION_CPI_PREFIX.as_ref()], bump, )] - pub bubblegum_program_authority: UncheckedAccount<'info>, + pub bubblegum_signer: UncheckedAccount<'info>, pub candy_wrapper: Program<'info, CandyWrapper>, pub gummyroll_program: Program<'info, Gummyroll>, #[account(mut)] @@ -716,15 +723,17 @@ fn process_collection_verification<'info>( index: u32, mut message: MetadataArgs, verify: bool, + new_collection: Option<[u8; 32]>, ) -> Result<()> { let owner = ctx.accounts.owner.to_account_info(); let delegate = ctx.accounts.delegate.to_account_info(); let merkle_slab = ctx.accounts.merkle_slab.to_account_info(); - let collection_metadata = &mut ctx.accounts.collection_metadata; + let collection_metadata = &ctx.accounts.collection_metadata; let collection_mint = ctx.accounts.collection_mint.to_account_info(); let edition_account = ctx.accounts.edition_account.to_account_info(); let collection_authority = ctx.accounts.collection_authority.to_account_info(); - let bubblegum_program_authority = ctx.accounts.bubblegum_program_authority.to_account_info(); + let bubblegum_signer = ctx.accounts.bubblegum_signer.to_account_info(); + let token_metadata_program = ctx.accounts.token_metadata_program.to_account_info(); // Look for collection authority record PDA as a remaining account. let collection_authority_record = if ctx.remaining_accounts.len() > 0 { @@ -751,6 +760,14 @@ fn process_collection_verification<'info>( let incoming_data_hash = hash_metadata(&message)?; assert_eq!(data_hash, incoming_data_hash); + // If new collection was provided, set it in the NFT metadata. + if new_collection.is_some() { + message.collection = new_collection.map(|c| metaplex_adapter::Collection { + verified: false, // Set to true below. + key: Pubkey::new(&c), + }); + } + // 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 @@ -777,90 +794,93 @@ fn process_collection_verification<'info>( collection_authority_record, )?; - // If this is a sized collection, then increment or decrement collection size. - if let Some(details) = &collection_metadata.collection_details { - // Increment or decrement existing size. - let new_size = match details { - CollectionDetails::V1 { size } => { - if verify { - size.checked_add(1) - .ok_or(BubblegumError::NumericalOverflowError)? - } else { - size.checked_sub(1) - .ok_or(BubblegumError::NumericalOverflowError)? - } - } - }; - - // CPI into to token-metadata program to change the collection size. - let mut bubblegum_set_collection_size_infos = vec![ - collection_metadata.to_account_info(), - collection_authority.clone(), - collection_mint.clone(), - bubblegum_program_authority.clone(), - ]; + // Update collection in metadata args. Note since this is a mutable reference, + // it is still updating `message.collection` after being destructured. + collection.verified = verify; + } else { + return Err(BubblegumError::CollectionNotFound.into()); + } - if let Some(record) = collection_authority_record { - bubblegum_set_collection_size_infos.push(record.clone()); + // If this is a sized collection, then increment or decrement collection size. + if let Some(details) = &collection_metadata.collection_details { + // Increment or decrement existing size. + let new_size = match details { + CollectionDetails::V1 { size } => { + if verify { + size.checked_add(1) + .ok_or(BubblegumError::NumericalOverflowError)? + } else { + size.checked_sub(1) + .ok_or(BubblegumError::NumericalOverflowError)? + } } + }; + + // CPI into to token-metadata program to change the collection size. + let mut bubblegum_set_collection_size_infos = vec![ + collection_metadata.to_account_info(), + collection_authority.clone(), + collection_mint.clone(), + bubblegum_signer.clone(), + ]; - invoke_signed( - &mpl_token_metadata::instruction::bubblegum_set_collection_size( - ctx.accounts.token_metadata_program.key(), - collection_metadata.to_account_info().key(), - collection_authority.key(), - collection_mint.key(), - bubblegum_program_authority.key(), - collection_authority_record.map(|r| r.key()), - new_size, - ), - bubblegum_set_collection_size_infos.as_slice(), - &[&[&[ctx.bumps["bubblegum_program_authority"]]]], - )?; + if let Some(record) = collection_authority_record { + bubblegum_set_collection_size_infos.push(record.clone()); } - // Update collection in metadata args. Note since this is a mutable reference, - // it is still updating `message.collection` after being destructured. - collection.verified = verify; + invoke_signed( + &mpl_token_metadata::instruction::bubblegum_set_collection_size( + token_metadata_program.key(), + collection_metadata.to_account_info().key(), + collection_authority.key(), + collection_mint.key(), + bubblegum_signer.key(), + collection_authority_record.map(|r| r.key()), + new_size, + ), + bubblegum_set_collection_size_infos.as_slice(), + &[&[ + COLLECTION_CPI_PREFIX.as_bytes(), + &[ctx.bumps["bubblegum_signer"]], + ]], + )?; + } - // Calculate new data hash. - let updated_data_hash = hash_metadata(&message)?; + // Calculate new data hash. + let updated_data_hash = hash_metadata(&message)?; - // Build previous leaf struct, new leaf struct, and replace the leaf in the tree. - let asset_id = get_asset_id(&merkle_slab.key(), nonce); - let previous_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - data_hash, - creator_hash, - ); - let new_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - updated_data_hash, - creator_hash, - ); - emit!(new_leaf.to_event()); - replace_leaf( - &merkle_slab.key(), - *ctx.bumps.get("authority").unwrap(), - &ctx.accounts.gummyroll_program.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.merkle_slab.to_account_info(), - &ctx.accounts.candy_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf.to_node(), - index, - ) - } else { - Err(BubblegumError::CollectionNotFound.into()) - } + // Build previous leaf struct, new leaf struct, and replace the leaf in the tree. + let asset_id = get_asset_id(&merkle_slab.key(), nonce); + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + data_hash, + creator_hash, + ); + let new_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + updated_data_hash, + creator_hash, + ); + emit!(new_leaf.to_event()); + replace_leaf( + &merkle_slab.key(), + *ctx.bumps.get("authority").unwrap(), + &ctx.accounts.gummyroll_program.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.merkle_slab.to_account_info(), + &ctx.accounts.candy_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf.to_node(), + index, + ) } #[program] @@ -1053,6 +1073,7 @@ pub mod bubblegum { index, message, true, + None, ) } @@ -1074,6 +1095,56 @@ pub mod bubblegum { index, message, false, + None, + ) + } + + pub fn set_and_verify_collection<'info>( + ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, + collection: [u8; 32], + ) -> Result<()> { + let incoming_tree_delegate = &ctx.accounts.tree_delegate; + let tree_creator = ctx.accounts.authority.creator; + let tree_delegate = ctx.accounts.authority.delegate; + let collection_metadata = &ctx.accounts.collection_metadata; + + // Require that either the tree authority signed this transaction, or the tree authority is + // the collection update authority which means the leaf update is approved via proxy, when + // we later call `assert_has_collection_authority()`. + // + // This is similar to logic in token-metadata for `set_and_verify_collection()` except + // this logic also allows the tree authority (which we are treating as the leaf metadata + // authority) to be different than the collection authority (actual or delegated). The + // token-metadata program required them to be the same. + let tree_authority_signed = incoming_tree_delegate.is_signer + && (incoming_tree_delegate.key() == tree_creator + || incoming_tree_delegate.key() == tree_delegate); + + let tree_authority_is_collection_update_authority = collection_metadata.update_authority + == tree_creator + || collection_metadata.update_authority == tree_delegate; + + require!( + tree_authority_signed || tree_authority_is_collection_update_authority, + BubblegumError::UpdateAuthorityIncorrect + ); + + process_collection_verification( + ctx, + root, + data_hash, + creator_hash, + nonce, + index, + message, + true, + Some(collection), ) } diff --git a/bubblegum/program/src/state/mod.rs b/bubblegum/program/src/state/mod.rs index f90d9c18f4..7d2ffffec5 100644 --- a/bubblegum/program/src/state/mod.rs +++ b/bubblegum/program/src/state/mod.rs @@ -12,6 +12,8 @@ pub const TREE_AUTHORITY_SIZE: usize = 88 + 8; pub const VOUCHER_SIZE: usize = 8 + 1 + 32 + 32 + 32 + 8 + 32 + 32 + 4 + 32; pub const VOUCHER_PREFIX: &str = "voucher"; pub const ASSET_PREFIX: &str = "asset"; +pub const COLLECTION_CPI_PREFIX: &str = "collection_cpi"; + #[account] #[derive(Copy)] pub struct TreeConfig { diff --git a/token-metadata/program/src/instruction.rs b/token-metadata/program/src/instruction.rs index 31c6fb2171..83c8e97697 100644 --- a/token-metadata/program/src/instruction.rs +++ b/token-metadata/program/src/instruction.rs @@ -1674,7 +1674,7 @@ pub fn bubblegum_set_collection_size( metadata_account: Pubkey, update_authority: Pubkey, mint: Pubkey, - bubblegum_program_authority: Pubkey, + bubblegum_signer: Pubkey, collection_authority_record: Option, size: u64, ) -> Instruction { @@ -1682,7 +1682,7 @@ pub fn bubblegum_set_collection_size( AccountMeta::new(metadata_account, false), AccountMeta::new_readonly(update_authority, true), AccountMeta::new_readonly(mint, false), - AccountMeta::new_readonly(bubblegum_program_authority, true), + AccountMeta::new_readonly(bubblegum_signer, true), ]; if let Some(record) = collection_authority_record { diff --git a/token-metadata/program/src/processor.rs b/token-metadata/program/src/processor.rs index 91fc4d0ee7..c5c55270a3 100644 --- a/token-metadata/program/src/processor.rs +++ b/token-metadata/program/src/processor.rs @@ -1906,7 +1906,7 @@ pub fn bubblegum_set_collection_size( let parent_nft_metadata_account_info = next_account_info(account_info_iter)?; let collection_update_authority_account_info = next_account_info(account_info_iter)?; let collection_mint_account_info = next_account_info(account_info_iter)?; - let bubblegum_program_authority_info = next_account_info(account_info_iter)?; + let bubblegum_signer_info = next_account_info(account_info_iter)?; let using_delegated_collection_authority = accounts.len() == 5; @@ -1916,8 +1916,8 @@ pub fn bubblegum_set_collection_size( } // This instruction can only be called by the Bubblegum program. - assert_owned_by(bubblegum_program_authority_info, &BUBBLEGUM_PROGRAM_ADDRESS)?; - assert_signer(bubblegum_program_authority_info)?; + assert_owned_by(bubblegum_signer_info, &BUBBLEGUM_PROGRAM_ADDRESS)?; + assert_signer(bubblegum_signer_info)?; // Owned by token-metadata program. assert_owned_by(parent_nft_metadata_account_info, program_id)?;