diff --git a/token-metadata/Cargo.lock b/token-metadata/Cargo.lock index 8b0b80eb67..2a2bd2ca80 100644 --- a/token-metadata/Cargo.lock +++ b/token-metadata/Cargo.lock @@ -1959,19 +1959,25 @@ dependencies = [ [[package]] name = "mpl-token-metadata" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b20f44c87498baa14d504da1098da105cc1ddbb1adb7411fd60e8949c2b901" +version = "1.10.0" dependencies = [ "arrayref", + "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "mpl-utils 0.0.6", + "mpl-token-metadata 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-token-metadata-context-derive 0.2.1", + "mpl-utils 0.2.0", "num-derive", "num-traits", + "rmp-serde", + "rooster", + "serde", + "serde_with", "shank 0.0.11", "solana-program", + "solana-program-test", + "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -1980,23 +1986,18 @@ dependencies = [ [[package]] name = "mpl-token-metadata" version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e674db8846d4a603454ce9c93233dd8d503d62f2afe1b9e6486ce81e431f89" dependencies = [ "arrayref", - "async-trait", "borsh", "mpl-token-auth-rules", - "mpl-token-metadata-context-derive 0.2.1", - "mpl-utils 0.2.0", + "mpl-token-metadata-context-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mpl-utils 0.1.0", "num-derive", "num-traits", - "rmp-serde", - "rooster", - "serde", - "serde_with", "shank 0.0.11", "solana-program", - "solana-program-test", - "solana-sdk", "spl-associated-token-account", "spl-token", "thiserror", @@ -2034,9 +2035,9 @@ dependencies = [ [[package]] name = "mpl-utils" -version = "0.0.6" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6195ce98b92f1d0ea06d0cc9b2392d81673e02b8fb063589926fa73ee6b071a" +checksum = "7fc48e64c50dba956acb46eec86d6968ef0401ef37031426da479f1f2b592066" dependencies = [ "arrayref", "borsh", @@ -2857,10 +2858,10 @@ dependencies = [ [[package]] name = "rooster" version = "0.1.0" -source = "git+https://github.com/metaplex-foundation/rooster#6923ee3bf83957920c64ad271ae7cff80b19ab0e" +source = "git+https://github.com/metaplex-foundation/rooster#ca1221c98fb425096f97277031bfa4dd73fe3f29" dependencies = [ "borsh", - "mpl-token-metadata 1.8.0", + "mpl-token-metadata 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "mpl-utils 0.0.5", "num-derive", "num-traits", diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index bb45431bf1..6512e18f8a 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -3332,7 +3332,7 @@ }, { "name": "edition", - "isMut": true, + "isMut": false, "isSigner": false, "desc": "Edition account", "optional": true @@ -3353,7 +3353,7 @@ "name": "sysvarInstructions", "isMut": false, "isSigner": false, - "desc": "System program" + "desc": "Instructions sysvar account" }, { "name": "authorizationRulesProgram", @@ -4671,7 +4671,7 @@ ] }, { - "name": "UpdateV1", + "name": "DataV1", "fields": [ { "name": "authorization_data", @@ -4759,6 +4759,58 @@ } } ] + }, + { + "name": "AuthorityItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "DataItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "CollectionItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "ProgrammableConfigItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } @@ -4778,7 +4830,7 @@ "name": "TransferV1" }, { - "name": "UpdateV1" + "name": "DataV1" }, { "name": "UtilityV1" @@ -4797,6 +4849,18 @@ }, { "name": "MigrationV1" + }, + { + "name": "AuthorityItemV1" + }, + { + "name": "DataItemV1" + }, + { + "name": "CollectionItemV1" + }, + { + "name": "ProgrammableConfigItemV1" } ] } @@ -4807,7 +4871,7 @@ "kind": "enum", "variants": [ { - "name": "Authority" + "name": "AuthorityItem" }, { "name": "Collection" @@ -4816,10 +4880,19 @@ "name": "Use" }, { - "name": "Update" + "name": "Data" }, { "name": "ProgrammableConfig" + }, + { + "name": "DataItem" + }, + { + "name": "CollectionItem" + }, + { + "name": "ProgrammableConfigItem" } ] } @@ -4974,6 +5047,234 @@ } } ] + }, + { + "name": "AsUpdateAuthorityV2", + "fields": [ + { + "name": "new_update_authority", + "type": { + "option": "publicKey" + } + }, + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "primary_sale_happened", + "type": { + "option": "bool" + } + }, + { + "name": "is_mutable", + "type": { + "option": "bool" + } + }, + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "collection_details", + "type": { + "defined": "CollectionDetailsToggle" + } + }, + { + "name": "uses", + "type": { + "defined": "UsesToggle" + } + }, + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "token_standard", + "type": { + "option": { + "defined": "TokenStandard" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsAuthorityItemDelegateV2", + "fields": [ + { + "name": "new_update_authority", + "type": { + "option": "publicKey" + } + }, + { + "name": "primary_sale_happened", + "type": { + "option": "bool" + } + }, + { + "name": "is_mutable", + "type": { + "option": "bool" + } + }, + { + "name": "token_standard", + "type": { + "option": { + "defined": "TokenStandard" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsCollectionDelegateV2", + "fields": [ + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsDataDelegateV2", + "fields": [ + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsProgConfigDelegateV2", + "fields": [ + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsDataItemDelegateV2", + "fields": [ + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsCollectionItemDelegateV2", + "fields": [ + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "AsProgrammableConfigItemDelegateV2", + "fields": [ + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } diff --git a/token-metadata/js/src/generated/instructions/Update.ts b/token-metadata/js/src/generated/instructions/Update.ts index 0a01bef877..0bd5759368 100644 --- a/token-metadata/js/src/generated/instructions/Update.ts +++ b/token-metadata/js/src/generated/instructions/Update.ts @@ -41,9 +41,9 @@ export const UpdateStruct = new beet.FixableBeetArgsStruct< * @property [] token (optional) Token account * @property [] mint Mint account * @property [_writable_] metadata Metadata account - * @property [_writable_] edition (optional) Edition account + * @property [] edition (optional) Edition account * @property [_writable_, **signer**] payer Payer - * @property [] sysvarInstructions System program + * @property [] sysvarInstructions Instructions sysvar account * @property [] authorizationRulesProgram (optional) Token Authorization Rules Program * @property [] authorizationRules (optional) Token Authorization Rules account * @category Instructions @@ -116,7 +116,7 @@ export function createUpdateInstruction( }, { pubkey: accounts.edition ?? programId, - isWritable: accounts.edition != null, + isWritable: false, isSigner: false, }, { diff --git a/token-metadata/js/src/generated/types/DelegateArgs.ts b/token-metadata/js/src/generated/types/DelegateArgs.ts index 395df7c152..4ed1910498 100644 --- a/token-metadata/js/src/generated/types/DelegateArgs.ts +++ b/token-metadata/js/src/generated/types/DelegateArgs.ts @@ -22,7 +22,7 @@ export type DelegateArgsRecord = { CollectionV1: { authorizationData: beet.COption }; SaleV1: { amount: beet.bignum; authorizationData: beet.COption }; TransferV1: { amount: beet.bignum; authorizationData: beet.COption }; - UpdateV1: { authorizationData: beet.COption }; + DataV1: { authorizationData: beet.COption }; UtilityV1: { amount: beet.bignum; authorizationData: beet.COption }; StakingV1: { amount: beet.bignum; authorizationData: beet.COption }; StandardV1: { amount: beet.bignum }; @@ -32,6 +32,10 @@ export type DelegateArgsRecord = { authorizationData: beet.COption; }; ProgrammableConfigV1: { authorizationData: beet.COption }; + AuthorityItemV1: { authorizationData: beet.COption }; + DataItemV1: { authorizationData: beet.COption }; + CollectionItemV1: { authorizationData: beet.COption }; + ProgrammableConfigItemV1: { authorizationData: beet.COption }; }; /** @@ -55,9 +59,8 @@ export const isDelegateArgsSaleV1 = (x: DelegateArgs): x is DelegateArgs & { __k export const isDelegateArgsTransferV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'TransferV1' } => x.__kind === 'TransferV1'; -export const isDelegateArgsUpdateV1 = ( - x: DelegateArgs, -): x is DelegateArgs & { __kind: 'UpdateV1' } => x.__kind === 'UpdateV1'; +export const isDelegateArgsDataV1 = (x: DelegateArgs): x is DelegateArgs & { __kind: 'DataV1' } => + x.__kind === 'DataV1'; export const isDelegateArgsUtilityV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'UtilityV1' } => x.__kind === 'UtilityV1'; @@ -73,6 +76,19 @@ export const isDelegateArgsLockedTransferV1 = ( export const isDelegateArgsProgrammableConfigV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'ProgrammableConfigV1' } => x.__kind === 'ProgrammableConfigV1'; +export const isDelegateArgsAuthorityItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'AuthorityItemV1' } => x.__kind === 'AuthorityItemV1'; +export const isDelegateArgsDataItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'DataItemV1' } => x.__kind === 'DataItemV1'; +export const isDelegateArgsCollectionItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'CollectionItemV1' } => x.__kind === 'CollectionItemV1'; +export const isDelegateArgsProgrammableConfigItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'ProgrammableConfigItemV1' } => + x.__kind === 'ProgrammableConfigItemV1'; /** * @category userTypes @@ -110,10 +126,10 @@ export const delegateArgsBeet = beet.dataEnum([ ], [ - 'UpdateV1', - new beet.FixableBeetArgsStruct( + 'DataV1', + new beet.FixableBeetArgsStruct( [['authorizationData', beet.coption(authorizationDataBeet)]], - 'DelegateArgsRecord["UpdateV1"]', + 'DelegateArgsRecord["DataV1"]', ), ], @@ -166,4 +182,36 @@ export const delegateArgsBeet = beet.dataEnum([ 'DelegateArgsRecord["ProgrammableConfigV1"]', ), ], + + [ + 'AuthorityItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["AuthorityItemV1"]', + ), + ], + + [ + 'DataItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["DataItemV1"]', + ), + ], + + [ + 'CollectionItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["CollectionItemV1"]', + ), + ], + + [ + 'ProgrammableConfigItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["ProgrammableConfigItemV1"]', + ), + ], ]) as beet.FixableBeet; diff --git a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts index e103d46880..8c80ea6c9f 100644 --- a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts +++ b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts @@ -11,11 +11,14 @@ import * as beet from '@metaplex-foundation/beet'; * @category generated */ export enum MetadataDelegateRole { - Authority, + AuthorityItem, Collection, Use, - Update, + Data, ProgrammableConfig, + DataItem, + CollectionItem, + ProgrammableConfigItem, } /** diff --git a/token-metadata/js/src/generated/types/RevokeArgs.ts b/token-metadata/js/src/generated/types/RevokeArgs.ts index 9ba7c3b1aa..07758b25ed 100644 --- a/token-metadata/js/src/generated/types/RevokeArgs.ts +++ b/token-metadata/js/src/generated/types/RevokeArgs.ts @@ -14,13 +14,17 @@ export enum RevokeArgs { CollectionV1, SaleV1, TransferV1, - UpdateV1, + DataV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, ProgrammableConfigV1, MigrationV1, + AuthorityItemV1, + DataItemV1, + CollectionItemV1, + ProgrammableConfigItemV1, } /** diff --git a/token-metadata/js/src/generated/types/UpdateArgs.ts b/token-metadata/js/src/generated/types/UpdateArgs.ts index 7b731d9f0c..309458acf1 100644 --- a/token-metadata/js/src/generated/types/UpdateArgs.ts +++ b/token-metadata/js/src/generated/types/UpdateArgs.ts @@ -14,6 +14,7 @@ import { CollectionDetailsToggle, collectionDetailsToggleBeet } from './Collecti import { UsesToggle, usesToggleBeet } from './UsesToggle'; import { RuleSetToggle, ruleSetToggleBeet } from './RuleSetToggle'; import { AuthorizationData, authorizationDataBeet } from './AuthorizationData'; +import { TokenStandard, tokenStandardBeet } from './TokenStandard'; /** * This type is used to derive the {@link UpdateArgs} type as well as the de/serializer. * However don't refer to it in your code but use the {@link UpdateArgs} type instead. @@ -35,6 +36,49 @@ export type UpdateArgsRecord = { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; + AsUpdateAuthorityV2: { + newUpdateAuthority: beet.COption; + data: beet.COption; + primarySaleHappened: beet.COption; + isMutable: beet.COption; + collection: CollectionToggle; + collectionDetails: CollectionDetailsToggle; + uses: UsesToggle; + ruleSet: RuleSetToggle; + tokenStandard: beet.COption; + authorizationData: beet.COption; + }; + AsAuthorityItemDelegateV2: { + newUpdateAuthority: beet.COption; + primarySaleHappened: beet.COption; + isMutable: beet.COption; + tokenStandard: beet.COption; + authorizationData: beet.COption; + }; + AsCollectionDelegateV2: { + collection: CollectionToggle; + authorizationData: beet.COption; + }; + AsDataDelegateV2: { + data: beet.COption; + authorizationData: beet.COption; + }; + AsProgConfigDelegateV2: { + ruleSet: RuleSetToggle; + authorizationData: beet.COption; + }; + AsDataItemDelegateV2: { + data: beet.COption; + authorizationData: beet.COption; + }; + AsCollectionItemDelegateV2: { + collection: CollectionToggle; + authorizationData: beet.COption; + }; + AsProgrammableConfigItemDelegateV2: { + ruleSet: RuleSetToggle; + authorizationData: beet.COption; + }; }; /** @@ -52,6 +96,33 @@ export type UpdateArgs = beet.DataEnumKeyAsKind; export const isUpdateArgsV1 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V1' } => x.__kind === 'V1'; +export const isUpdateArgsAsUpdateAuthorityV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsUpdateAuthorityV2' } => x.__kind === 'AsUpdateAuthorityV2'; +export const isUpdateArgsAsAuthorityItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsAuthorityItemDelegateV2' } => + x.__kind === 'AsAuthorityItemDelegateV2'; +export const isUpdateArgsAsCollectionDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsCollectionDelegateV2' } => x.__kind === 'AsCollectionDelegateV2'; +export const isUpdateArgsAsDataDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsDataDelegateV2' } => x.__kind === 'AsDataDelegateV2'; +export const isUpdateArgsAsProgConfigDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsProgConfigDelegateV2' } => x.__kind === 'AsProgConfigDelegateV2'; +export const isUpdateArgsAsDataItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsDataItemDelegateV2' } => x.__kind === 'AsDataItemDelegateV2'; +export const isUpdateArgsAsCollectionItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsCollectionItemDelegateV2' } => + x.__kind === 'AsCollectionItemDelegateV2'; +export const isUpdateArgsAsProgrammableConfigItemDelegateV2 = ( + x: UpdateArgs, +): x is UpdateArgs & { __kind: 'AsProgrammableConfigItemDelegateV2' } => + x.__kind === 'AsProgrammableConfigItemDelegateV2'; /** * @category userTypes @@ -75,4 +146,103 @@ export const updateArgsBeet = beet.dataEnum([ 'UpdateArgsRecord["V1"]', ), ], + + [ + 'AsUpdateAuthorityV2', + new beet.FixableBeetArgsStruct( + [ + ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], + ['data', beet.coption(dataBeet)], + ['primarySaleHappened', beet.coption(beet.bool)], + ['isMutable', beet.coption(beet.bool)], + ['collection', collectionToggleBeet], + ['collectionDetails', collectionDetailsToggleBeet], + ['uses', usesToggleBeet], + ['ruleSet', ruleSetToggleBeet], + ['tokenStandard', beet.coption(tokenStandardBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsUpdateAuthorityV2"]', + ), + ], + + [ + 'AsAuthorityItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], + ['primarySaleHappened', beet.coption(beet.bool)], + ['isMutable', beet.coption(beet.bool)], + ['tokenStandard', beet.coption(tokenStandardBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsAuthorityItemDelegateV2"]', + ), + ], + + [ + 'AsCollectionDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['collection', collectionToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsCollectionDelegateV2"]', + ), + ], + + [ + 'AsDataDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['data', beet.coption(dataBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsDataDelegateV2"]', + ), + ], + + [ + 'AsProgConfigDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['ruleSet', ruleSetToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsProgConfigDelegateV2"]', + ), + ], + + [ + 'AsDataItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['data', beet.coption(dataBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsDataItemDelegateV2"]', + ), + ], + + [ + 'AsCollectionItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['collection', collectionToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsCollectionItemDelegateV2"]', + ), + ], + + [ + 'AsProgrammableConfigItemDelegateV2', + new beet.FixableBeetArgsStruct( + [ + ['ruleSet', ruleSetToggleBeet], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["AsProgrammableConfigItemDelegateV2"]', + ), + ], ]) as beet.FixableBeet; diff --git a/token-metadata/js/test/update.test.ts b/token-metadata/js/test/update.test.ts index f627969d0a..19a2bf8e04 100644 --- a/token-metadata/js/test/update.test.ts +++ b/token-metadata/js/test/update.test.ts @@ -1177,7 +1177,7 @@ test('Update: Invalid Update Authority Fails', async (t) => { await updateTx.assertError(t, /Invalid authority type/); }); -test('Update: Delegate Authority Type Not Supported', async (t) => { +test('Update: Delegate Authority Role Not Allowed to Update Data', async (t) => { const API = new InitTransactions(); const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); @@ -1192,7 +1192,7 @@ test('Update: Delegate Authority Type Not Supported', async (t) => { Buffer.from('metadata'), PROGRAM_ID.toBuffer(), daManager.mint.toBuffer(), - Buffer.from('update_delegate'), + Buffer.from('collection_item_delegate'), payer.publicKey.toBuffer(), delegate.publicKey.toBuffer(), ], @@ -1201,7 +1201,7 @@ test('Update: Delegate Authority Type Not Supported', async (t) => { amman.addr.addLabel('Delegate Record', delegateRecord); const args: DelegateArgs = { - __kind: 'UpdateV1', + __kind: 'CollectionItemV1', authorizationData: null, }; @@ -1246,7 +1246,7 @@ test('Update: Delegate Authority Type Not Supported', async (t) => { daManager.masterEdition, ); updateTx.then((x) => - x.assertLogs(t, [/Invalid authority type/i], { + x.assertLogs(t, [/Authority cannot apply all update args/i], { txLabel: 'tx: Update', }), ); diff --git a/token-metadata/program/Cargo.toml b/token-metadata/program/Cargo.toml index fb764b067e..b7b369dd8c 100644 --- a/token-metadata/program/Cargo.toml +++ b/token-metadata/program/Cargo.toml @@ -36,6 +36,7 @@ solana-sdk = "1.14" solana-program-test = "1.14" serde = { version = "1.0.147", features = ["derive"]} async-trait = "0.1.64" +old-token-metadata = { package = "mpl-token-metadata", version = "=1.10.0", features = ["no-entrypoint"] } [lib] crate-type = ["cdylib", "lib"] diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 07bad02a46..e8dddfe377 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -11,6 +11,7 @@ use solana_program::{ use super::InstructionBuilder; use crate::{instruction::MetadataInstruction, processor::AuthorizationData}; +/// Delegate args can specify Metadata delegates and Token delegates. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] @@ -29,7 +30,7 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - UpdateV1 { + DataV1 { /// Required authorization data to validate the request. authorization_data: Option, }, @@ -57,6 +58,22 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + AuthorityItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + DataItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + CollectionItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + ProgrammableConfigItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, } #[repr(C)] @@ -66,34 +83,44 @@ pub enum RevokeArgs { CollectionV1, SaleV1, TransferV1, - UpdateV1, + DataV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, ProgrammableConfigV1, MigrationV1, + AuthorityItemV1, + DataItemV1, + CollectionItemV1, + ProgrammableConfigItemV1, } #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum MetadataDelegateRole { - Authority, + AuthorityItem, Collection, Use, - Update, + Data, ProgrammableConfig, + DataItem, + CollectionItem, + ProgrammableConfigItem, } impl fmt::Display for MetadataDelegateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self { - Self::Authority => "authority_delegate".to_string(), + Self::AuthorityItem => "authority_item_delegate".to_string(), Self::Collection => "collection_delegate".to_string(), Self::Use => "use_delegate".to_string(), - Self::Update => "update_delegate".to_string(), + Self::Data => "data_delegate".to_string(), Self::ProgrammableConfig => "programmable_config_delegate".to_string(), + Self::DataItem => "data_item_delegate".to_string(), + Self::CollectionItem => "collection_item_delegate".to_string(), + Self::ProgrammableConfigItem => "prog_config_item_delegate".to_string(), }; write!(f, "{message}") diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 1d80a5083e..ee7bea53d5 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -15,7 +15,7 @@ use crate::{ processor::AuthorizationData, state::{ AssetData, Collection, CollectionDetails, Creator, Data, DataV2, MigrationType, - PrintSupply, Uses, + PrintSupply, TokenStandard, Uses, }, }; @@ -71,9 +71,9 @@ pub enum TransferArgs { /// Struct representing the values to be updated for an `update` instructions. /// -/// Values that are set to 'None' are not changed; any value set to `Some(_)` will -/// have its value updated. There are properties that have three valid states, which -/// allow the value to remaing the same, to be cleared or to set a new value. +/// Values that are set to `None` are not changed. Any value set to `Some(...)` will +/// have its value updated. There are properties that have three valid states, and +/// use a "toggle" type that allows the value to be set, cleared, or remain the same. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] @@ -100,20 +100,200 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + AsUpdateAuthorityV2 { + /// The new update authority. + new_update_authority: Option, + /// The metadata details. + data: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + is_mutable: Option, + /// Collection information. + collection: CollectionToggle, + /// Additional details of the collection. + collection_details: CollectionDetailsToggle, + /// Uses information. + uses: UsesToggle, + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + /// Token standard. + token_standard: Option, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsAuthorityItemDelegateV2 { + /// The new update authority. + new_update_authority: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + is_mutable: Option, + /// Token standard. + token_standard: Option, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsCollectionDelegateV2 { + /// Collection information. + collection: CollectionToggle, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsDataDelegateV2 { + /// The metadata details. + data: Option, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsProgConfigDelegateV2 { + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsDataItemDelegateV2 { + /// The metadata details. + data: Option, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsCollectionItemDelegateV2 { + /// Collection information. + collection: CollectionToggle, + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AsProgrammableConfigItemDelegateV2 { + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + /// Required authorization data to validate the request. + authorization_data: Option, + }, } -impl Default for UpdateArgs { - fn default() -> Self { +impl UpdateArgs { + pub fn default_v1() -> Self { Self::V1 { + new_update_authority: None, + data: None, + primary_sale_happened: None, + is_mutable: None, + collection: CollectionToggle::default(), + collection_details: CollectionDetailsToggle::default(), + uses: UsesToggle::default(), + rule_set: RuleSetToggle::default(), authorization_data: None, + } + } + + pub fn default_as_update_authority() -> Self { + Self::AsUpdateAuthorityV2 { + new_update_authority: None, + data: None, + primary_sale_happened: None, + is_mutable: None, + collection: CollectionToggle::default(), + collection_details: CollectionDetailsToggle::default(), + uses: UsesToggle::default(), + rule_set: RuleSetToggle::default(), + token_standard: None, + authorization_data: None, + } + } + + pub fn default_as_authority_item_delegate() -> Self { + Self::AsAuthorityItemDelegateV2 { + new_update_authority: None, + primary_sale_happened: None, + is_mutable: None, + token_standard: None, + authorization_data: None, + } + } + + pub fn default_as_collection_delegate() -> Self { + Self::AsCollectionDelegateV2 { + collection: CollectionToggle::default(), + authorization_data: None, + } + } + + pub fn default_as_data_delegate() -> Self { + Self::AsDataDelegateV2 { + data: None, + authorization_data: None, + } + } + + pub fn default_as_programmable_config_delegate() -> Self { + Self::AsProgConfigDelegateV2 { + rule_set: RuleSetToggle::default(), + authorization_data: None, + } + } + + pub fn default_as_data_item_delegate() -> Self { + Self::AsDataItemDelegateV2 { + data: None, + authorization_data: None, + } + } + + pub fn default_as_collection_item_delegate() -> Self { + Self::AsCollectionItemDelegateV2 { + collection: CollectionToggle::default(), + authorization_data: None, + } + } + + pub fn default_as_programmable_config_item_delegate() -> Self { + Self::AsProgrammableConfigItemDelegateV2 { + rule_set: RuleSetToggle::default(), + authorization_data: None, + } + } +} + +pub(crate) struct InternalUpdateArgs { + /// The new update authority. + pub new_update_authority: Option, + /// The metadata details. + pub data: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + pub primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + pub is_mutable: Option, + /// Collection information. + pub collection: CollectionToggle, + /// Additional details of the collection. + pub collection_details: CollectionDetailsToggle, + /// Uses information. + pub uses: UsesToggle, + // Programmable rule set configuration (only applicable to `Programmable` asset types). + pub rule_set: RuleSetToggle, + // Token standard. + pub token_standard: Option, +} + +impl Default for InternalUpdateArgs { + fn default() -> Self { + Self { new_update_authority: None, data: None, primary_sale_happened: None, is_mutable: None, collection: CollectionToggle::None, - uses: UsesToggle::None, collection_details: CollectionDetailsToggle::None, + uses: UsesToggle::None, rule_set: RuleSetToggle::None, + token_standard: None, } } } @@ -122,8 +302,9 @@ impl Default for UpdateArgs { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum CollectionToggle { + #[default] None, Clear, Set(Collection), @@ -157,8 +338,9 @@ impl CollectionToggle { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum UsesToggle { + #[default] None, Clear, Set(Uses), @@ -192,8 +374,9 @@ impl UsesToggle { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum CollectionDetailsToggle { + #[default] None, Clear, Set(CollectionDetails), @@ -230,8 +413,9 @@ impl CollectionDetailsToggle { #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] -#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Default)] pub enum RuleSetToggle { + #[default] None, Clear, Set(Pubkey), @@ -723,23 +907,11 @@ impl InstructionBuilder for super::builders::Update { fn instruction(&self) -> solana_program::instruction::Instruction { let mut accounts = vec![ AccountMeta::new_readonly(self.authority, true), - if let Some(record) = self.delegate_record { - AccountMeta::new(record, false) - } else { - AccountMeta::new_readonly(crate::ID, false) - }, - if let Some(token) = self.token { - AccountMeta::new(token, false) - } else { - AccountMeta::new_readonly(crate::ID, false) - }, + AccountMeta::new_readonly(self.delegate_record.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(self.token.unwrap_or(crate::ID), false), AccountMeta::new_readonly(self.mint, false), AccountMeta::new(self.metadata, false), - if let Some(edition) = self.edition { - AccountMeta::new(edition, false) - } else { - AccountMeta::new_readonly(crate::ID, false) - }, + AccountMeta::new_readonly(self.edition.unwrap_or(crate::ID), false), AccountMeta::new(self.payer, true), AccountMeta::new_readonly(self.system_program, false), AccountMeta::new_readonly(self.sysvar_instructions, false), diff --git a/token-metadata/program/src/instruction/mod.rs b/token-metadata/program/src/instruction/mod.rs index 23e7196619..ccdd5917dc 100644 --- a/token-metadata/program/src/instruction/mod.rs +++ b/token-metadata/program/src/instruction/mod.rs @@ -725,10 +725,10 @@ pub enum MetadataInstruction { #[account(2, optional, name="token", desc="Token account")] #[account(3, name="mint", desc="Mint account")] #[account(4, writable, name="metadata", desc="Metadata account")] - #[account(5, optional, writable, name="edition", desc="Edition account")] + #[account(5, optional, name="edition", desc="Edition account")] #[account(6, signer, writable, name="payer", desc="Payer")] #[account(7, name="system_program", desc="System program")] - #[account(8, name="sysvar_instructions", desc="System program")] + #[account(8, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(9, optional, name="authorization_rules_program", desc="Token Authorization Rules Program")] #[account(10, optional, name="authorization_rules", desc="Token Authorization Rules account")] #[default_optional_accounts] diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index 6bcb710f8e..8f370b12c0 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -35,18 +35,23 @@ impl Display for DelegateScenario { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let message = match self { Self::Metadata(role) => match role { - MetadataDelegateRole::Authority => "Authority".to_string(), + MetadataDelegateRole::AuthorityItem => "AuthorityItem".to_string(), MetadataDelegateRole::Collection => "Collection".to_string(), MetadataDelegateRole::Use => "Use".to_string(), - MetadataDelegateRole::Update => "Update".to_string(), + MetadataDelegateRole::Data => "Data".to_string(), MetadataDelegateRole::ProgrammableConfig => "ProgrammableConfig".to_string(), + MetadataDelegateRole::DataItem => "DataItem".to_string(), + MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), + MetadataDelegateRole::ProgrammableConfigItem => { + "ProgrammableConfigItem".to_string() + } }, Self::Token(role) => match role { TokenDelegateRole::Sale => "Sale".to_string(), TokenDelegateRole::Transfer => "Transfer".to_string(), - TokenDelegateRole::LockedTransfer => "LockedTransfer".to_string(), TokenDelegateRole::Utility => "Utility".to_string(), TokenDelegateRole::Staking => "Staking".to_string(), + TokenDelegateRole::LockedTransfer => "LockedTransfer".to_string(), _ => panic!("Invalid delegate role"), }, }; @@ -75,16 +80,6 @@ pub fn delegate<'a>( amount, authorization_data, } => Some((TokenDelegateRole::Transfer, amount, authorization_data)), - // LockedTransfer - DelegateArgs::LockedTransferV1 { - amount, - authorization_data, - .. - } => Some(( - TokenDelegateRole::LockedTransfer, - amount, - authorization_data, - )), // Utility DelegateArgs::UtilityV1 { amount, @@ -97,6 +92,17 @@ pub fn delegate<'a>( } => Some((TokenDelegateRole::Staking, amount, authorization_data)), // Standard DelegateArgs::StandardV1 { amount } => Some((TokenDelegateRole::Standard, amount, &None)), + // LockedTransfer + DelegateArgs::LockedTransferV1 { + amount, + authorization_data, + .. + } => Some(( + TokenDelegateRole::LockedTransfer, + amount, + authorization_data, + )), + // we don't need to fail if did not find a match at this point _ => None, }; @@ -118,12 +124,26 @@ pub fn delegate<'a>( DelegateArgs::CollectionV1 { authorization_data } => { Some((MetadataDelegateRole::Collection, authorization_data)) } - DelegateArgs::UpdateV1 { authorization_data } => { - Some((MetadataDelegateRole::Update, authorization_data)) + DelegateArgs::DataV1 { authorization_data } => { + Some((MetadataDelegateRole::Data, authorization_data)) } DelegateArgs::ProgrammableConfigV1 { authorization_data } => { Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) } + DelegateArgs::AuthorityItemV1 { authorization_data } => { + Some((MetadataDelegateRole::AuthorityItem, authorization_data)) + } + DelegateArgs::DataItemV1 { authorization_data } => { + Some((MetadataDelegateRole::DataItem, authorization_data)) + } + DelegateArgs::CollectionItemV1 { authorization_data } => { + Some((MetadataDelegateRole::CollectionItem, authorization_data)) + } + DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => Some(( + MetadataDelegateRole::ProgrammableConfigItem, + authorization_data, + )), + // we don't need to fail if did not find a match at this point _ => None, }; diff --git a/token-metadata/program/src/processor/delegate/revoke.rs b/token-metadata/program/src/processor/delegate/revoke.rs index 21a24d2892..70c8441ac7 100644 --- a/token-metadata/program/src/processor/delegate/revoke.rs +++ b/token-metadata/program/src/processor/delegate/revoke.rs @@ -33,16 +33,16 @@ pub fn revoke<'a>( RevokeArgs::SaleV1 => Some(TokenDelegateRole::Sale), // Transfer RevokeArgs::TransferV1 => Some(TokenDelegateRole::Transfer), - // LockedTransfer - RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), // Utility RevokeArgs::UtilityV1 => Some(TokenDelegateRole::Utility), // Staking RevokeArgs::StakingV1 => Some(TokenDelegateRole::Staking), - // Migration - RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // Standard RevokeArgs::StandardV1 => Some(TokenDelegateRole::Standard), + // LockedTransfer + RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), + // Migration + RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // we don't need to fail if did not find a match at this point _ => None, }; @@ -55,8 +55,12 @@ pub fn revoke<'a>( // checks if it is a MetadataDelegate creation let metadata_delegate = match &args { RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), - RevokeArgs::UpdateV1 => Some(MetadataDelegateRole::Update), + RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), + RevokeArgs::AuthorityItemV1 => Some(MetadataDelegateRole::AuthorityItem), + RevokeArgs::DataItemV1 => Some(MetadataDelegateRole::DataItem), + RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), + RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point _ => None, }; diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 29ce52e35e..14a12e0629 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -10,7 +10,10 @@ use spl_token::state::Account; use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, - instruction::{Context, MetadataDelegateRole, Update, UpdateArgs}, + instruction::{ + CollectionDetailsToggle, CollectionToggle, Context, InternalUpdateArgs, + MetadataDelegateRole, Update, UpdateArgs, UsesToggle, + }, pda::{EDITION, PREFIX}, state::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, @@ -43,47 +46,38 @@ pub fn update<'a>( ) -> ProgramResult { let context = Update::to_context(accounts)?; - match args { - UpdateArgs::V1 { .. } => update_v1(program_id, context, args), - } + update_v1(program_id, context, args) } fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> ProgramResult { - //** Account Validation **/ // Assert signers - // This account should always be a signer regardless of the authority type, + // Authority should always be a signer regardless of the authority type, // because at least one signer is required to update the metadata. assert_signer(ctx.accounts.authority_info)?; + assert_signer(ctx.accounts.payer_info)?; // Assert program ownership - assert_owned_by(ctx.accounts.metadata_info, program_id)?; + if let Some(delegate_record_info) = ctx.accounts.delegate_record_info { + assert_owned_by(delegate_record_info, &crate::ID)?; + } + + if let Some(token_info) = ctx.accounts.token_info { + assert_owned_by(token_info, &spl_token::ID)?; + } + assert_owned_by(ctx.accounts.mint_info, &spl_token::ID)?; + assert_owned_by(ctx.accounts.metadata_info, program_id)?; if let Some(edition) = ctx.accounts.edition_info { assert_owned_by(edition, program_id)?; - // checks that we got the correct master account - assert_derivation( - program_id, - edition, - &[ - PREFIX.as_bytes(), - program_id.as_ref(), - ctx.accounts.mint_info.key.as_ref(), - EDITION.as_bytes(), - ], - )?; } - // token owner - if let Some(holder_token_account) = ctx.accounts.token_info { - assert_owned_by(holder_token_account, &spl_token::ID)?; - } - // delegate - if let Some(delegate_record_info) = ctx.accounts.delegate_record_info { - assert_owned_by(delegate_record_info, &crate::ID)?; - } + // Note that we do NOT check the ownership of authorization rules account here as this allows + // `Update` to be used to correct a previously invalid `RuleSet`. In practice the ownership of + // authorization rules is checked by the Auth Rules program each time the program is invoked to + // validate rules. // Check program IDs @@ -95,46 +89,77 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } // If the current rule set is passed in, also require the mpl-token-auth-rules program - // to be passed in. + // to be passed in (and check its program ID). if ctx.accounts.authorization_rules_info.is_some() { - if let Some(authorization_rules_program) = ctx.accounts.authorization_rules_program_info { - if authorization_rules_program.key != &mpl_token_auth_rules::ID { - return Err(ProgramError::IncorrectProgramId); - } - } else { - return Err(MetadataError::MissingAuthorizationRulesProgram.into()); + let authorization_rules_program = ctx + .accounts + .authorization_rules_program_info + .ok_or(MetadataError::MissingAuthorizationRulesProgram)?; + + if authorization_rules_program.key != &mpl_token_auth_rules::ID { + return Err(ProgramError::IncorrectProgramId); } } // Validate relationships + // Token + let (token_pubkey, token) = if let Some(token_info) = ctx.accounts.token_info { + let token = Account::unpack(&token_info.try_borrow_data()?)?; + + // Token mint must match mint account key. + if token.mint != *ctx.accounts.mint_info.key { + return Err(MetadataError::MintMismatch.into()); + } + + (Some(token_info.key), Some(token)) + } else { + (None, None) + }; + + // Metadata let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; - // Mint must match metadata mint + // Metadata mint must match mint account key. if metadata.mint != *ctx.accounts.mint_info.key { return Err(MetadataError::MintMismatch.into()); } - let token_standard = if let Some(token_standard) = metadata.token_standard { - token_standard - } else { - check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? - }; + // Edition + if let Some(edition) = ctx.accounts.edition_info { + // checks that we got the correct edition account + assert_derivation( + program_id, + edition, + &[ + PREFIX.as_bytes(), + program_id.as_ref(), + ctx.accounts.mint_info.key.as_ref(), + EDITION.as_bytes(), + ], + )?; + } - let (token_pubkey, token) = if let Some(token_info) = ctx.accounts.token_info { - ( - Some(token_info.key), - Some(Account::unpack(&token_info.try_borrow_data()?)?), - ) - } else { - (None, None) - }; + // Check authority. - // there is a special case for collection-level delegates, where the - // validation should use the collection key as the mint parameter - let collection_mint = if let Some(Collection { key, .. }) = &metadata.collection { - Some(key) - } else { - None + // There is a special case for collection-level delegates, where the + // validation should use the collection key as the mint parameter. + let existing_collection_mint = metadata + .collection + .as_ref() + .map(|Collection { key, .. }| key); + + // Check if caller passed in a collection and if so use that. Note that + // `validate_update` checks that the authority has permission to pass in + // a new collection value. + let collection_mint = match &args { + UpdateArgs::V1 { collection, .. } + | UpdateArgs::AsUpdateAuthorityV2 { collection, .. } + | UpdateArgs::AsCollectionDelegateV2 { collection, .. } + | UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => match collection { + CollectionToggle::Set(Collection { key, .. }) => Some(key), + _ => existing_collection_mint, + }, + _ => existing_collection_mint, }; // Determines if we have a valid authority to perform the update. This must @@ -152,7 +177,20 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro token: token_pubkey, token_account: token.as_ref(), metadata_delegate_record_info: ctx.accounts.delegate_record_info, - metadata_delegate_roles: vec![MetadataDelegateRole::ProgrammableConfig], + metadata_delegate_roles: vec![ + MetadataDelegateRole::AuthorityItem, + MetadataDelegateRole::Data, + MetadataDelegateRole::DataItem, + MetadataDelegateRole::Collection, + MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::ProgrammableConfigItem, + ], + collection_metadata_delegate_roles: vec![ + MetadataDelegateRole::Data, + MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, + ], precedence: &[ AuthorityType::Metadata, AuthorityType::MetadataDelegate, @@ -161,6 +199,26 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ..Default::default() })?; + // Validate that authority has permission to use the update args that were provided. + let internal_update_args = validate_update(args, &authority_type, metadata_delegate_role)?; + + // Find existing token standard from metadata or infer it. + let existing_or_inferred_token_std = if let Some(token_standard) = metadata.token_standard { + token_standard + } else { + check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? + }; + + // If the caller passed in a token standard, use it if it passes the check. If the user did + // not pass in a token standard, use the existing or inferred token standard. + let token_standard = match internal_update_args.token_standard { + Some(desired_token_standard) => { + check_desired_token_standard(existing_or_inferred_token_std, desired_token_standard)?; + desired_token_standard + } + None => existing_or_inferred_token_std, + }; + // For pNFTs, we need to validate the authorization rules. if matches!(token_standard, TokenStandard::ProgrammableNonFungible) { // If the metadata account has a current rule set, we validate that @@ -174,18 +232,14 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } } - validate_update(&args, &authority_type, metadata_delegate_role)?; - // If we reach here without errors we have validated that the authority is allowed to // perform an update. metadata.update_v1( - args, + internal_update_args, ctx.accounts.authority_info, ctx.accounts.metadata_info, token, - Some(token_standard), - authority_type, - metadata_delegate_role, + token_standard, )?; Ok(()) @@ -194,63 +248,163 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro /// Validates that the authority is only updating metadata fields /// that it has access to. fn validate_update( - args: &UpdateArgs, + args: UpdateArgs, authority_type: &AuthorityType, metadata_delegate_role: Option, -) -> ProgramResult { - // validate the authority type +) -> Result { + let mut internal_update_args = InternalUpdateArgs::default(); + // validate the authority type match authority_type { AuthorityType::Metadata => { - // metadata authority is the paramount (upadte) authority + // metadata authority is the paramount (update) authority msg!("Auth type: Metadata"); - } - AuthorityType::MetadataDelegate => { - // support for delegate update - msg!("Auth type: Delegate"); + + match args { + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } => { + internal_update_args.new_update_authority = new_update_authority; + internal_update_args.data = data; + internal_update_args.primary_sale_happened = primary_sale_happened; + internal_update_args.is_mutable = is_mutable; + internal_update_args.collection = collection; + internal_update_args.collection_details = collection_details; + internal_update_args.uses = uses; + internal_update_args.rule_set = rule_set; + internal_update_args.token_standard = None; + } + UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + .. + } => { + internal_update_args.new_update_authority = new_update_authority; + internal_update_args.data = data; + internal_update_args.primary_sale_happened = primary_sale_happened; + internal_update_args.is_mutable = is_mutable; + internal_update_args.collection = collection; + internal_update_args.collection_details = collection_details; + internal_update_args.uses = uses; + internal_update_args.rule_set = rule_set; + internal_update_args.token_standard = token_standard; + } + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + } + + return Ok(internal_update_args); } AuthorityType::Holder => { // support for holder update msg!("Auth type: Holder"); return Err(MetadataError::FeatureNotSupported.into()); } - _ => { - return Err(MetadataError::InvalidAuthorityType.into()); + AuthorityType::MetadataDelegate => { + // support for delegate update + msg!("Auth type: Delegate"); } + _ => return Err(MetadataError::InvalidAuthorityType.into()), } - let UpdateArgs::V1 { - data, - primary_sale_happened, - is_mutable, - collection, - uses, - new_update_authority, - collection_details, - .. - } = args; - // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to - match metadata_delegate_role { - Some(MetadataDelegateRole::ProgrammableConfig) => { - // can only update the programmable config - if data.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection.is_some() - || uses.is_some() - || new_update_authority.is_some() - || collection_details.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); + if let Some(metadata_delegate_role) = metadata_delegate_role { + match (metadata_delegate_role, args) { + ( + MetadataDelegateRole::AuthorityItem, + UpdateArgs::AsAuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard, + .. + }, + ) => { + internal_update_args.new_update_authority = new_update_authority; + internal_update_args.primary_sale_happened = primary_sale_happened; + internal_update_args.is_mutable = is_mutable; + internal_update_args.token_standard = token_standard; } - } - Some(_) => { - return Err(MetadataError::InvalidAuthorityType.into()); - } - None => { /* no delegate role to check */ } + (MetadataDelegateRole::Data, UpdateArgs::AsDataDelegateV2 { data, .. }) => { + internal_update_args.data = data; + } + (MetadataDelegateRole::DataItem, UpdateArgs::AsDataItemDelegateV2 { data, .. }) => { + internal_update_args.data = data; + } + ( + MetadataDelegateRole::Collection, + UpdateArgs::AsCollectionDelegateV2 { collection, .. }, + ) => { + internal_update_args.collection = collection; + } + ( + MetadataDelegateRole::CollectionItem, + UpdateArgs::AsCollectionItemDelegateV2 { collection, .. }, + ) => { + internal_update_args.collection = collection; + } + ( + // V1 supported Programmable config, leaving here for backwards + // compatibility. + MetadataDelegateRole::ProgrammableConfig, + UpdateArgs::V1 { + new_update_authority: None, + data: None, + primary_sale_happened: None, + is_mutable: None, + collection: CollectionToggle::None, + collection_details: CollectionDetailsToggle::None, + uses: UsesToggle::None, + rule_set, + .. + }, + ) => { + internal_update_args.rule_set = rule_set; + } + ( + MetadataDelegateRole::ProgrammableConfig, + UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. }, + ) => { + internal_update_args.rule_set = rule_set; + } + ( + MetadataDelegateRole::ProgrammableConfigItem, + UpdateArgs::AsProgrammableConfigItemDelegateV2 { rule_set, .. }, + ) => { + internal_update_args.rule_set = rule_set; + } + _ => return Err(MetadataError::InvalidUpdateArgs.into()), + }; } - Ok(()) + Ok(internal_update_args) +} + +fn check_desired_token_standard( + existing_or_inferred_token_std: TokenStandard, + desired_token_standard: TokenStandard, +) -> ProgramResult { + match (existing_or_inferred_token_std, desired_token_standard) { + ( + TokenStandard::Fungible | TokenStandard::FungibleAsset, + TokenStandard::Fungible | TokenStandard::FungibleAsset, + ) => Ok(()), + (existing, desired) if existing == desired => Ok(()), + _ => Err(MetadataError::InvalidTokenStandard.into()), + } } diff --git a/token-metadata/program/src/processor/verification/collection.rs b/token-metadata/program/src/processor/verification/collection.rs index a517e2f319..811aecb0d4 100644 --- a/token-metadata/program/src/processor/verification/collection.rs +++ b/token-metadata/program/src/processor/verification/collection.rs @@ -160,11 +160,15 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context let authority_response = if parent_burned { // If the collection parent is burned, we need to use an authority for the item rather than - // the collection. The required authority is either the item's metadata update authority, - // or an update delegate for the item. This call fails if no valid authority is present. + // the collection. The required authority is either the item's metadata update authority + // or a delegate for the item that can update the item's collection field. This call fails + // if no valid authority is present. auth_request.mint = &metadata.mint; auth_request.update_authority = &metadata.update_authority; - auth_request.metadata_delegate_roles = vec![MetadataDelegateRole::Update]; + auth_request.metadata_delegate_roles = vec![ + MetadataDelegateRole::Collection, + MetadataDelegateRole::CollectionItem, + ]; AuthorityType::get_authority_type(auth_request) } else { // If the parent is not burned, we need to ensure the collection metadata account is owned @@ -182,6 +186,10 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context // If the collection parent is not burned, the required authority is either the collection // parent's metadata update authority, or a collection delegate for the collection parent. // This call fails if no valid authority is present. + // + // Note that this is sending the delegate in the `metadata_delegate_roles` vec and NOT the + // `collection_metadata_delegate_roles` vec because in this case we are authorizing using + // the collection parent's update authority. auth_request.mint = collection_mint_info.key; auth_request.update_authority = &collection_metadata.update_authority; auth_request.metadata_delegate_roles = vec![MetadataDelegateRole::Collection]; diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index 32cb0c8ab8..9f74ad95f0 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -4,9 +4,7 @@ use crate::{ collection::assert_collection_update_is_valid, metadata::assert_data_valid, uses::assert_valid_use, }, - instruction::{ - CollectionDetailsToggle, CollectionToggle, MetadataDelegateRole, RuleSetToggle, UpdateArgs, - }, + instruction::{CollectionDetailsToggle, CollectionToggle, InternalUpdateArgs, RuleSetToggle}, utils::{clean_write_metadata, puff_out_data_fields}, }; @@ -97,148 +95,118 @@ impl Metadata { pub(crate) fn update_v1<'a>( &mut self, - args: UpdateArgs, + args: InternalUpdateArgs, update_authority: &AccountInfo<'a>, metadata: &AccountInfo<'a>, token: Option, - token_standard: Option, - authority_type: AuthorityType, - delegate_role: Option, + token_standard: TokenStandard, ) -> ProgramResult { - let UpdateArgs::V1 { - data, - primary_sale_happened, - is_mutable, - collection, - uses, - new_update_authority, - rule_set, - collection_details, - .. - } = args; - - // updates the token standard only if the current value is None - let token_standard = match self.token_standard { - Some(ts) => ts, - None => { - if let Some(ts) = token_standard { - self.token_standard = Some(ts); - ts - } else { - return Err(MetadataError::InvalidTokenStandard.into()); - } + // Update the token standard if it is changed. + self.token_standard = Some(token_standard); + + if args.uses.is_some() { + let uses_option = args.uses.to_option(); + // If already None leave it as None. + assert_valid_use(&uses_option, &self.uses)?; + self.uses = uses_option; + } + + if let CollectionDetailsToggle::Set(collection_details) = args.collection_details { + // only unsized collections can have the size set, and only once. + if self.collection_details.is_some() { + return Err(MetadataError::SizedCollection.into()); } - }; - if matches!(authority_type, AuthorityType::Metadata) { - if let Some(data) = data { - if !self.is_mutable { - return Err(MetadataError::DataIsImmutable.into()); - } + self.collection_details = Some(collection_details); + } - assert_data_valid( - &data, - update_authority.key, - self, - false, - update_authority.is_signer, - )?; - self.data = data; - } + if let Some(authority) = args.new_update_authority { + self.update_authority = authority; + } - // if the Collection data is 'Set', only allow updating if it is unverified - // or if it exactly matches the existing collection info; if the Collection data - // is 'Clear', then only set to 'None' if it is unverified. - match collection { - CollectionToggle::Set(_) => { - let collection_option = collection.to_option(); - assert_collection_update_is_valid(false, &self.collection, &collection_option)?; - self.collection = collection_option; - } - CollectionToggle::Clear => { - if let Some(current_collection) = self.collection.as_ref() { - // Can't change a verified collection in this command. - if current_collection.verified { - return Err(MetadataError::CannotUpdateVerifiedCollection.into()); - } - // If it's unverified, it's ok to set to None. - self.collection = None; - } - } - CollectionToggle::None => { /* nothing to do */ } + if let Some(primary_sale) = args.primary_sale_happened { + // If received primary_sale is true, flip to true. + if primary_sale || !self.primary_sale_happened { + self.primary_sale_happened = primary_sale + } else { + return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into()); } + } - if uses.is_some() { - let uses_option = uses.to_option(); - // If already None leave it as None. - assert_valid_use(&uses_option, &self.uses)?; - self.uses = uses_option; + if let Some(mutable) = args.is_mutable { + // If received value is false, flip to false. + if !mutable || self.is_mutable { + self.is_mutable = mutable + } else { + return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); } + } - if let Some(authority) = new_update_authority { - self.update_authority = authority; + // Update Authority or Data Delegates can update this section. + if let Some(data) = args.data { + if !self.is_mutable { + return Err(MetadataError::DataIsImmutable.into()); } - if let Some(primary_sale) = primary_sale_happened { - // If received primary_sale is true, flip to true. - if primary_sale || !self.primary_sale_happened { - self.primary_sale_happened = primary_sale - } else { - return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into()); - } - } + assert_data_valid( + &data, + update_authority.key, + self, + false, + update_authority.is_signer, + )?; + self.data = data; + } - if let Some(mutable) = is_mutable { - // If received value is false, flip to false. - if !mutable || self.is_mutable { - self.is_mutable = mutable - } else { - return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); - } + match args.collection { + // if the Collection data is 'Set', only allow updating if it is unverified + // or if it exactly matches the existing collection info; if the Collection data + // is 'Clear', then only set to 'None' if it is unverified. + collection @ CollectionToggle::Set(_) => { + let collection_option = collection.to_option(); + assert_collection_update_is_valid(false, &self.collection, &collection_option)?; + self.collection = collection_option; } - - if let CollectionDetailsToggle::Set(collection_details) = collection_details { - // only unsized collections can have the size set, and only once. - if self.collection_details.is_some() { - return Err(MetadataError::SizedCollection.into()); + CollectionToggle::Clear => { + if let Some(current_collection) = self.collection.as_ref() { + // Can't change a verified collection in this command. + if current_collection.verified { + return Err(MetadataError::CannotUpdateVerifiedCollection.into()); + } + // If it's unverified, it's ok to set to None. + self.collection = None; } - - self.collection_details = Some(collection_details); } + CollectionToggle::None => { /* nothing to do */ } } - if matches!(authority_type, AuthorityType::Metadata) - || matches!( - delegate_role, - Some(MetadataDelegateRole::ProgrammableConfig) - ) - { - // if the rule_set data is either 'Set' or 'Clear', only allow updating if the - // token standard is equal to `ProgrammableNonFungible` and no SPL delegate is set. - if matches!(rule_set, RuleSetToggle::Clear | RuleSetToggle::Set(_)) { - if token_standard != TokenStandard::ProgrammableNonFungible { - return Err(MetadataError::InvalidTokenStandard.into()); - } + // if the rule_set data is either 'Set' or 'Clear', only allow updating if the + // token standard is equal to `ProgrammableNonFungible` and no SPL delegate is set. + if matches!(args.rule_set, RuleSetToggle::Clear | RuleSetToggle::Set(_)) { + if token_standard != TokenStandard::ProgrammableNonFungible { + return Err(MetadataError::InvalidTokenStandard.into()); + } - // Require the token so we can check if it has a token delegate. - let token = token.ok_or(MetadataError::MissingTokenAccount)?; + // Require the token so we can check if it has a token delegate. + let token = token.ok_or(MetadataError::MissingTokenAccount)?; - // If the token has a delegate, we cannot update the rule set. - if token.delegate.is_some() { - return Err(MetadataError::CannotUpdateAssetWithDelegate.into()); - } + // If the token has a delegate, we cannot update the rule set. + if token.delegate.is_some() { + return Err(MetadataError::CannotUpdateAssetWithDelegate.into()); + } - self.programmable_config = - rule_set.to_option().map(|rule_set| ProgrammableConfig::V1 { + self.programmable_config = + args.rule_set + .clone() + .to_option() + .map(|rule_set| ProgrammableConfig::V1 { rule_set: Some(rule_set), }); - } } + // Re-serialize metadata. puff_out_data_fields(self); - clean_write_metadata(self, metadata)?; - - Ok(()) + clean_write_metadata(self, metadata) } pub fn into_asset_data(self) -> AssetData { diff --git a/token-metadata/program/src/state/programmable.rs b/token-metadata/program/src/state/programmable.rs index 1a5f55a1b7..91023135b3 100644 --- a/token-metadata/program/src/state/programmable.rs +++ b/token-metadata/program/src/state/programmable.rs @@ -219,6 +219,8 @@ pub struct AuthorityRequest<'a, 'b> { pub metadata_delegate_record_info: Option<&'a AccountInfo<'a>>, /// Expected `MetadataDelegateRole` for the request. pub metadata_delegate_roles: Vec, + /// Expected collection-level `MetadataDelegateRole` for the request. + pub collection_metadata_delegate_roles: Vec, /// `TokenRecord` account. pub token_record_info: Option<&'a AccountInfo<'a>>, /// Expected `TokenDelegateRole` for the request. @@ -242,6 +244,7 @@ impl<'a, 'b> Default for AuthorityRequest<'a, 'b> { token_account: None, metadata_delegate_record_info: None, metadata_delegate_roles: Vec::with_capacity(0), + collection_metadata_delegate_roles: Vec::with_capacity(0), token_record_info: None, token_delegate_roles: Vec::with_capacity(0), } @@ -347,12 +350,14 @@ impl AuthorityType { }); } } + } - // looking up the delegate on the collection mint (this is for - // collection-level delegates) - if let Some(mint) = request.collection_mint { + // looking up the delegate on the collection mint (this is for + // collection-level delegates) + if let Some(collection_mint) = request.collection_mint { + for role in &request.collection_metadata_delegate_roles { let (pda_key, _) = find_metadata_delegate_record_account( - mint, + collection_mint, *role, request.update_authority, request.authority, diff --git a/token-metadata/program/src/utils/mod.rs b/token-metadata/program/src/utils/mod.rs index ffb1b6c403..ed05ae1eb7 100644 --- a/token-metadata/program/src/utils/mod.rs +++ b/token-metadata/program/src/utils/mod.rs @@ -69,6 +69,11 @@ pub fn check_token_standard( } } +pub fn mint_decimals_is_zero(mint_info: &AccountInfo) -> Result { + let mint_decimals = get_mint_decimals(mint_info)?; + Ok(mint_decimals == 0) +} + pub fn is_master_edition( edition_account_info: &AccountInfo, mint_decimals: u8, diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index 8f191146b9..8c602c8fe7 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -1262,12 +1262,12 @@ mod unverify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_unverify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_unverify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify( AssetToDelegate::CollectionParent, @@ -1310,12 +1310,12 @@ mod unverify_collection { } #[tokio::test] - async fn items_update_delegate_cannot_unverify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn items_collection_item_delegate_cannot_unverify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify( AssetToDelegate::Item, @@ -1908,12 +1908,14 @@ mod unverify_collection { .await .unwrap(); - let mut args = UpdateArgs::default(); - let UpdateArgs::V1 { - new_update_authority, - .. - } = &mut args; - *new_update_authority = Some(new_collection_update_authority.pubkey()); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + .. + } => *new_update_authority = Some(new_collection_update_authority.pubkey()), + _ => panic!("Unexpected enum variant"), + } let payer = context.payer.dirty_clone(); test_items @@ -1982,12 +1984,14 @@ mod unverify_collection { // Change the collection to have a different update authority. let new_collection_update_authority = Keypair::new(); - let mut args = UpdateArgs::default(); - let UpdateArgs::V1 { - new_update_authority, - .. - } = &mut args; - *new_update_authority = Some(new_collection_update_authority.pubkey()); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + .. + } => *new_update_authority = Some(new_collection_update_authority.pubkey()), + _ => panic!("Unexpected enum variant"), + } let payer = context.payer.dirty_clone(); test_items @@ -2036,7 +2040,7 @@ mod unverify_collection { } #[tokio::test] - async fn pass_unverify_burned_pnft_parent_using_item_update_delegate() { + async fn pass_unverify_burned_pnft_parent_using_item_collection_delegate() { let mut context = program_test().start_with_context().await; let mut test_items = create_mint_verify_collection_check( @@ -2071,7 +2075,7 @@ mod unverify_collection { let payer = context.payer.dirty_clone(); let payer_pubkey = payer.pubkey(); - let delegate_args = DelegateArgs::UpdateV1 { + let delegate_args = DelegateArgs::CollectionV1 { authorization_data: None, }; test_items @@ -2083,7 +2087,7 @@ mod unverify_collection { // Find delegate record PDA. let (delegate_record, _) = find_metadata_delegate_record_account( &test_items.da.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::Collection, &payer_pubkey, &delegate.pubkey(), ); @@ -2111,28 +2115,87 @@ mod unverify_collection { } #[tokio::test] - async fn collections_collection_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::CollectionV1 { + async fn pass_unverify_burned_pnft_parent_using_item_collection_item_delegate() { + let mut context = program_test().start_with_context().await; + + let mut test_items = create_mint_verify_collection_check( + &mut context, + DEFAULT_COLLECTION_DETAILS, + TokenStandard::ProgrammableNonFungible, + TokenStandard::ProgrammableNonFungible, + ) + .await; + + // Burn collection parent. + let args = BurnArgs::V1 { amount: 1 }; + let payer = context.payer.dirty_clone(); + test_items + .collection_parent_da + .burn(&mut context, payer, args, None, None) + .await + .unwrap(); + + // Assert that metadata, edition, token and token record accounts are closed. + test_items + .collection_parent_da + .assert_burned(&mut context) + .await + .unwrap(); + + // Create a metadata update delegate for the item. + let delegate = Keypair::new(); + airdrop(&mut context, &delegate.pubkey(), LAMPORTS_PER_SOL) + .await + .unwrap(); + + let payer = context.payer.dirty_clone(); + let payer_pubkey = payer.pubkey(); + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; + test_items + .da + .delegate(&mut context, payer, delegate.pubkey(), delegate_args) + .await + .unwrap(); - let delegate_role = MetadataDelegateRole::Collection; + // Find delegate record PDA. + let (delegate_record, _) = find_metadata_delegate_record_account( + &test_items.da.mint.pubkey(), + MetadataDelegateRole::CollectionItem, + &payer_pubkey, + &delegate.pubkey(), + ); - other_metadata_delegates_cannot_unverify_burned_pnft_parent( - AssetToDelegate::CollectionParent, - delegate_args, - delegate_role, - ) - .await; + // Unverify. + let args = VerificationArgs::CollectionV1; + test_items + .da + .unverify( + &mut context, + delegate, + args, + None, + Some(delegate_record), + Some(test_items.collection_parent_da.mint.pubkey()), + Some(test_items.collection_parent_da.metadata), + ) + .await + .unwrap(); + + test_items + .da + .assert_item_collection_matches_on_chain(&mut context, &test_items.collection) + .await; } #[tokio::test] - async fn collections_update_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_delegate_cannot_unverify_burned_pnft_parent() { + let delegate_args = DelegateArgs::CollectionV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::Collection; other_metadata_delegates_cannot_unverify_burned_pnft_parent( AssetToDelegate::CollectionParent, @@ -2143,12 +2206,12 @@ mod unverify_collection { } #[tokio::test] - async fn collections_prgm_config_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::ProgrammableConfigV1 { + async fn collections_collection_item_delegate_cannot_unverify_burned_pnft_parent() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::ProgrammableConfig; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify_burned_pnft_parent( AssetToDelegate::CollectionParent, @@ -2159,15 +2222,15 @@ mod unverify_collection { } #[tokio::test] - async fn items_collection_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::CollectionV1 { + async fn collections_prgm_config_delegate_cannot_unverify_burned_pnft_parent() { + let delegate_args = DelegateArgs::ProgrammableConfigV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Collection; + let delegate_role = MetadataDelegateRole::ProgrammableConfig; other_metadata_delegates_cannot_unverify_burned_pnft_parent( - AssetToDelegate::Item, + AssetToDelegate::CollectionParent, delegate_args, delegate_role, ) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index c94a6ecc85..02dcfe0fd1 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -2,42 +2,2021 @@ pub mod utils; use mpl_token_metadata::{ - instruction::{builders::UpdateBuilder, InstructionBuilder}, + error::MetadataError, + instruction::{ + builders::UpdateBuilder, CollectionToggle, DelegateArgs, InstructionBuilder, RuleSetToggle, + TransferArgs, UpdateArgs, + }, + state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard}, state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, utils::puffed_out_string, }; use num_traits::FromPrimitive; +use solana_program::pubkey::Pubkey; use solana_program_test::*; use solana_sdk::{ instruction::InstructionError, + signature::Keypair, signature::Signer, transaction::{Transaction, TransactionError}, }; use utils::{DigitalAsset, *}; mod update { + use super::*; + + #[tokio::test] + async fn success_update_by_update_authority() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!( + metadata.data.name, + puffed_out_string(DEFAULT_NAME, MAX_NAME_LENGTH) + ); + assert_eq!( + metadata.data.symbol, + puffed_out_string(DEFAULT_SYMBOL, MAX_SYMBOL_LENGTH) + ); + assert_eq!( + metadata.data.uri, + puffed_out_string(DEFAULT_URI, MAX_URI_LENGTH) + ); + assert_eq!(metadata.update_authority, update_authority.pubkey()); + + let new_name = puffed_out_string("New Name", MAX_NAME_LENGTH); + let new_symbol = puffed_out_string("NEW", MAX_SYMBOL_LENGTH); + let new_uri = puffed_out_string("https://new.digital.asset.org", MAX_URI_LENGTH); + + // Change a few values and update the metadata. + let data = Data { + name: new_name.clone(), + symbol: new_symbol.clone(), + uri: new_uri.clone(), + creators: metadata.data.creators, // keep the same creators + seller_fee_basis_points: 0, + }; + + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(update_authority.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.data.name, new_name); + assert_eq!(metadata.data.symbol, new_symbol); + assert_eq!(metadata.data.uri, new_uri); + } + + #[tokio::test] + async fn success_update_by_items_authority_item_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.update_authority, update_authority.pubkey()); + assert!(!metadata.primary_sale_happened); + assert!(metadata.is_mutable); + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate( + context, + update_authority, + delegate.pubkey(), + DelegateArgs::AuthorityItemV1 { + authorization_data: None, + }, + ) + .await + .unwrap() + .unwrap(); + + // Change a few values that this delegate is allowed to change. + let mut args = UpdateArgs::default_as_authority_item_delegate(); + + match &mut args { + UpdateArgs::AsAuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + .. + } => { + *new_update_authority = Some(delegate.pubkey()); + *primary_sale_happened = Some(true); + *is_mutable = Some(false); + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.update_authority, delegate.pubkey()); + assert!(metadata.primary_sale_happened); + assert!(!metadata.is_mutable); + } + + #[tokio::test] + async fn success_update_by_items_collection_delegate() { + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + + let mut update_args = UpdateArgs::default_as_collection_delegate(); + match &mut update_args { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + success_update_collection_by_items_delegate(delegate_args, new_collection, update_args) + .await; + } + + #[tokio::test] + async fn success_update_by_items_collection_item_delegate() { + let delegate_args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + + let mut update_args = UpdateArgs::default_as_collection_item_delegate(); + match &mut update_args { + UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + success_update_collection_by_items_delegate(delegate_args, new_collection, update_args) + .await; + } + + async fn success_update_collection_by_items_delegate( + delegate_args: DelegateArgs, + collection: Collection, + update_args: UpdateArgs, + ) { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change the collection. + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.collection, Some(collection)); + } + + #[tokio::test] + async fn success_update_by_items_data_delegate() { + let delegate_args = DelegateArgs::DataV1 { + authorization_data: None, + }; + + success_update_data_by_items_delegate( + delegate_args, + UpdateArgs::default_as_data_delegate(), + ) + .await; + } + + #[tokio::test] + async fn success_update_by_items_data_item_delegate() { + let delegate_args = DelegateArgs::DataItemV1 { + authorization_data: None, + }; + + success_update_data_by_items_delegate( + delegate_args, + UpdateArgs::default_as_data_item_delegate(), + ) + .await; + } + + async fn success_update_data_by_items_delegate( + delegate_args: DelegateArgs, + mut update_args: UpdateArgs, + ) { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + // Check initial data and update authority. + let metadata = da.get_metadata(context).await; + assert_eq!( + metadata.data.name, + puffed_out_string(DEFAULT_NAME, MAX_NAME_LENGTH) + ); + assert_eq!( + metadata.data.symbol, + puffed_out_string(DEFAULT_SYMBOL, MAX_SYMBOL_LENGTH) + ); + assert_eq!( + metadata.data.uri, + puffed_out_string(DEFAULT_URI, MAX_URI_LENGTH) + ); + assert_eq!(metadata.update_authority, update_authority.pubkey()); + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change some data. + let new_name = puffed_out_string("New Name", MAX_NAME_LENGTH); + let new_symbol = puffed_out_string("NEW", MAX_SYMBOL_LENGTH); + let new_uri = puffed_out_string("https://new.digital.asset.org", MAX_URI_LENGTH); + let data = Data { + name: new_name.clone(), + symbol: new_symbol.clone(), + uri: new_uri.clone(), + creators: metadata.data.creators, // keep the same creators + seller_fee_basis_points: 0, + }; + + match &mut update_args { + UpdateArgs::AsDataDelegateV2 { + data: current_data, .. + } => *current_data = Some(data), + UpdateArgs::AsDataItemDelegateV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + }; + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // Check the updated data. + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.data.name, new_name); + assert_eq!(metadata.data.symbol, new_symbol); + assert_eq!(metadata.data.uri, new_uri); + } + + #[tokio::test] + async fn success_update_pfnt_config_by_update_authority() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // remove the rule set + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.programmable_config, None); + } + + #[tokio::test] + async fn fail_update_pfnt_config_token_and_mint_mismatch() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data.clone()), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create second digital asset. + let mut second_da = DigitalAsset::new(); + second_da + .create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Trying to remove the RuleSet from first digital asset. + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + // Send the wrong token. + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(second_da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::MintMismatch); + + // `RuleSet` should not have changed on first asset. + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + } + + #[tokio::test] + async fn fail_update_pfnt_config_metadata_and_mint_mismatch() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data.clone()), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create second digital asset. + let mut second_da = DigitalAsset::new(); + second_da + .create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Trying to remove the RuleSet from first digital asset. + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + // Send the wrong mint and wrong token so that we are checking mint against the metadata. + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(second_da.mint.pubkey()) + .token(second_da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::MintMismatch); + + // `RuleSet` should not have changed onf first asset. + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + } + + #[tokio::test] + async fn fail_update_pfnt_config_by_update_authority_wrong_edition() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data.clone()), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create second digital asset. + let mut second_da = DigitalAsset::new(); + second_da + .create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Trying to remove the RuleSet from first digital asset. + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(update_authority.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(update_authority.pubkey()); + + // Send the wrong edition. + if let Some(edition) = second_da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&update_authority.pubkey()), + &[&update_authority], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::DerivedKeyInvalid); + + // `RuleSet` should not have changed onf first asset. + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + } + + #[tokio::test] + async fn success_update_pnft_by_items_programmable_config_delegate() { + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + + let mut update_args = UpdateArgs::default_as_programmable_config_delegate(); + match &mut update_args { + UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + success_update_pnft_by_items_delegate(delegate_args, update_args).await; + } + + #[tokio::test] + async fn success_update_pnft_by_items_programmable_config_item_delegate() { + let delegate_args = DelegateArgs::ProgrammableConfigItemV1 { + authorization_data: None, + }; + + let mut update_args = UpdateArgs::default_as_programmable_config_item_delegate(); + match &mut update_args { + UpdateArgs::AsProgrammableConfigItemDelegateV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Clear + } + _ => panic!("Unexpected enum variant"), + } + + success_update_pnft_by_items_delegate(delegate_args, update_args).await; + } + + async fn success_update_pnft_by_items_delegate( + delegate_args: DelegateArgs, + update_args: UpdateArgs, + ) { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Change a value that this delegate is allowed to change. + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.programmable_config, None); + } + + #[tokio::test] + async fn fail_update_by_items_authority_item_delegate() { + let args = DelegateArgs::AuthorityItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_collection_delegate() { + let args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_data_delegate() { + let args = DelegateArgs::DataV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_programmable_config_delegate() { + let args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_data_item_delegate() { + let args = DelegateArgs::DataItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_collection_item_delegate() { + let args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + #[tokio::test] + async fn fail_update_by_items_programmable_config_item_delegate() { + let args = DelegateArgs::ProgrammableConfigItemV1 { + authorization_data: None, + }; + + fail_update_by_items_delegate(args).await; + } + + async fn fail_update_by_items_delegate(delegate_args: DelegateArgs) { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create rule-set for the transfer + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + ) + .await + .unwrap(); + + // Create metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Use update args variant that no delegates are allowed to use. + let update_args = UpdateArgs::default_as_update_authority(); + match update_args { + UpdateArgs::AsUpdateAuthorityV2 { .. } => (), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidUpdateArgs); + } + + #[tokio::test] + async fn fail_update_by_items_persistent_delegate() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + ) + .await + .unwrap(); + + // Create `TokenDelegate` type of delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::UtilityV1 { + amount: 1, + authorization_data: None, + }; + let delegate_record = da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Use update args variant that no delegates are allowed to use. + let update_args = UpdateArgs::default_as_update_authority(); + match update_args { + UpdateArgs::AsUpdateAuthorityV2 { .. } => (), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + } + + #[tokio::test] + async fn fail_update_by_holder() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create_and_mint( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + ) + .await + .unwrap(); + + // Transfer to a new holder. + let holder = Keypair::new(); + holder.airdrop(context, 1_000_000_000).await.unwrap(); + + let args = TransferArgs::V1 { + authorization_data: None, + amount: 1, + }; + + da.transfer(TransferParams { + context, + authority: &update_authority, + source_owner: &update_authority.pubkey(), + destination_owner: holder.pubkey(), + destination_token: None, // fn will create the ATA + payer: &update_authority, + authorization_rules: None, + args, + }) + .await + .unwrap(); + + // Attempt to update. There are no `AsHolder` update args available but + // we expect to fail before we get to the point of checking args anyways. + let update_args = UpdateArgs::default_as_update_authority(); + match update_args { + UpdateArgs::AsUpdateAuthorityV2 { .. } => (), + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(holder.pubkey()) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .payer(holder.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&holder.pubkey()), + &[&holder], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::FeatureNotSupported); + } + + #[tokio::test] + async fn success_update_token_standard() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::FungibleAsset, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); + + // Update token standard + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } => { + *token_standard = Some(TokenStandard::Fungible) + } + _ => panic!("Unexpected enum variant"), + } + + da.update(context, update_authority.dirty_clone(), args) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::Fungible)); + } + + #[tokio::test] + async fn success_update_token_standard_to_same() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::NonFungible)); + + // Update token standard + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } => { + *token_standard = Some(TokenStandard::NonFungible) + } + _ => panic!("Unexpected enum variant"), + } + + da.update(context, update_authority.dirty_clone(), args) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::NonFungible)); + } + + #[tokio::test] + async fn fail_invalid_update_token_standard() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::FungibleAsset, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); + + // Update token standard + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { token_standard, .. } => { + *token_standard = Some(TokenStandard::NonFungible) + } + _ => panic!("Unexpected enum variant"), + } + + let err = da + .update(context, update_authority.dirty_clone(), args) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidTokenStandard); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.token_standard, Some(TokenStandard::FungibleAsset)); + } + + #[tokio::test] + async fn fail_update_to_verified_collection() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + // This creates with update authority as a verified creator. + da.create_and_mint(context, TokenStandard::FungibleAsset, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Set collection to a value with verified set to true. + let new_collection = Collection { + verified: true, + key: Keypair::new().pubkey(), + }; + + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let err = da + .update(context, update_authority.dirty_clone(), args) + .await + .unwrap_err(); + + assert_custom_error!( + err, + MetadataError::CollectionCannotBeVerifiedInThisInstruction + ); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + + #[tokio::test] + async fn success_update_collection_by_collections_collection_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + let mut args = UpdateArgs::default_as_collection_delegate(); + match &mut args { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // Check that collection changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, Some(new_collection)); + } + + #[tokio::test] + async fn fail_update_collection_by_collections_collection_item_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + let mut args = UpdateArgs::default_as_collection_item_delegate(); + match &mut args { + UpdateArgs::AsCollectionItemDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + + #[tokio::test] + async fn fail_update_collection_delegate_update_authority_mismatch() { + let context = &mut program_test().start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Change the collection to have a different update authority. + let new_collection_update_authority = Keypair::new(); + new_collection_update_authority + .airdrop(context, 1_000_000_000) + .await + .unwrap(); + + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + .. + } => *new_update_authority = Some(new_collection_update_authority.pubkey()), + _ => panic!("Unexpected enum variant"), + } + + let payer = context.payer.dirty_clone(); + collection_parent_da + .update(context, payer, args) + .await + .unwrap(); + + // Verify update authority is changed. + let metadata = collection_parent_da.get_metadata(context).await; + assert_eq!( + metadata.update_authority, + new_collection_update_authority.pubkey() + ); + + // Verify cannot create metadata delegate on the collection using the old update authority. + let old_update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let fail_delegate = Keypair::new(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let err = collection_parent_da + .delegate( + context, + old_update_authority, + fail_delegate.pubkey(), + delegate_args, + ) + .await + .unwrap_err(); + + assert_custom_error_ix!(1, err, MetadataError::UpdateAuthorityIncorrect); + + // Create metadata delegate on the collection using the new update authority. + let pass_delegate = Keypair::new(); + pass_delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + let pass_delegate_record = collection_parent_da + .delegate( + context, + new_collection_update_authority, + pass_delegate.pubkey(), + delegate_args, + ) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + let mut args = UpdateArgs::default_as_collection_delegate(); + match &mut args { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(pass_delegate.pubkey()) + .delegate_record(pass_delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(pass_delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&pass_delegate.pubkey()), + &[&pass_delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + + #[tokio::test] + async fn fail_update_collection_by_collections_programmable_config_delegate() { + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_delegate(delegate_args).await + } + + #[tokio::test] + async fn fail_update_collection_by_collections_data_delegate() { + let delegate_args = DelegateArgs::DataV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_delegate(delegate_args).await + } + + async fn fail_update_collection_by_collections_delegate(delegate_args: DelegateArgs) { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; + + let mut args = UpdateArgs::default_as_collection_delegate(); + match &mut args { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidUpdateArgs); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } + + #[tokio::test] + async fn fail_update_collection_by_collections_programmable_config_item_delegate() { + let delegate_args = DelegateArgs::ProgrammableConfigItemV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_item_delegate(delegate_args).await + } + + #[tokio::test] + async fn fail_update_collection_by_collections_data_item_delegate() { + let delegate_args = DelegateArgs::DataItemV1 { + authorization_data: None, + }; + + fail_update_collection_by_collections_item_delegate(delegate_args).await + } + + async fn fail_update_collection_by_collections_item_delegate(delegate_args: DelegateArgs) { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create and mint item. + let mut da = DigitalAsset::new(); + da.create_and_mint(context, TokenStandard::NonFungible, None, None, 1) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Change collection. + let new_collection = Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }; - use mpl_token_metadata::{ - error::MetadataError, - instruction::{DelegateArgs, RuleSetToggle, UpdateArgs}, - state::{Creator, Data, ProgrammableConfig, TokenStandard}, - }; - use solana_program::pubkey::Pubkey; - use solana_sdk::signature::Keypair; + let mut args = UpdateArgs::default_as_collection_delegate(); + match &mut args { + UpdateArgs::AsCollectionDelegateV2 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } - use super::*; + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + } #[tokio::test] - async fn success_update() { - let context = &mut program_test().start_with_context().await; + async fn success_update_prog_config_by_collections_prog_config_delegate_v2_args() { + // Change programmable config, removing the RuleSet. + let mut args = UpdateArgs::default_as_programmable_config_delegate(); + match &mut args { + UpdateArgs::AsProgConfigDelegateV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + success_update_prog_config_by_collections_prog_config_delegate(args).await + } + + #[tokio::test] + async fn success_update_prog_config_by_collections_prog_config_delegate_v1_args() { + // Change programmable config, removing the RuleSet. + let mut args = UpdateArgs::default_v1(); + match &mut args { + UpdateArgs::V1 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } + + success_update_prog_config_by_collections_prog_config_delegate(args).await + } + + async fn success_update_prog_config_by_collections_prog_config_delegate( + update_args: UpdateArgs, + ) { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); let mut da = DigitalAsset::new(); - da.create(context, TokenStandard::NonFungible, None) + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + // Check collection. + assert_eq!(metadata.collection, collection); + + // Check programmable config. + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.programmable_config, None); + } + + #[tokio::test] + async fn success_update_data_by_collections_data_delegate() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::DataV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) .await + .unwrap() .unwrap(); + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = DigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + + // Check collection. + assert_eq!(metadata.collection, collection); + + // Check data and update authority. let metadata = da.get_metadata(context).await; assert_eq!( metadata.data.name, @@ -51,13 +2030,12 @@ mod update { metadata.data.uri, puffed_out_string(DEFAULT_URI, MAX_URI_LENGTH) ); - assert_eq!(metadata.update_authority, update_authority.pubkey()); + assert_eq!(metadata.update_authority, context.payer.pubkey()); + // Change some data. let new_name = puffed_out_string("New Name", MAX_NAME_LENGTH); let new_symbol = puffed_out_string("NEW", MAX_SYMBOL_LENGTH); let new_uri = puffed_out_string("https://new.digital.asset.org", MAX_URI_LENGTH); - - // Change a few values and update the metadata. let data = Data { name: new_name.clone(), symbol: new_symbol.clone(), @@ -66,35 +2044,40 @@ mod update { seller_fee_basis_points: 0, }; - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let mut args = UpdateArgs::default_as_data_delegate(); + match &mut args { + UpdateArgs::AsDataDelegateV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder - .authority(update_authority.pubkey()) + .authority(delegate.pubkey()) + .delegate_record(delegate_record) .metadata(da.metadata) .mint(da.mint.pubkey()) - .payer(update_authority.pubkey()); + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); if let Some(edition) = da.edition { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], - Some(&update_authority.pubkey()), - &[&update_authority], + Some(&delegate.pubkey()), + &[&delegate], context.last_blockhash, ); context.banks_client.process_transaction(tx).await.unwrap(); - // checks the created metadata values + // Check the updated data. let metadata = da.get_metadata(context).await; assert_eq!(metadata.data.name, new_name); @@ -103,74 +2086,229 @@ mod update { } #[tokio::test] - async fn update_pfnt_config() { + async fn fail_update_prog_config_by_col_prog_config_delegate_wrong_v1_args() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); let context = &mut program_test.start_with_context().await; - let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); let (authorization_rules, auth_data) = create_default_metaplex_rule_set(context, authority, false).await; - let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); let mut da = DigitalAsset::new(); - da.create_and_mint( + da.create_and_mint_item_with_collection( context, TokenStandard::ProgrammableNonFungible, Some(authorization_rules), Some(auth_data), 1, + collection.clone(), ) .await .unwrap(); + // Check collection. let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, collection); - if let Some(ProgrammableConfig::V1 { - rule_set: Some(rule_set), - }) = metadata.programmable_config - { - assert_eq!(rule_set, authorization_rules); - } else { - panic!("Missing rule set programmable config"); + // Check primary sale. + let metadata = da.get_metadata(context).await; + assert!(!metadata.primary_sale_happened); + + // Collection-level programmable config delegate is allowed to use V1 args for backwards + // compatibility. But RuleSet is the only allowed field this delegate can change. + let mut args = UpdateArgs::default_v1(); + match &mut args { + UpdateArgs::V1 { + primary_sale_happened, + .. + } => *primary_sale_happened = Some(true), + _ => panic!("Unexpected enum variant"), } - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - // remove the rule set - *rule_set = RuleSetToggle::Clear; - let mut builder = UpdateBuilder::new(); builder - .authority(update_authority.pubkey()) + .authority(delegate.pubkey()) + .delegate_record(delegate_record) .metadata(da.metadata) .mint(da.mint.pubkey()) .token(da.token.unwrap()) .authorization_rules(authorization_rules) - .payer(update_authority.pubkey()); + .payer(delegate.pubkey()); if let Some(edition) = da.edition { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], - Some(&update_authority.pubkey()), - &[&update_authority], + Some(&delegate.pubkey()), + &[&delegate], context.last_blockhash, ); - context.banks_client.process_transaction(tx).await.unwrap(); + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); - // checks the created metadata values + assert_custom_error!(err, MetadataError::InvalidUpdateArgs); + + // Check that metadata not changed. let metadata = da.get_metadata(context).await; + assert!(!metadata.primary_sale_happened); + } - assert_eq!(metadata.programmable_config, None); + #[tokio::test] + async fn fail_update_by_col_prog_config_delegate_using_new_collection_in_v1_args() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = DigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + DEFAULT_COLLECTION_DETAILS, + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = + create_default_metaplex_rule_set(context, authority, false).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = DigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection.clone(), + ) + .await + .unwrap(); + + // Check collection. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, collection); + + let new_collection = Collection { + key: Keypair::new().pubkey(), + verified: false, + }; + + // Collection-level programmable config delegate is allowed to use V1 args for backwards + // compatibility. But RuleSet is the only allowed field this delegate can change. But it + // won't get to that point in the code because it will not be authorized as a collection- + // level delegate based on the new collection it sent in. + let mut args = UpdateArgs::default_v1(); + match &mut args { + UpdateArgs::V1 { collection, .. } => { + *collection = CollectionToggle::Set(new_collection.clone()) + } + _ => panic!("Unexpected enum variant"), + } + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidAuthorityType); + + // Check that collection not changed. + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, collection); } #[tokio::test] @@ -212,9 +2350,13 @@ mod update { panic!("Missing rule set programmable config"); } - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - *rule_set = RuleSetToggle::Set(invalid_rule_set); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Set(invalid_rule_set) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -228,7 +2370,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args.clone()).unwrap().instruction(); + let update_ix = builder.build(args.clone()).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -266,7 +2408,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -284,9 +2426,13 @@ mod update { assert_custom_error!(err, MetadataError::InvalidAuthorizationRules); // Finally, try to update with the valid rule set, and it should succeed. - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - *rule_set = RuleSetToggle::Set(authorization_rules); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Set(authorization_rules) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -301,7 +2447,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -338,7 +2484,7 @@ mod update { let (authorization_rules, auth_data) = create_default_metaplex_rule_set(context, authority.dirty_clone(), false).await; - let (new_auth_rules, new_auth_data) = + let (new_auth_rules, _) = create_default_metaplex_rule_set(context, authority.dirty_clone(), false).await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -381,10 +2527,11 @@ mod update { .unwrap(); // Try to clear the rule set. - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - // remove the rule set - *rule_set = RuleSetToggle::Clear; + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => *rule_set = RuleSetToggle::Clear, + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -399,7 +2546,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -417,15 +2564,13 @@ mod update { assert_custom_error!(err, MetadataError::CannotUpdateAssetWithDelegate); // Try to update the rule set. - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - rule_set, - authorization_data, - .. - } = &mut update_args; - // update the rule set - *rule_set = RuleSetToggle::Set(new_auth_rules); - *authorization_data = Some(new_auth_data); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { rule_set, .. } => { + *rule_set = RuleSetToggle::Set(new_auth_rules) + } + _ => panic!("Unexpected enum variant"), + } let mut builder = UpdateBuilder::new(); builder @@ -440,7 +2585,7 @@ mod update { builder.edition(edition); } - let update_ix = builder.build(update_args).unwrap().instruction(); + let update_ix = builder.build(args).unwrap().instruction(); let tx = Transaction::new_signed_with_payer( &[update_ix], @@ -499,14 +2644,16 @@ mod update { seller_fee_basis_points: 0, }; - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } let err = da - .update(context, update_authority.dirty_clone(), update_args) + .update(context, update_authority.dirty_clone(), args) .await .unwrap_err(); @@ -557,13 +2704,15 @@ mod update { seller_fee_basis_points: metadata.data.seller_fee_basis_points, }; - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } - da.update(context, update_authority.dirty_clone(), update_args) + da.update(context, update_authority.dirty_clone(), args) .await .unwrap(); @@ -581,13 +2730,15 @@ mod update { seller_fee_basis_points: metadata.data.seller_fee_basis_points, }; - let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let mut args = UpdateArgs::default_as_update_authority(); + match &mut args { + UpdateArgs::AsUpdateAuthorityV2 { + data: current_data, .. + } => *current_data = Some(data), + _ => panic!("Unexpected enum variant"), + } - da.update(context, update_authority.dirty_clone(), update_args) + da.update(context, update_authority.dirty_clone(), args) .await .unwrap(); diff --git a/token-metadata/program/tests/update_with_old_lib.rs b/token-metadata/program/tests/update_with_old_lib.rs new file mode 100644 index 0000000000..5c66aea26a --- /dev/null +++ b/token-metadata/program/tests/update_with_old_lib.rs @@ -0,0 +1,609 @@ +#![cfg(feature = "test-bpf")] + +use mpl_token_auth_rules::{ + instruction::{ + builders::CreateOrUpdateBuilder, CreateOrUpdateArgs, + InstructionBuilder as AuthRulesInstructionBuilder, + }, + payload::Payload, + state::{CompareOp, Rule, RuleSetV1}, +}; +use old_token_metadata::{ + id, + instruction::{ + builders::{CreateBuilder, DelegateBuilder, MintBuilder, UpdateBuilder}, + CreateArgs, DelegateArgs, InstructionBuilder, MetadataDelegateRole, MintArgs, + RuleSetToggle, UpdateArgs, + }, + pda::{find_metadata_delegate_record_account, find_token_record_account}, + processor::{AuthorizationData, TransferScenario}, + state::{ + AssetData, Collection, CollectionDetails, Creator, Metadata, Operation, PayloadKey, + PrintSupply, ProgrammableConfig, TokenMetadataAccount, TokenStandard, EDITION, PREFIX, + }, +}; +use rmp_serde::Serializer; +use serde::Serialize; +use solana_program::{borsh::try_from_slice_unchecked, pubkey::Pubkey}; +use solana_program_test::*; +use solana_sdk::{ + account::Account, + compute_budget::ComputeBudgetInstruction, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; + +// This tests backwards compatibility of v1.10 changes with an older version of Token Metadata. +// It uses the binary created from the v1.10 version of token metadata but imports older instructions from +// the 1.9.1 version to ensure that the old instructions still work. + +// Note that to avoid version conflicts, requied test utilities are re-implemented in this file, including +// an `OldDigitalAsset` struct that is a limited version of `DigitalAsset` and compatible with 1.9.1. + +mod update { + use super::*; + + #[tokio::test] + async fn old_lib_success_update_by_collections_programmable_config_delegate() { + let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); + program_test.add_program("mpl_token_auth_rules", mpl_token_auth_rules::ID, None); + let context = &mut program_test.start_with_context().await; + + // Create a collection parent NFT or pNFT with the CollectionDetails struct populated. + let mut collection_parent_da = OldDigitalAsset::new(); + collection_parent_da + .create_and_mint_collection_parent( + context, + TokenStandard::ProgrammableNonFungible, + None, + None, + 1, + Some(CollectionDetails::V1 { size: 0 }), + ) + .await + .unwrap(); + + // Create metadata delegate on the collection. + let delegate = Keypair::new(); + airdrop(context, &delegate.pubkey(), 1_000_000_000) + .await + .unwrap(); + let delegate_args = DelegateArgs::ProgrammableConfigV1 { + authorization_data: None, + }; + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let delegate_record = collection_parent_da + .delegate(context, update_authority, delegate.pubkey(), delegate_args) + .await + .unwrap() + .unwrap(); + + // Create rule-set for the transfer + let authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let (authorization_rules, auth_data) = create_rule_set(context, authority).await; + + // Create and mint item with a collection. THIS IS NEEDED so that the collection-level + // delegate is authorized for this item. + let collection = Some(Collection { + key: collection_parent_da.mint.pubkey(), + verified: false, + }); + + let mut da = OldDigitalAsset::new(); + da.create_and_mint_item_with_collection( + context, + TokenStandard::ProgrammableNonFungible, + Some(authorization_rules), + Some(auth_data), + 1, + collection, + ) + .await + .unwrap(); + + // Check programmable config. + let metadata = da.get_metadata(context).await; + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + assert_eq!(rule_set, authorization_rules); + } else { + panic!("Missing rule set programmable config"); + } + + // Change programmable config. + let mut update_args = UpdateArgs::default(); + let UpdateArgs::V1 { rule_set, .. } = &mut update_args; + // remove the rule set + *rule_set = RuleSetToggle::Clear; + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .token(da.token.unwrap()) + .authorization_rules(authorization_rules) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.programmable_config, None); + } +} + +async fn airdrop( + context: &mut ProgramTestContext, + receiver: &Pubkey, + amount: u64, +) -> Result<(), BanksClientError> { + let tx = Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &context.payer.pubkey(), + receiver, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + Ok(()) +} + +async fn get_account(context: &mut ProgramTestContext, pubkey: &Pubkey) -> Account { + context + .banks_client + .get_account(*pubkey) + .await + .expect("account not found") + .expect("account empty") +} + +// This represents a generic Metaplex Digital asset of various Token Standards. +// It is used to abstract away the various accounts that are created for a given +// Digital Asset. Since different asset types have different accounts, care +// should be taken that appropriate handlers update appropriate accounts, such as when +// transferring a DigitalAsset, the token account should be updated. +struct OldDigitalAsset { + pub metadata: Pubkey, + pub mint: Keypair, + pub token: Option, + pub edition: Option, + pub token_record: Option, + pub token_standard: Option, +} + +impl Default for OldDigitalAsset { + fn default() -> Self { + Self::new() + } +} + +impl OldDigitalAsset { + fn new() -> Self { + let mint = Keypair::new(); + let mint_pubkey = mint.pubkey(); + let program_id = id(); + + let metadata_seeds = &[PREFIX.as_bytes(), program_id.as_ref(), mint_pubkey.as_ref()]; + let (metadata, _) = Pubkey::find_program_address(metadata_seeds, &program_id); + + Self { + metadata, + mint, + token: None, + edition: None, + token_record: None, + token_standard: None, + } + } + + async fn create_and_mint_item_with_collection( + &mut self, + context: &mut ProgramTestContext, + token_standard: TokenStandard, + authorization_rules: Option, + authorization_data: Option, + amount: u64, + collection: Option, + ) -> Result<(), BanksClientError> { + // creates the metadata + self.create_advanced( + context, + token_standard, + String::from("Old Digital Asset"), + String::from("DA"), + String::from("https://digital.asset.org"), + 500, + None, + collection, + None, + authorization_rules, + PrintSupply::Zero, + ) + .await + .unwrap(); + + // mints tokens + self.mint(context, authorization_rules, authorization_data, amount) + .await + } + + async fn create_and_mint_collection_parent( + &mut self, + context: &mut ProgramTestContext, + token_standard: TokenStandard, + authorization_rules: Option, + authorization_data: Option, + amount: u64, + collection_details: Option, + ) -> Result<(), BanksClientError> { + // creates the metadata + self.create_advanced( + context, + token_standard, + String::from("Old Digital Asset"), + String::from("DA"), + String::from("https://digital.asset.org"), + 500, + None, + None, + collection_details, + authorization_rules, + PrintSupply::Zero, + ) + .await + .unwrap(); + + // mints tokens + self.mint(context, authorization_rules, authorization_data, amount) + .await + } + + async fn create_advanced( + &mut self, + context: &mut ProgramTestContext, + token_standard: TokenStandard, + name: String, + symbol: String, + uri: String, + seller_fee_basis_points: u16, + creators: Option>, + collection: Option, + collection_details: Option, + authorization_rules: Option, + print_supply: PrintSupply, + ) -> Result<(), BanksClientError> { + let mut asset = AssetData::new(token_standard, name, symbol, uri); + asset.seller_fee_basis_points = seller_fee_basis_points; + asset.creators = creators; + asset.collection = collection; + asset.collection_details = collection_details; + asset.rule_set = authorization_rules; + + let payer_pubkey = context.payer.pubkey(); + let mint_pubkey = self.mint.pubkey(); + + let program_id = id(); + let mut builder = CreateBuilder::new(); + builder + .metadata(self.metadata) + .mint(self.mint.pubkey()) + .authority(payer_pubkey) + .payer(payer_pubkey) + .update_authority(payer_pubkey) + .initialize_mint(true) + .update_authority_as_signer(true); + + let edition = match token_standard { + TokenStandard::NonFungible | TokenStandard::ProgrammableNonFungible => { + // master edition PDA address + let edition_seeds = &[ + PREFIX.as_bytes(), + program_id.as_ref(), + mint_pubkey.as_ref(), + EDITION.as_bytes(), + ]; + let (edition, _) = Pubkey::find_program_address(edition_seeds, &id()); + // sets the master edition to the builder + builder.master_edition(edition); + Some(edition) + } + _ => None, + }; + // builds the instruction + let create_ix = builder + .build(CreateArgs::V1 { + asset_data: asset, + decimals: Some(0), + print_supply: Some(print_supply), + }) + .unwrap() + .instruction(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(800_000); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, create_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &self.mint], + context.last_blockhash, + ); + + self.edition = edition; + self.token_standard = Some(token_standard); + + context.banks_client.process_transaction(tx).await + } + + async fn mint( + &mut self, + context: &mut ProgramTestContext, + authorization_rules: Option, + authorization_data: Option, + amount: u64, + ) -> Result<(), BanksClientError> { + let payer_pubkey = context.payer.pubkey(); + let (token, _) = Pubkey::find_program_address( + &[ + &payer_pubkey.to_bytes(), + &spl_token::id().to_bytes(), + &self.mint.pubkey().to_bytes(), + ], + &spl_associated_token_account::id(), + ); + + let (token_record, _) = find_token_record_account(&self.mint.pubkey(), &token); + + let token_record_opt = if self.is_pnft(context).await { + Some(token_record) + } else { + None + }; + + let mut builder = MintBuilder::new(); + builder + .token(token) + .token_record(token_record) + .token_owner(payer_pubkey) + .metadata(self.metadata) + .mint(self.mint.pubkey()) + .payer(payer_pubkey) + .authority(payer_pubkey); + + if let Some(edition) = self.edition { + builder.master_edition(edition); + } + + if let Some(authorization_rules) = authorization_rules { + builder.authorization_rules(authorization_rules); + } + + let mint_ix = builder + .build(MintArgs::V1 { + amount, + authorization_data, + }) + .unwrap() + .instruction(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(800_000); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, mint_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.map(|_| { + self.token = Some(token); + self.token_record = token_record_opt; + }) + } + + async fn delegate( + &mut self, + context: &mut ProgramTestContext, + payer: Keypair, + delegate: Pubkey, + args: DelegateArgs, + ) -> Result, BanksClientError> { + let mut builder = DelegateBuilder::new(); + builder + .delegate(delegate) + .mint(self.mint.pubkey()) + .metadata(self.metadata) + .payer(payer.pubkey()) + .authority(payer.pubkey()) + .spl_token_program(spl_token::ID); + + let mut delegate_or_token_record = None; + + match args { + // Token delegates. + DelegateArgs::SaleV1 { .. } + | DelegateArgs::TransferV1 { .. } + | DelegateArgs::UtilityV1 { .. } + | DelegateArgs::StakingV1 { .. } + | DelegateArgs::LockedTransferV1 { .. } => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + delegate_or_token_record = Some(token_record); + } + DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + DelegateArgs::CollectionV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Collection, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::UpdateV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Update, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::ProgrammableConfigV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfig, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + } + + if let Some(edition) = self.edition { + builder.master_edition(edition); + } + + if let Some(token) = self.token { + builder.token(token); + } + + // determines if we need to set the rule set + let metadata_account = get_account(context, &self.metadata).await; + let metadata: Metadata = try_from_slice_unchecked(&metadata_account.data).unwrap(); + + if let Some(ProgrammableConfig::V1 { + rule_set: Some(rule_set), + }) = metadata.programmable_config + { + builder.authorization_rules(rule_set); + builder.authorization_rules_program(mpl_token_auth_rules::ID); + } + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + + let delegate_ix = builder.build(args.clone()).unwrap().instruction(); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, delegate_ix], + Some(&payer.pubkey()), + &[&payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await?; + Ok(delegate_or_token_record) + } + + pub async fn get_metadata(&self, context: &mut ProgramTestContext) -> Metadata { + let metadata_account = context + .banks_client + .get_account(self.metadata) + .await + .unwrap() + .unwrap(); + + Metadata::safe_deserialize(&metadata_account.data).unwrap() + } + + async fn is_pnft(&self, context: &mut ProgramTestContext) -> bool { + let md = self.get_metadata(context).await; + if let Some(standard) = md.token_standard { + if standard == TokenStandard::ProgrammableNonFungible { + return true; + } + } + + false + } +} + +async fn create_rule_set( + context: &mut ProgramTestContext, + creator: Keypair, +) -> (Pubkey, AuthorizationData) { + let name = String::from("RuleSet"); + let (ruleset_addr, _ruleset_bump) = + mpl_token_auth_rules::pda::find_rule_set_address(creator.pubkey(), name.clone()); + + let nft_amount = Rule::Amount { + field: PayloadKey::Amount.to_string(), + amount: 1, + operator: CompareOp::Eq, + }; + + let owner_operation = Operation::Transfer { + scenario: TransferScenario::Holder, + }; + + let mut rule_set = RuleSetV1::new(name, creator.pubkey()); + rule_set + .add(owner_operation.to_string(), nft_amount) + .unwrap(); + + // Serialize the RuleSet using RMP serde. + let mut serialized_data = Vec::new(); + rule_set + .serialize(&mut Serializer::new(&mut serialized_data)) + .unwrap(); + + // Create a `create` instruction. + let create_ix = CreateOrUpdateBuilder::new() + .rule_set_pda(ruleset_addr) + .payer(creator.pubkey()) + .build(CreateOrUpdateArgs::V1 { + serialized_rule_set: serialized_data, + }) + .unwrap() + .instruction(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + + // Add it to a transaction. + let create_tx = Transaction::new_signed_with_payer( + &[compute_ix, create_ix], + Some(&creator.pubkey()), + &[&creator], + context.last_blockhash, + ); + + // Process the transaction. + context + .banks_client + .process_transaction(create_tx) + .await + .expect("creation should succeed"); + + // Client can add additional rules to the Payload but does not need to in this case. + let payload = Payload::new(); + let auth_data = AuthorizationData { payload }; + + (ruleset_addr, auth_data) +} diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index 4b19ea60e5..6eb85d7f61 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -560,7 +560,7 @@ impl DigitalAsset { payer: Keypair, delegate: Pubkey, args: DelegateArgs, - ) -> Result<(), BanksClientError> { + ) -> Result, BanksClientError> { let mut builder = DelegateBuilder::new(); builder .delegate(delegate) @@ -570,16 +570,10 @@ impl DigitalAsset { .authority(payer.pubkey()) .spl_token_program(spl_token::ID); + let mut delegate_or_token_record = None; + match args { - DelegateArgs::CollectionV1 { .. } => { - let (delegate_record, _) = find_metadata_delegate_record_account( - &self.mint.pubkey(), - MetadataDelegateRole::Collection, - &payer.pubkey(), - &delegate, - ); - builder.delegate_record(delegate_record); - } + // Token delegates. DelegateArgs::SaleV1 { .. } | DelegateArgs::TransferV1 { .. } | DelegateArgs::UtilityV1 { .. } @@ -588,15 +582,30 @@ impl DigitalAsset { let (token_record, _) = find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); builder.token_record(token_record); + delegate_or_token_record = Some(token_record); + } + DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + DelegateArgs::CollectionV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Collection, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } - DelegateArgs::UpdateV1 { .. } => { + DelegateArgs::DataV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::Data, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::ProgrammableConfigV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -606,8 +615,48 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::AuthorityItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::AuthorityItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::DataItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::DataItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::CollectionItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::CollectionItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); + } + DelegateArgs::ProgrammableConfigItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfigItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } - DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } } if let Some(edition) = self.edition { @@ -641,7 +690,8 @@ impl DigitalAsset { context.last_blockhash, ); - context.banks_client.process_transaction(tx).await + context.banks_client.process_transaction(tx).await?; + Ok(delegate_or_token_record) } pub async fn migrate( @@ -772,15 +822,7 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { - RevokeArgs::CollectionV1 => { - let (delegate_record, _) = find_metadata_delegate_record_account( - &self.mint.pubkey(), - MetadataDelegateRole::Collection, - &payer.pubkey(), - &delegate, - ); - builder.delegate_record(delegate_record); - } + // Token delegates. RevokeArgs::SaleV1 | RevokeArgs::TransferV1 | RevokeArgs::UtilityV1 @@ -791,10 +833,22 @@ impl DigitalAsset { find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); builder.token_record(token_record); } - RevokeArgs::UpdateV1 => { + RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + RevokeArgs::CollectionV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Collection, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::DataV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::Data, &payer.pubkey(), &delegate, ); @@ -809,7 +863,43 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } + RevokeArgs::AuthorityItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::AuthorityItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::DataItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::DataItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::CollectionItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::CollectionItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + + RevokeArgs::ProgrammableConfigItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfigItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } } if let Some(edition) = self.edition { diff --git a/token-metadata/program/tests/verify.rs b/token-metadata/program/tests/verify.rs index ad3ec1f5db..2536f8144b 100644 --- a/token-metadata/program/tests/verify.rs +++ b/token-metadata/program/tests/verify.rs @@ -2118,12 +2118,12 @@ mod verify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_verify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_verify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_verify( AssetToDelegate::CollectionParent, @@ -2162,12 +2162,12 @@ mod verify_collection { } #[tokio::test] - async fn items_update_delegate_cannot_verify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn items_collection_item_delegate_cannot_verify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_verify(AssetToDelegate::Item, delegate_args, delegate_role) .await;