diff --git a/bubblegum/js/idl/bubblegum.json b/bubblegum/js/idl/bubblegum.json index c49cfd7535..99313382da 100644 --- a/bubblegum/js/idl/bubblegum.json +++ b/bubblegum/js/idl/bubblegum.json @@ -8,7 +8,16 @@ { "name": "treeAuthority", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "merkleTree", @@ -64,7 +73,19 @@ { "name": "treeAuthority", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + }, + "relations": [ + "tree_creator" + ] }, { "name": "treeCreator", @@ -95,7 +116,16 @@ { "name": "treeAuthority", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -153,7 +183,16 @@ { "name": "treeAuthority", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -188,7 +227,11 @@ { "name": "collectionAuthorityRecordPda", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "If there is no collecton authority record PDA then", + "this must be the Bubblegum program address." + ] }, { "name": "collectionMint", @@ -208,7 +251,16 @@ { "name": "bubblegumSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "collection_cpi" + } + ] + } }, { "name": "logWrapper", @@ -246,7 +298,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -339,7 +400,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -432,7 +502,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -457,7 +536,11 @@ { "name": "treeDelegate", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] }, { "name": "collectionAuthority", @@ -467,7 +550,11 @@ { "name": "collectionAuthorityRecordPda", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "If there is no collecton authority record PDA then", + "this must be the Bubblegum program address." + ] }, { "name": "collectionMint", @@ -487,7 +574,16 @@ { "name": "bubblegumSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "collection_cpi" + } + ] + } }, { "name": "logWrapper", @@ -560,7 +656,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -585,7 +690,11 @@ { "name": "treeDelegate", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] }, { "name": "collectionAuthority", @@ -595,7 +704,11 @@ { "name": "collectionAuthorityRecordPda", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "If there is no collecton authority record PDA then", + "this must be the Bubblegum program address." + ] }, { "name": "collectionMint", @@ -615,7 +728,16 @@ { "name": "bubblegumSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "collection_cpi" + } + ] + } }, { "name": "logWrapper", @@ -688,7 +810,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -713,7 +844,11 @@ { "name": "treeDelegate", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] }, { "name": "collectionAuthority", @@ -723,7 +858,11 @@ { "name": "collectionAuthorityRecordPda", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "If there is no collecton authority record PDA then", + "this must be the Bubblegum program address." + ] }, { "name": "collectionMint", @@ -743,7 +882,16 @@ { "name": "bubblegumSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "collection_cpi" + } + ] + } }, { "name": "logWrapper", @@ -815,11 +963,45 @@ ] }, { - "name": "transfer", + "name": "updateMetadata", "accounts": [ { "name": "treeAuthority", "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } + }, + { + "name": "treeDelegate", + "isMut": false, + "isSigner": true + }, + { + "name": "collectionAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "collectionMint", + "isMut": false, + "isSigner": false + }, + { + "name": "collectionMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "collectionAuthorityRecordPda", + "isMut": false, "isSigner": false }, { @@ -832,6 +1014,126 @@ "isMut": false, "isSigner": false }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "logWrapper", + "isMut": false, + "isSigner": false + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "oldMetadata", + "type": { + "defined": "MetadataArgs" + } + }, + { + "name": "newName", + "type": { + "option": "string" + } + }, + { + "name": "newSymbol", + "type": { + "option": "string" + } + }, + { + "name": "newUri", + "type": { + "option": "string" + } + }, + { + "name": "newSellerFeeBasisPoints", + "type": { + "option": "u16" + } + }, + { + "name": "newPrimarySaleHappened", + "type": { + "option": "bool" + } + }, + { + "name": "newIsMutable", + "type": { + "option": "bool" + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + } + ] + }, + { + "name": "transfer", + "accounts": [ + { + "name": "treeAuthority", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } + }, + { + "name": "leafOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "leafDelegate", + "isMut": false, + "isSigner": false + }, { "name": "newLeafOwner", "isMut": false, @@ -902,7 +1204,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -984,7 +1295,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -1061,7 +1381,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -1081,7 +1410,26 @@ { "name": "voucher", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "voucher" + }, + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + }, + { + "kind": "arg", + "type": "u64", + "path": "nonce" + } + ] + } }, { "name": "logWrapper", @@ -1143,7 +1491,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -1158,7 +1515,29 @@ { "name": "voucher", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "voucher" + }, + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + }, + { + "kind": "account", + "type": { + "defined": "LeafSchema" + }, + "account": "Voucher", + "path": "voucher.leaf_schema" + } + ] + } }, { "name": "logWrapper", @@ -1194,7 +1573,30 @@ { "name": "voucher", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "voucher" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Voucher", + "path": "voucher.merkle_tree" + }, + { + "kind": "account", + "type": { + "defined": "LeafSchema" + }, + "account": "Voucher", + "path": "voucher.leaf_schema" + } + ] + } }, { "name": "leafOwner", @@ -1209,12 +1611,44 @@ { "name": "mint", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "asset" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Voucher", + "path": "voucher.merkle_tree" + }, + { + "kind": "account", + "type": { + "defined": "LeafSchema" + }, + "account": "Voucher", + "path": "voucher.leaf_schema" + } + ] + } }, { "name": "mintAuthority", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "mint" + } + ] + } }, { "name": "metadata", @@ -1272,7 +1706,16 @@ { "name": "treeAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "merkle_tree" + } + ] + } }, { "name": "leafOwner", @@ -1461,18 +1904,30 @@ "fields": [ { "name": "name", + "docs": [ + "The name of the asset" + ], "type": "string" }, { "name": "symbol", + "docs": [ + "The symbol for the asset" + ], "type": "string" }, { "name": "uri", + "docs": [ + "URI pointing to JSON representing the asset" + ], "type": "string" }, { "name": "sellerFeeBasisPoints", + "docs": [ + "Royalty basis points that goes to creators in secondary sales (0-10000)" + ], "type": "u16" }, { @@ -1485,12 +1940,18 @@ }, { "name": "editionNonce", + "docs": [ + "nonce for easy calculation of editions, if present" + ], "type": { "option": "u8" } }, { "name": "tokenStandard", + "docs": [ + "Since we cannot easily change Metadata, we add the new DataV2 fields here at the end." + ], "type": { "option": { "defined": "TokenStandard" @@ -1499,6 +1960,9 @@ }, { "name": "collection", + "docs": [ + "Collection" + ], "type": { "option": { "defined": "Collection" @@ -1507,6 +1971,9 @@ }, { "name": "uses", + "docs": [ + "Uses" + ], "type": { "option": { "defined": "Uses" @@ -1705,6 +2172,9 @@ }, { "name": "MintToCollectionV1" + }, + { + "name": "UpdateMetadata" } ] } @@ -1845,6 +2315,16 @@ "code": 6026, "name": "CollectionMustBeSized", "msg": "Collection Not Compatable with Compression, Must be Sized" + }, + { + "code": 6027, + "name": "MetadataImmutable", + "msg": "Metadata Not Mutable" + }, + { + "code": 6028, + "name": "CollectionMismatch", + "msg": "Collection mismatch" } ], "metadata": { diff --git a/bubblegum/js/src/generated/errors/index.ts b/bubblegum/js/src/generated/errors/index.ts index f8df0bf115..cb56e45b67 100644 --- a/bubblegum/js/src/generated/errors/index.ts +++ b/bubblegum/js/src/generated/errors/index.ts @@ -569,6 +569,46 @@ export class CollectionMustBeSizedError extends Error { createErrorFromCodeLookup.set(0x178a, () => new CollectionMustBeSizedError()); createErrorFromNameLookup.set('CollectionMustBeSized', () => new CollectionMustBeSizedError()); +/** + * MetadataImmutable: 'Metadata Not Mutable' + * + * @category Errors + * @category generated + */ +export class MetadataImmutableError extends Error { + readonly code: number = 0x178b; + readonly name: string = 'MetadataImmutable'; + constructor() { + super('Metadata Not Mutable'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, MetadataImmutableError); + } + } +} + +createErrorFromCodeLookup.set(0x178b, () => new MetadataImmutableError()); +createErrorFromNameLookup.set('MetadataImmutable', () => new MetadataImmutableError()); + +/** + * CollectionMismatch: 'Collection mismatch' + * + * @category Errors + * @category generated + */ +export class CollectionMismatchError extends Error { + readonly code: number = 0x178c; + readonly name: string = 'CollectionMismatch'; + constructor() { + super('Collection mismatch'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, CollectionMismatchError); + } + } +} + +createErrorFromCodeLookup.set(0x178c, () => new CollectionMismatchError()); +createErrorFromNameLookup.set('CollectionMismatch', () => new CollectionMismatchError()); + /** * 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 71ee2b26ce..6752c4a13b 100644 --- a/bubblegum/js/src/generated/instructions/index.ts +++ b/bubblegum/js/src/generated/instructions/index.ts @@ -12,5 +12,6 @@ export * from './setTreeDelegate'; export * from './transfer'; export * from './unverifyCollection'; export * from './unverifyCreator'; +export * from './updateMetadata'; export * from './verifyCollection'; export * from './verifyCreator'; diff --git a/bubblegum/js/src/generated/instructions/updateMetadata.ts b/bubblegum/js/src/generated/instructions/updateMetadata.ts new file mode 100644 index 0000000000..97ad40182e --- /dev/null +++ b/bubblegum/js/src/generated/instructions/updateMetadata.ts @@ -0,0 +1,198 @@ +/** + * 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 UpdateMetadata + * @category generated + */ +export type UpdateMetadataInstructionArgs = { + root: number[] /* size: 32 */; + oldMetadata: MetadataArgs; + newName: beet.COption; + newSymbol: beet.COption; + newUri: beet.COption; + newSellerFeeBasisPoints: beet.COption; + newPrimarySaleHappened: beet.COption; + newIsMutable: beet.COption; + nonce: beet.bignum; + index: number; +}; +/** + * @category Instructions + * @category UpdateMetadata + * @category generated + */ +export const updateMetadataStruct = new beet.FixableBeetArgsStruct< + UpdateMetadataInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */; + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['root', beet.uniformFixedSizeArray(beet.u8, 32)], + ['oldMetadata', metadataArgsBeet], + ['newName', beet.coption(beet.utf8String)], + ['newSymbol', beet.coption(beet.utf8String)], + ['newUri', beet.coption(beet.utf8String)], + ['newSellerFeeBasisPoints', beet.coption(beet.u16)], + ['newPrimarySaleHappened', beet.coption(beet.bool)], + ['newIsMutable', beet.coption(beet.bool)], + ['nonce', beet.u64], + ['index', beet.u32], + ], + 'UpdateMetadataInstructionArgs', +); +/** + * Accounts required by the _updateMetadata_ instruction + * + * @property [] treeAuthority + * @property [**signer**] treeDelegate + * @property [] collectionAuthority + * @property [] collectionMint + * @property [] collectionMetadata + * @property [] collectionAuthorityRecordPda + * @property [] leafOwner + * @property [] leafDelegate + * @property [**signer**] payer + * @property [_writable_] merkleTree + * @property [] logWrapper + * @property [] compressionProgram + * @property [] tokenMetadataProgram + * @category Instructions + * @category UpdateMetadata + * @category generated + */ +export type UpdateMetadataInstructionAccounts = { + treeAuthority: web3.PublicKey; + treeDelegate: web3.PublicKey; + collectionAuthority: web3.PublicKey; + collectionMint: web3.PublicKey; + collectionMetadata: web3.PublicKey; + collectionAuthorityRecordPda: web3.PublicKey; + leafOwner: web3.PublicKey; + leafDelegate: web3.PublicKey; + payer: web3.PublicKey; + merkleTree: web3.PublicKey; + logWrapper: web3.PublicKey; + compressionProgram: web3.PublicKey; + tokenMetadataProgram: web3.PublicKey; + systemProgram?: web3.PublicKey; + anchorRemainingAccounts?: web3.AccountMeta[]; +}; + +export const updateMetadataInstructionDiscriminator = [170, 182, 43, 239, 97, 78, 225, 186]; + +/** + * Creates a _UpdateMetadata_ 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 UpdateMetadata + * @category generated + */ +export function createUpdateMetadataInstruction( + accounts: UpdateMetadataInstructionAccounts, + args: UpdateMetadataInstructionArgs, + programId = new web3.PublicKey('BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY'), +) { + const [data] = updateMetadataStruct.serialize({ + instructionDiscriminator: updateMetadataInstructionDiscriminator, + ...args, + }); + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.treeAuthority, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.treeDelegate, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.collectionAuthority, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.collectionMint, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.collectionMetadata, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.collectionAuthorityRecordPda, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.leafOwner, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.leafDelegate, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.payer, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.merkleTree, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.logWrapper, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.compressionProgram, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.tokenMetadataProgram, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ]; + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc); + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }); + return ix; +} diff --git a/bubblegum/js/src/generated/types/InstructionName.ts b/bubblegum/js/src/generated/types/InstructionName.ts index 16f5f4f58a..5ea8bf7097 100644 --- a/bubblegum/js/src/generated/types/InstructionName.ts +++ b/bubblegum/js/src/generated/types/InstructionName.ts @@ -27,6 +27,7 @@ export enum InstructionName { UnverifyCollection, SetAndVerifyCollection, MintToCollectionV1, + UpdateMetadata, } /** diff --git a/bubblegum/program/src/error.rs b/bubblegum/program/src/error.rs index 74cc570c98..7acf60f793 100644 --- a/bubblegum/program/src/error.rs +++ b/bubblegum/program/src/error.rs @@ -56,4 +56,8 @@ pub enum BubblegumError { LeafAuthorityMustSign, #[msg("Collection Not Compatable with Compression, Must be Sized")] CollectionMustBeSized, + #[msg("Metadata Not Mutable")] + MetadataImmutable, + #[msg("Collection mismatch")] + CollectionMismatch, } diff --git a/bubblegum/program/src/lib.rs b/bubblegum/program/src/lib.rs index 531cd9202d..77bcb172c5 100644 --- a/bubblegum/program/src/lib.rs +++ b/bubblegum/program/src/lib.rs @@ -229,6 +229,36 @@ pub struct Transfer<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct UpdateMetadata<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfig>, + pub tree_delegate: Signer<'info>, + /// CHECK: This account is checked in the instruction + pub collection_authority: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub collection_mint: UncheckedAccount<'info>, + pub collection_metadata: Box>, + /// CHECK: This account is checked in the instruction + pub collection_authority_record_pda: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: UncheckedAccount<'info>, + /// CHECK: This account is chekced in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + pub payer: Signer<'info>, + #[account(mut)] + /// CHECK: This account is modified in the downstream program + pub merkle_tree: UncheckedAccount<'info>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub token_metadata_program: Program<'info, MplTokenMetadata>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct Delegate<'info> { #[account( @@ -468,6 +498,7 @@ pub enum InstructionName { UnverifyCollection, SetAndVerifyCollection, MintToCollectionV1, + UpdateMetadata, } pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { @@ -492,6 +523,7 @@ pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { [56, 113, 101, 253, 79, 55, 122, 169] => InstructionName::VerifyCollection, [250, 251, 42, 106, 41, 137, 186, 168] => InstructionName::UnverifyCollection, [235, 242, 121, 216, 158, 234, 180, 234] => InstructionName::SetAndVerifyCollection, + [170, 182, 43, 239, 97, 78, 225, 186] => InstructionName::UpdateMetadata, _ => InstructionName::Unknown, } @@ -518,13 +550,7 @@ fn process_mint_v1<'info>( } } - // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace - // instructions, without passing the entire, un-hashed MetadataArgs struct - let metadata_args_hash = keccak::hashv(&[message.try_to_vec()?.as_slice()]); - let data_hash = keccak::hashv(&[ - &metadata_args_hash.to_bytes(), - &message.seller_fee_basis_points.to_le_bytes(), - ]); + let data_hash = hash_metadata(&message)?; // Use the metadata auth to check whether we can allow `verified` to be set to true in the // creator Vec. @@ -555,7 +581,7 @@ fn process_mint_v1<'info>( owner, delegate, authority.num_minted, - data_hash.to_bytes(), + data_hash, creator_hash.to_bytes(), ); @@ -673,6 +699,41 @@ fn process_creator_verification<'info>( ) } +// TODO sorend: simplify with the function below +fn assert_signed_by_collection_authority<'info>( + collection_metadata: &Account<'info, TokenMetadata>, + collection_mint: &AccountInfo<'info>, + collection_authority: &AccountInfo<'info>, + collection_authority_record_pda: &AccountInfo<'info>, + token_metadata_program: &AccountInfo<'info>, +) -> Result<()> { + // See if a collection authority record PDA was provided. + let collection_authority_record = if collection_authority_record_pda.key() == crate::id() { + None + } else { + Some(collection_authority_record_pda) + }; + + // Verify correct account ownerships. + require!( + *collection_metadata.to_account_info().owner == token_metadata_program.key(), + BubblegumError::IncorrectOwner + ); + require!( + *collection_mint.owner == spl_token::id(), + BubblegumError::IncorrectOwner + ); + + // Collection authority assert from token-metadata. + assert_has_collection_authority( + collection_authority, + collection_metadata, + collection_mint.key, + collection_authority_record, + )?; + Ok(()) +} + fn process_collection_verification_mpl_only<'info>( collection_metadata: &Account<'info, TokenMetadata>, collection_mint: &AccountInfo<'info>, @@ -1192,6 +1253,137 @@ pub mod bubblegum { ) } + pub fn update_metadata<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateMetadata<'info>>, + root: [u8; 32], + old_metadata: MetadataArgs, + new_name: Option, + new_symbol: Option, + new_uri: Option, + new_seller_fee_basis_points: Option, + new_primary_sale_happened: Option, + new_is_mutable: Option, + nonce: u64, + index: u32, + ) -> Result<()> { + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + let owner = ctx.accounts.leaf_owner.to_account_info(); + let delegate = ctx.accounts.leaf_delegate.to_account_info(); + let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); + let authority = &mut ctx.accounts.tree_authority; + let tree_creator = authority.tree_creator; + let tree_delegate = authority.tree_delegate; + if !authority.is_public { + require!( + incoming_tree_delegate == tree_creator || incoming_tree_delegate == tree_delegate, + BubblegumError::TreeAuthorityIncorrect, + ); + } + // Old metadata must be mutable to allow metadata update + require!(old_metadata.is_mutable, BubblegumError::MetadataImmutable); + + // If the metadata of the NFT is verified with a collection, then ensure a collection authority + // has signed the tx to confirm the metadata update. + if let Some(collection) = &old_metadata.collection { + if collection.verified { + // Verified collection must match key passed to instruction + require!( + ctx.accounts.collection_mint.key() == collection.key, + BubblegumError::CollectionMismatch + ); + let collection_metadata = &ctx.accounts.collection_metadata; + let collection_mint = ctx.accounts.collection_mint.to_account_info(); + let collection_authority = ctx.accounts.collection_authority.to_account_info(); + let collection_authority_record_pda = ctx + .accounts + .collection_authority_record_pda + .to_account_info(); + let token_metadata_program = ctx.accounts.token_metadata_program.to_account_info(); + assert_signed_by_collection_authority( + collection_metadata, + &collection_mint, + &collection_authority, + &collection_authority_record_pda, + &token_metadata_program, + )?; + } + } + + let old_data_hash = hash_metadata(&old_metadata)?; + let creator_hash = hash_creators(&old_metadata.creators)?; + + // Update metadata + let mut new_metadata = old_metadata; + match new_name { + None => {} + Some(name) => new_metadata.name = name, + }; + match new_symbol { + None => {} + Some(symbol) => new_metadata.symbol = symbol, + }; + match new_uri { + None => {} + Some(uri) => new_metadata.uri = uri, + }; + match new_seller_fee_basis_points { + None => {} + Some(seller_fee_basis_points) => { + new_metadata.seller_fee_basis_points = seller_fee_basis_points + } + }; + match new_primary_sale_happened { + None => {} + Some(primary_sale_happened) => { + if !new_metadata.primary_sale_happened { + new_metadata.primary_sale_happened = primary_sale_happened + } + } + }; + match new_is_mutable { + None => {} + Some(is_mutable) => new_metadata.is_mutable = is_mutable, + }; + + // ensure new metadata is mpl_compatible + assert_metadata_is_mpl_compatible(&new_metadata)?; + let new_data_hash = hash_metadata(&new_metadata)?; + + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + old_data_hash, + creator_hash, + ); + let new_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + new_data_hash, + creator_hash, + ); + + wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf.to_node(), + index, + ) + } + pub fn transfer<'info>( ctx: Context<'_, '_, '_, 'info, Transfer<'info>>, root: [u8; 32],