From 433a2ddc3132bafd1de27bc56df99433df0f8b36 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:34:37 -0700 Subject: [PATCH] Add `updateV2` which allows adding/removing an asset from collection (#142) * Add ability to add/remove an asset from a collection * Regenerate clients * Add back check for V1 removing from collection * Validate permission to add to new collection * Update comments * Simplify condition * Kinobi default for updateV2 and initial tests * Add test changing collection using delegate * Add wrong collection tests and collection size checks * Additional change collection tests * More checks * Update JS SDK V1 to use updateV2 * Update updateV2 test to use SDK * Use SDK test helpers * Only increment num_minted on create * Add negative test cases * Reorder tests * Add more update delegate tests, rename tests, add asserts * Separate increment into two methods --- .../js/src/generated/instructions/index.ts | 1 + .../js/src/generated/instructions/updateV2.ts | 190 +++ clients/js/src/instructions/update.ts | 12 +- clients/js/test/update.test.ts | 4 +- clients/js/test/updateV2.test.ts | 1217 +++++++++++++++++ .../rust/src/generated/instructions/mod.rs | 2 + .../src/generated/instructions/update_v2.rs | 672 +++++++++ configs/kinobi.cjs | 13 + idls/mpl_core.json | 105 ++ programs/mpl-core/src/instruction.rs | 12 +- programs/mpl-core/src/processor/burn.rs | 2 +- programs/mpl-core/src/processor/create.rs | 3 +- programs/mpl-core/src/processor/mod.rs | 6 +- programs/mpl-core/src/processor/update.rs | 160 ++- programs/mpl-core/src/state/collection.rs | 13 +- .../mpl-core/src/state/update_authority.rs | 32 +- programs/mpl-core/src/utils.rs | 4 +- 17 files changed, 2386 insertions(+), 62 deletions(-) create mode 100644 clients/js/src/generated/instructions/updateV2.ts create mode 100644 clients/js/test/updateV2.test.ts create mode 100644 clients/rust/src/generated/instructions/update_v2.rs diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 6b62997e..f52941cd 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -34,5 +34,6 @@ export * from './updateCollectionV1'; export * from './updateExternalPluginAdapterV1'; export * from './updatePluginV1'; export * from './updateV1'; +export * from './updateV2'; export * from './writeCollectionExternalPluginAdapterDataV1'; export * from './writeExternalPluginAdapterDataV1'; diff --git a/clients/js/src/generated/instructions/updateV2.ts b/clients/js/src/generated/instructions/updateV2.ts new file mode 100644 index 00000000..cf28a63c --- /dev/null +++ b/clients/js/src/generated/instructions/updateV2.ts @@ -0,0 +1,190 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Option, + OptionOrNullable, + Pda, + PublicKey, + Signer, + TransactionBuilder, + none, + transactionBuilder, +} from '@metaplex-foundation/umi'; +import { + Serializer, + mapSerializer, + option, + string, + struct, + u8, +} from '@metaplex-foundation/umi/serializers'; +import { + ResolvedAccount, + ResolvedAccountsWithIndices, + getAccountMetasAndSigners, +} from '../shared'; +import { + BaseUpdateAuthority, + BaseUpdateAuthorityArgs, + getBaseUpdateAuthoritySerializer, +} from '../types'; + +// Accounts. +export type UpdateV2InstructionAccounts = { + /** The address of the asset */ + asset: PublicKey | Pda; + /** The collection to which the asset belongs */ + collection?: PublicKey | Pda; + /** The account paying for the storage fees */ + payer?: Signer; + /** The update authority or update authority delegate of the asset */ + authority?: Signer; + /** A new collection to which to move the asset */ + newCollection?: PublicKey | Pda; + /** The system program */ + systemProgram?: PublicKey | Pda; + /** The SPL Noop Program */ + logWrapper?: PublicKey | Pda; +}; + +// Data. +export type UpdateV2InstructionData = { + discriminator: number; + newName: Option; + newUri: Option; + newUpdateAuthority: Option; +}; + +export type UpdateV2InstructionDataArgs = { + newName?: OptionOrNullable; + newUri?: OptionOrNullable; + newUpdateAuthority?: OptionOrNullable; +}; + +export function getUpdateV2InstructionDataSerializer(): Serializer< + UpdateV2InstructionDataArgs, + UpdateV2InstructionData +> { + return mapSerializer< + UpdateV2InstructionDataArgs, + any, + UpdateV2InstructionData + >( + struct( + [ + ['discriminator', u8()], + ['newName', option(string())], + ['newUri', option(string())], + ['newUpdateAuthority', option(getBaseUpdateAuthoritySerializer())], + ], + { description: 'UpdateV2InstructionData' } + ), + (value) => ({ + ...value, + discriminator: 30, + newName: value.newName ?? none(), + newUri: value.newUri ?? none(), + newUpdateAuthority: value.newUpdateAuthority ?? none(), + }) + ) as Serializer; +} + +// Args. +export type UpdateV2InstructionArgs = UpdateV2InstructionDataArgs; + +// Instruction. +export function updateV2( + context: Pick, + input: UpdateV2InstructionAccounts & UpdateV2InstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey( + 'mplCore', + 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d' + ); + + // Accounts. + const resolvedAccounts = { + asset: { + index: 0, + isWritable: true as boolean, + value: input.asset ?? null, + }, + collection: { + index: 1, + isWritable: true as boolean, + value: input.collection ?? null, + }, + payer: { + index: 2, + isWritable: true as boolean, + value: input.payer ?? null, + }, + authority: { + index: 3, + isWritable: false as boolean, + value: input.authority ?? null, + }, + newCollection: { + index: 4, + isWritable: true as boolean, + value: input.newCollection ?? null, + }, + systemProgram: { + index: 5, + isWritable: false as boolean, + value: input.systemProgram ?? null, + }, + logWrapper: { + index: 6, + isWritable: false as boolean, + value: input.logWrapper ?? null, + }, + } satisfies ResolvedAccountsWithIndices; + + // Arguments. + const resolvedArgs: UpdateV2InstructionArgs = { ...input }; + + // Default values. + if (!resolvedAccounts.payer.value) { + resolvedAccounts.payer.value = context.payer; + } + if (!resolvedAccounts.systemProgram.value) { + resolvedAccounts.systemProgram.value = context.programs.getPublicKey( + 'splSystem', + '11111111111111111111111111111111' + ); + resolvedAccounts.systemProgram.isWritable = false; + } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values( + resolvedAccounts + ).sort((a, b) => a.index - b.index); + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners( + orderedAccounts, + 'programId', + programId + ); + + // Data. + const data = getUpdateV2InstructionDataSerializer().serialize( + resolvedArgs as UpdateV2InstructionDataArgs + ); + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0; + + return transactionBuilder([ + { instruction: { keys, programId, data }, signers, bytesCreatedOnChain }, + ]); +} diff --git a/clients/js/src/instructions/update.ts b/clients/js/src/instructions/update.ts index a7b949e4..c3bbd233 100644 --- a/clients/js/src/instructions/update.ts +++ b/clients/js/src/instructions/update.ts @@ -1,21 +1,21 @@ import { Context } from '@metaplex-foundation/umi'; import { CollectionV1, - updateV1, AssetV1, - UpdateV1InstructionDataArgs, + UpdateV2InstructionDataArgs, + updateV2, } from '../generated'; import { findExtraAccounts } from '../plugins'; import { deriveExternalPluginAdapters } from '../helpers'; export type UpdateArgs = Omit< - Parameters[1], + Parameters[1], 'asset' | 'collection' | 'newName' | 'newUri' > & { asset: Pick; collection?: Pick; - name?: UpdateV1InstructionDataArgs['newName']; - uri?: UpdateV1InstructionDataArgs['newUri']; + name?: UpdateV2InstructionDataArgs['newName']; + uri?: UpdateV2InstructionDataArgs['newUri']; }; export const update = ( @@ -38,7 +38,7 @@ export const update = ( } ); - return updateV1(context, { + return updateV2(context, { ...args, asset: asset.publicKey, collection: collection?.publicKey, diff --git a/clients/js/test/update.test.ts b/clients/js/test/update.test.ts index 506d823b..d773f4b8 100644 --- a/clients/js/test/update.test.ts +++ b/clients/js/test/update.test.ts @@ -181,7 +181,7 @@ test('it can update an asset update authority', async (t) => { }); }); -test('it cannot update an asset update authority to be part of a collection (right now)', async (t) => { +test('it cannot update an asset update authority to be part of a collection using updateV1', async (t) => { // Given a Umi instance and a new signer. const umi = await createUmi(); const asset = await createAsset(umi); @@ -199,7 +199,7 @@ test('it cannot update an asset update authority to be part of a collection (rig await t.throwsAsync(result, { name: 'NotAvailable' }); }); -test('it cannot remove an asset from a collection (right now)', async (t) => { +test('it cannot remove an asset from a collection using updateV1', async (t) => { // Given a Umi instance and a new signer. const umi = await createUmi(); const { asset, collection } = await createAssetWithCollection(umi, {}, {}); diff --git a/clients/js/test/updateV2.test.ts b/clients/js/test/updateV2.test.ts new file mode 100644 index 00000000..c22a267c --- /dev/null +++ b/clients/js/test/updateV2.test.ts @@ -0,0 +1,1217 @@ +import test from 'ava'; + +import { generateSigner } from '@metaplex-foundation/umi'; +import { + update, + updateAuthority, + updateCollection, + addCollectionPlugin, + approveCollectionPluginAuthority, +} from '../src'; +import { + assertAsset, + assertCollection, + createUmi, + DEFAULT_ASSET, + DEFAULT_COLLECTION, +} from './_setupRaw'; + +import { + createAsset, + createAssetWithCollection, + createCollection, +} from './_setupSdk'; + +test('it can update an asset to be larger', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi); + + await update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); +}); + +test('it cannot update an asset using asset as authority', async (t) => { + const umi = await createUmi(); + const myAsset = generateSigner(umi); + + const asset = await createAsset(umi, { + asset: myAsset, + name: 'short', + uri: 'https://short.com', + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + authority: myAsset, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + name: 'short', + uri: 'https://short.com', + }); +}); + +test('it can update an asset to be smaller', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi); + + await update(umi, { + asset, + name: '', + uri: '', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + name: '', + uri: '', + }); +}); + +test('it can update an asset with plugins to be larger', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi, { + name: 'short', + uri: 'https://short.com', + plugins: [ + { + type: 'FreezeDelegate', + frozen: false, + }, + ], + }); + + await update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + freezeDelegate: { + authority: { + type: 'Owner', + }, + frozen: false, + }, + }); +}); + +test('it can update an asset with plugins to be smaller', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi, { + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + plugins: [ + { + type: 'FreezeDelegate', + frozen: false, + }, + ], + }); + + await update(umi, { + asset, + name: '', + uri: '', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + name: '', + uri: '', + freezeDelegate: { + authority: { + type: 'Owner', + }, + frozen: false, + }, + }); +}); + +test('it can update an asset update authority', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi); + const newUpdateAuthority = generateSigner(umi); + + await update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Address', [ + newUpdateAuthority.publicKey, + ]), + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: newUpdateAuthority.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); +}); + +test('it cannot update an asset using wrong authority', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const updateAuth = generateSigner(umi); + const asset = await createAsset(umi, { + updateAuthority: updateAuth, + }); + + const newUpdateAuthority = generateSigner(umi); + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Address', [ + newUpdateAuthority.publicKey, + ]), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: updateAuth.publicKey }, + }); +}); + +test('it cannot use an invalid system program for assets', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi); + const fakeSystemProgram = generateSigner(umi); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + systemProgram: fakeSystemProgram.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidSystemProgram' }); +}); + +test('it cannot use an invalid noop program for assets', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const asset = await createAsset(umi); + const fakeLogWrapper = generateSigner(umi); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + logWrapper: fakeLogWrapper.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidLogWrapperProgram' }); +}); + +test('it cannot use an invalid system program for collections', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const collection = await createCollection(umi); + const fakeSystemProgram = generateSigner(umi); + + const result = updateCollection(umi, { + collection: collection.publicKey, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + systemProgram: fakeSystemProgram.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidSystemProgram' }); +}); + +test('it cannot use an invalid noop program for collections', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const collection = await createCollection(umi); + const fakeLogWrapper = generateSigner(umi); + + const result = updateCollection(umi, { + collection: collection.publicKey, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + logWrapper: fakeLogWrapper.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidLogWrapperProgram' }); +}); + +test('it can remove an asset from a collection using update', async (t) => { + const umi = await createUmi(); + const { asset, collection } = await createAssetWithCollection(umi, {}, {}); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 1, + numMinted: 1, + }); + + await update(umi, { + asset, + collection, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 1, + }); +}); + +test('it cannot remove an asset from a collection if not collection update auth', async (t) => { + const umi = await createUmi(); + const collectionAuthority = generateSigner(umi); + const { asset, collection } = await createAssetWithCollection( + umi, + { authority: collectionAuthority }, + { updateAuthority: collectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + const result = update(umi, { + asset, + collection, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); +}); + +test('it cannot remove an asset from a collection when missing collection account', async (t) => { + const umi = await createUmi(); + const { asset, collection } = await createAssetWithCollection(umi, {}, {}); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 1, + numMinted: 1, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'MissingCollection' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 1, + numMinted: 1, + }); +}); + +test('it cannot remove an asset from a collection when using incorrect collection account', async (t) => { + const umi = await createUmi(); + const { asset, collection } = await createAssetWithCollection(umi, {}, {}); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + const randomCollection = await createCollection(umi); + + const result = update(umi, { + asset, + collection: randomCollection, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidCollection' }); +}); + +test('it can add asset to collection using update', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const collection = await createCollection(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + }); + + await update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 1, + numMinted: 0, + }); +}); + +test('it cannot add asset to collection when missing collection account', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const collection = await createCollection(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'MissingCollection' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + }); +}); + +test('it cannot add asset to collection when using incorrect collection account', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const collection = await createCollection(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: generateSigner(umi).publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidCollection' }); +}); + +test('it cannot add asset to collection using only new collection authority', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const newCollectionAuthority = generateSigner(umi); + const collection = await createCollection(umi, { + updateAuthority: newCollectionAuthority, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + authority: newCollectionAuthority, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); +}); + +test('it cannot add asset to collection if not both asset and collection auth', async (t) => { + const umi = await createUmi(); + + const assetAuthority = generateSigner(umi); + const asset = await createAsset(umi, { updateAuthority: assetAuthority }); + + const collectionAuthority = generateSigner(umi); + const collection = await createCollection(umi, { + updateAuthority: collectionAuthority, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: assetAuthority.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + // Attempt to update using asset authority. + const result = update(umi, { + asset, + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + authority: assetAuthority, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: assetAuthority.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + // Attempt to update using collection authority. + const result2 = update(umi, { + asset, + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await t.throwsAsync(result2, { name: 'NoApprovals' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: assetAuthority.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); +}); + +test('it can change an asset collection using same update authority', async (t) => { + const umi = await createUmi(); + const collectionAuthority = generateSigner(umi); + const { asset, collection: originalCollection } = + await createAssetWithCollection( + umi, + { authority: collectionAuthority }, + { updateAuthority: collectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { + type: 'Collection', + address: originalCollection.publicKey, + }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: originalCollection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + const newCollection = await createCollection(umi, { + updateAuthority: collectionAuthority, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + await update(umi, { + asset, + collection: originalCollection, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [ + newCollection.publicKey, + ]), + newCollection: newCollection.publicKey, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: newCollection.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: originalCollection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 1, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 0, + }); +}); + +test('it cannot change an asset collection if not both asset and collection auth', async (t) => { + const umi = await createUmi(); + const originalCollectionAuthority = generateSigner(umi); + const { asset, collection: originalCollection } = + await createAssetWithCollection( + umi, + { authority: originalCollectionAuthority }, + { updateAuthority: originalCollectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { + type: 'Collection', + address: originalCollection.publicKey, + }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: originalCollection.publicKey, + updateAuthority: originalCollectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + const newCollectionAuthority = generateSigner(umi); + const newCollection = await createCollection(umi, { + updateAuthority: newCollectionAuthority, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateAuthority: newCollectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + // Attempt to update using original collection authority. + const result = update(umi, { + asset, + collection: originalCollection, + newUpdateAuthority: updateAuthority('Collection', [ + newCollection.publicKey, + ]), + newCollection: newCollection.publicKey, + authority: originalCollectionAuthority, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { + type: 'Collection', + address: originalCollection.publicKey, + }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: originalCollection.publicKey, + updateAuthority: originalCollectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateAuthority: newCollectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + // Attempt to update using new collection authority. + const result2 = update(umi, { + asset, + collection: originalCollection, + newUpdateAuthority: updateAuthority('Collection', [ + newCollection.publicKey, + ]), + newCollection: newCollection.publicKey, + authority: newCollectionAuthority, + }).sendAndConfirm(umi); + + await t.throwsAsync(result2, { name: 'NoApprovals' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { + type: 'Collection', + address: originalCollection.publicKey, + }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: originalCollection.publicKey, + updateAuthority: originalCollectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateAuthority: newCollectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); +}); + +test('it can change an asset collection using delegate', async (t) => { + const umi = await createUmi(); + const originalCollectionAuthority = generateSigner(umi); + const { asset, collection: originalCollection } = + await createAssetWithCollection( + umi, + { authority: originalCollectionAuthority }, + { updateAuthority: originalCollectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { + type: 'Collection', + address: originalCollection.publicKey, + }, + }); + + const newCollectionAuthority = generateSigner(umi); + const newCollection = await createCollection(umi, { + updateAuthority: newCollectionAuthority, + }); + + await addCollectionPlugin(umi, { + collection: newCollection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [], + }, + authority: newCollectionAuthority, + }).sendAndConfirm(umi); + + await approveCollectionPluginAuthority(umi, { + collection: newCollection.publicKey, + plugin: { + type: 'UpdateDelegate', + }, + newAuthority: { + type: 'Address', + address: originalCollectionAuthority.publicKey, + }, + authority: newCollectionAuthority, + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateDelegate: { + authority: { + type: 'Address', + address: originalCollectionAuthority.publicKey, + }, + additionalDelegates: [], + }, + updateAuthority: newCollectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + await update(umi, { + asset, + collection: originalCollection, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [ + newCollection.publicKey, + ]), + newCollection: newCollection.publicKey, + authority: originalCollectionAuthority, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: newCollection.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateDelegate: { + authority: { + type: 'Address', + address: originalCollectionAuthority.publicKey, + }, + additionalDelegates: [], + }, + updateAuthority: newCollectionAuthority.publicKey, + currentSize: 1, + numMinted: 0, + }); +}); + +test('it can change an asset collection using same update authority (delegate exists but not used)', async (t) => { + const umi = await createUmi(); + const collectionAuthority = generateSigner(umi); + const { asset, collection: originalCollection } = + await createAssetWithCollection( + umi, + { authority: collectionAuthority }, + { updateAuthority: collectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { + type: 'Collection', + address: originalCollection.publicKey, + }, + }); + + const newCollection = await createCollection(umi, { + updateAuthority: collectionAuthority, + }); + + await addCollectionPlugin(umi, { + collection: newCollection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [], + }, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + const updateDelegate = generateSigner(umi); + await approveCollectionPluginAuthority(umi, { + collection: newCollection.publicKey, + plugin: { + type: 'UpdateDelegate', + }, + newAuthority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + await update(umi, { + asset, + collection: originalCollection, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [ + newCollection.publicKey, + ]), + newCollection: newCollection.publicKey, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: newCollection.publicKey }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: newCollection.publicKey, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 0, + }); +}); + +test('it cannot remove an asset from collection using update delegate', async (t) => { + const umi = await createUmi(); + const collectionAuthority = generateSigner(umi); + const { asset, collection } = await createAssetWithCollection( + umi, + { authority: collectionAuthority }, + { updateAuthority: collectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + await addCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [], + }, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + const updateDelegate = generateSigner(umi); + await approveCollectionPluginAuthority(umi, { + collection: collection.publicKey, + plugin: { + type: 'UpdateDelegate', + }, + newAuthority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + const result = update(umi, { + asset, + collection, + newUpdateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + authority: updateDelegate, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); +}); + +test('it cannot remove an asset from collection using additional update delegate', async (t) => { + const umi = await createUmi(); + const collectionAuthority = generateSigner(umi); + const { asset, collection } = await createAssetWithCollection( + umi, + { authority: collectionAuthority }, + { updateAuthority: collectionAuthority } + ); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + }); + + const additionalDelegate = generateSigner(umi); + await addCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [additionalDelegate.publicKey], + }, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateDelegate: { + authority: { + type: 'UpdateAuthority', + }, + additionalDelegates: [additionalDelegate.publicKey], + }, + updateAuthority: collectionAuthority.publicKey, + currentSize: 1, + numMinted: 1, + }); + + const result = update(umi, { + asset, + collection, + newUpdateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + authority: additionalDelegate, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); +}); + +test('it cannot add asset to collection using additional update delegate on new collection', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + const collectionAuthority = generateSigner(umi); + const collection = await createCollection(umi, { + updateAuthority: collectionAuthority, + }); + + await addCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [umi.identity.publicKey], + }, + authority: collectionAuthority, + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateDelegate: { + authority: { + type: 'UpdateAuthority', + }, + additionalDelegates: [umi.identity.publicKey], + }, + updateAuthority: collectionAuthority.publicKey, + currentSize: 0, + numMinted: 0, + }); + + const result = update(umi, { + asset, + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + authority: umi.identity, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); +}); diff --git a/clients/rust/src/generated/instructions/mod.rs b/clients/rust/src/generated/instructions/mod.rs index 116673db..b42e06ac 100644 --- a/clients/rust/src/generated/instructions/mod.rs +++ b/clients/rust/src/generated/instructions/mod.rs @@ -33,6 +33,7 @@ pub(crate) mod r#update_collection_v1; pub(crate) mod r#update_external_plugin_adapter_v1; pub(crate) mod r#update_plugin_v1; pub(crate) mod r#update_v1; +pub(crate) mod r#update_v2; pub(crate) mod r#write_collection_external_plugin_adapter_data_v1; pub(crate) mod r#write_external_plugin_adapter_data_v1; @@ -64,5 +65,6 @@ pub use self::r#update_collection_v1::*; pub use self::r#update_external_plugin_adapter_v1::*; pub use self::r#update_plugin_v1::*; pub use self::r#update_v1::*; +pub use self::r#update_v2::*; pub use self::r#write_collection_external_plugin_adapter_data_v1::*; pub use self::r#write_external_plugin_adapter_data_v1::*; diff --git a/clients/rust/src/generated/instructions/update_v2.rs b/clients/rust/src/generated/instructions/update_v2.rs new file mode 100644 index 00000000..925e4a7b --- /dev/null +++ b/clients/rust/src/generated/instructions/update_v2.rs @@ -0,0 +1,672 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! [https://github.com/metaplex-foundation/kinobi] +//! + +use crate::generated::types::UpdateAuthority; +#[cfg(feature = "anchor")] +use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct UpdateV2 { + /// The address of the asset + pub asset: solana_program::pubkey::Pubkey, + /// The collection to which the asset belongs + pub collection: Option, + /// The account paying for the storage fees + pub payer: solana_program::pubkey::Pubkey, + /// The update authority or update authority delegate of the asset + pub authority: Option, + /// A new collection to which to move the asset + pub new_collection: Option, + /// The system program + pub system_program: solana_program::pubkey::Pubkey, + /// The SPL Noop Program + pub log_wrapper: Option, +} + +impl UpdateV2 { + pub fn instruction( + &self, + args: UpdateV2InstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: UpdateV2InstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.asset, false, + )); + if let Some(collection) = self.collection { + accounts.push(solana_program::instruction::AccountMeta::new( + collection, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + if let Some(authority) = self.authority { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + authority, true, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + if let Some(new_collection) = self.new_collection { + accounts.push(solana_program::instruction::AccountMeta::new( + new_collection, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + if let Some(log_wrapper) = self.log_wrapper { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + log_wrapper, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + accounts.extend_from_slice(remaining_accounts); + let mut data = UpdateV2InstructionData::new().try_to_vec().unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::MPL_CORE_ID, + accounts, + data, + } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +pub struct UpdateV2InstructionData { + discriminator: u8, +} + +impl UpdateV2InstructionData { + pub fn new() -> Self { + Self { discriminator: 30 } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpdateV2InstructionArgs { + pub new_name: Option, + pub new_uri: Option, + pub new_update_authority: Option, +} + +/// Instruction builder for `UpdateV2`. +/// +/// ### Accounts: +/// +/// 0. `[writable]` asset +/// 1. `[writable, optional]` collection +/// 2. `[writable, signer]` payer +/// 3. `[signer, optional]` authority +/// 4. `[writable, optional]` new_collection +/// 5. `[optional]` system_program (default to `11111111111111111111111111111111`) +/// 6. `[optional]` log_wrapper +#[derive(Default)] +pub struct UpdateV2Builder { + asset: Option, + collection: Option, + payer: Option, + authority: Option, + new_collection: Option, + system_program: Option, + log_wrapper: Option, + new_name: Option, + new_uri: Option, + new_update_authority: Option, + __remaining_accounts: Vec, +} + +impl UpdateV2Builder { + pub fn new() -> Self { + Self::default() + } + /// The address of the asset + #[inline(always)] + pub fn asset(&mut self, asset: solana_program::pubkey::Pubkey) -> &mut Self { + self.asset = Some(asset); + self + } + /// `[optional account]` + /// The collection to which the asset belongs + #[inline(always)] + pub fn collection(&mut self, collection: Option) -> &mut Self { + self.collection = collection; + self + } + /// The account paying for the storage fees + #[inline(always)] + pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + /// `[optional account]` + /// The update authority or update authority delegate of the asset + #[inline(always)] + pub fn authority(&mut self, authority: Option) -> &mut Self { + self.authority = authority; + self + } + /// `[optional account]` + /// A new collection to which to move the asset + #[inline(always)] + pub fn new_collection( + &mut self, + new_collection: Option, + ) -> &mut Self { + self.new_collection = new_collection; + self + } + /// `[optional account, default to '11111111111111111111111111111111']` + /// The system program + #[inline(always)] + pub fn system_program(&mut self, system_program: solana_program::pubkey::Pubkey) -> &mut Self { + self.system_program = Some(system_program); + self + } + /// `[optional account]` + /// The SPL Noop Program + #[inline(always)] + pub fn log_wrapper( + &mut self, + log_wrapper: Option, + ) -> &mut Self { + self.log_wrapper = log_wrapper; + self + } + /// `[optional argument]` + #[inline(always)] + pub fn new_name(&mut self, new_name: String) -> &mut Self { + self.new_name = Some(new_name); + self + } + /// `[optional argument]` + #[inline(always)] + pub fn new_uri(&mut self, new_uri: String) -> &mut Self { + self.new_uri = Some(new_uri); + self + } + /// `[optional argument]` + #[inline(always)] + pub fn new_update_authority(&mut self, new_update_authority: UpdateAuthority) -> &mut Self { + self.new_update_authority = Some(new_update_authority); + self + } + /// Add an aditional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = UpdateV2 { + asset: self.asset.expect("asset is not set"), + collection: self.collection, + payer: self.payer.expect("payer is not set"), + authority: self.authority, + new_collection: self.new_collection, + system_program: self + .system_program + .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), + log_wrapper: self.log_wrapper, + }; + let args = UpdateV2InstructionArgs { + new_name: self.new_name.clone(), + new_uri: self.new_uri.clone(), + new_update_authority: self.new_update_authority.clone(), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `update_v2` CPI accounts. +pub struct UpdateV2CpiAccounts<'a, 'b> { + /// The address of the asset + pub asset: &'b solana_program::account_info::AccountInfo<'a>, + /// The collection to which the asset belongs + pub collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The account paying for the storage fees + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + /// The update authority or update authority delegate of the asset + pub authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// A new collection to which to move the asset + pub new_collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The SPL Noop Program + pub log_wrapper: Option<&'b solana_program::account_info::AccountInfo<'a>>, +} + +/// `update_v2` CPI instruction. +pub struct UpdateV2Cpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + /// The address of the asset + pub asset: &'b solana_program::account_info::AccountInfo<'a>, + /// The collection to which the asset belongs + pub collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The account paying for the storage fees + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + /// The update authority or update authority delegate of the asset + pub authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// A new collection to which to move the asset + pub new_collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The SPL Noop Program + pub log_wrapper: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The arguments for the instruction. + pub __args: UpdateV2InstructionArgs, +} + +impl<'a, 'b> UpdateV2Cpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: UpdateV2CpiAccounts<'a, 'b>, + args: UpdateV2InstructionArgs, + ) -> Self { + Self { + __program: program, + asset: accounts.asset, + collection: accounts.collection, + payer: accounts.payer, + authority: accounts.authority, + new_collection: accounts.new_collection, + system_program: accounts.system_program, + log_wrapper: accounts.log_wrapper, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.asset.key, + false, + )); + if let Some(collection) = self.collection { + accounts.push(solana_program::instruction::AccountMeta::new( + *collection.key, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new( + *self.payer.key, + true, + )); + if let Some(authority) = self.authority { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *authority.key, + true, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + if let Some(new_collection) = self.new_collection { + accounts.push(solana_program::instruction::AccountMeta::new( + *new_collection.key, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.system_program.key, + false, + )); + if let Some(log_wrapper) = self.log_wrapper { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *log_wrapper.key, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = UpdateV2InstructionData::new().try_to_vec().unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::MPL_CORE_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(7 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.asset.clone()); + if let Some(collection) = self.collection { + account_infos.push(collection.clone()); + } + account_infos.push(self.payer.clone()); + if let Some(authority) = self.authority { + account_infos.push(authority.clone()); + } + if let Some(new_collection) = self.new_collection { + account_infos.push(new_collection.clone()); + } + account_infos.push(self.system_program.clone()); + if let Some(log_wrapper) = self.log_wrapper { + account_infos.push(log_wrapper.clone()); + } + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `UpdateV2` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable]` asset +/// 1. `[writable, optional]` collection +/// 2. `[writable, signer]` payer +/// 3. `[signer, optional]` authority +/// 4. `[writable, optional]` new_collection +/// 5. `[]` system_program +/// 6. `[optional]` log_wrapper +pub struct UpdateV2CpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> UpdateV2CpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(UpdateV2CpiBuilderInstruction { + __program: program, + asset: None, + collection: None, + payer: None, + authority: None, + new_collection: None, + system_program: None, + log_wrapper: None, + new_name: None, + new_uri: None, + new_update_authority: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + /// The address of the asset + #[inline(always)] + pub fn asset(&mut self, asset: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.asset = Some(asset); + self + } + /// `[optional account]` + /// The collection to which the asset belongs + #[inline(always)] + pub fn collection( + &mut self, + collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.collection = collection; + self + } + /// The account paying for the storage fees + #[inline(always)] + pub fn payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + /// `[optional account]` + /// The update authority or update authority delegate of the asset + #[inline(always)] + pub fn authority( + &mut self, + authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.authority = authority; + self + } + /// `[optional account]` + /// A new collection to which to move the asset + #[inline(always)] + pub fn new_collection( + &mut self, + new_collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.new_collection = new_collection; + self + } + /// The system program + #[inline(always)] + pub fn system_program( + &mut self, + system_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.system_program = Some(system_program); + self + } + /// `[optional account]` + /// The SPL Noop Program + #[inline(always)] + pub fn log_wrapper( + &mut self, + log_wrapper: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.log_wrapper = log_wrapper; + self + } + /// `[optional argument]` + #[inline(always)] + pub fn new_name(&mut self, new_name: String) -> &mut Self { + self.instruction.new_name = Some(new_name); + self + } + /// `[optional argument]` + #[inline(always)] + pub fn new_uri(&mut self, new_uri: String) -> &mut Self { + self.instruction.new_uri = Some(new_uri); + self + } + /// `[optional argument]` + #[inline(always)] + pub fn new_update_authority(&mut self, new_update_authority: UpdateAuthority) -> &mut Self { + self.instruction.new_update_authority = Some(new_update_authority); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = UpdateV2InstructionArgs { + new_name: self.instruction.new_name.clone(), + new_uri: self.instruction.new_uri.clone(), + new_update_authority: self.instruction.new_update_authority.clone(), + }; + let instruction = UpdateV2Cpi { + __program: self.instruction.__program, + + asset: self.instruction.asset.expect("asset is not set"), + + collection: self.instruction.collection, + + payer: self.instruction.payer.expect("payer is not set"), + + authority: self.instruction.authority, + + new_collection: self.instruction.new_collection, + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + + log_wrapper: self.instruction.log_wrapper, + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +struct UpdateV2CpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + asset: Option<&'b solana_program::account_info::AccountInfo<'a>>, + collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + new_collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + log_wrapper: Option<&'b solana_program::account_info::AccountInfo<'a>>, + new_name: Option, + new_uri: Option, + new_update_authority: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index 63684cf5..e5bd5f64 100755 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -130,6 +130,19 @@ kinobi.update( }, } }, + updateV2: { + arguments: { + newUpdateAuthority: { + defaultValue: k.noneValueNode() + }, + newName: { + defaultValue: k.noneValueNode() + }, + newUri: { + defaultValue: k.noneValueNode() + }, + } + }, updateCollectionV1: { arguments: { newName: { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index b4c34756..26f49b23 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -1929,6 +1929,83 @@ "type": "u8", "value": 29 } + }, + { + "name": "UpdateV2", + "accounts": [ + { + "name": "asset", + "isMut": true, + "isSigner": false, + "docs": [ + "The address of the asset" + ] + }, + { + "name": "collection", + "isMut": true, + "isSigner": false, + "isOptional": true, + "docs": [ + "The collection to which the asset belongs" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "The account paying for the storage fees" + ] + }, + { + "name": "authority", + "isMut": false, + "isSigner": true, + "isOptional": true, + "docs": [ + "The update authority or update authority delegate of the asset" + ] + }, + { + "name": "newCollection", + "isMut": true, + "isSigner": false, + "isOptional": true, + "docs": [ + "A new collection to which to move the asset" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The system program" + ] + }, + { + "name": "logWrapper", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "The SPL Noop Program" + ] + } + ], + "args": [ + { + "name": "updateV2Args", + "type": { + "defined": "UpdateV2Args" + } + } + ], + "discriminant": { + "type": "u8", + "value": 30 + } } ], "accounts": [ @@ -3450,6 +3527,34 @@ ] } }, + { + "name": "UpdateV2Args", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newName", + "type": { + "option": "string" + } + }, + { + "name": "newUri", + "type": { + "option": "string" + } + }, + { + "name": "newUpdateAuthority", + "type": { + "option": { + "defined": "UpdateAuthority" + } + } + } + ] + } + }, { "name": "UpdateCollectionV1Args", "type": { diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index 721b86d8..ff657069 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -11,7 +11,7 @@ use crate::processor::{ RemoveExternalPluginAdapterV1Args, RemovePluginV1Args, RevokeCollectionPluginAuthorityV1Args, RevokePluginAuthorityV1Args, TransferV1Args, UpdateCollectionExternalPluginAdapterV1Args, UpdateCollectionPluginV1Args, UpdateCollectionV1Args, UpdateExternalPluginAdapterV1Args, - UpdatePluginV1Args, UpdateV1Args, WriteCollectionExternalPluginAdapterDataV1Args, + UpdatePluginV1Args, UpdateV1Args, UpdateV2Args, WriteCollectionExternalPluginAdapterDataV1Args, WriteExternalPluginAdapterDataV1Args, }; @@ -282,4 +282,14 @@ pub(crate) enum MplAssetInstruction { #[account(4, name="system_program", desc = "The system program")] #[account(5, optional, name="log_wrapper", desc = "The SPL Noop Program")] WriteCollectionExternalPluginAdapterDataV1(WriteCollectionExternalPluginAdapterDataV1Args), + + /// Update an mpl-core V2. + #[account(0, writable, name="asset", desc = "The address of the asset")] + #[account(1, optional, writable, name="collection", desc = "The collection to which the asset belongs")] + #[account(2, writable, signer, name="payer", desc = "The account paying for the storage fees")] + #[account(3, optional, signer, name="authority", desc = "The update authority or update authority delegate of the asset")] + #[account(4, optional, writable, name="new_collection", desc = "A new collection to which to move the asset")] + #[account(5, name="system_program", desc = "The system program")] + #[account(6, optional, name="log_wrapper", desc = "The SPL Noop Program")] + UpdateV2(UpdateV2Args), } diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index 5708bf02..486de812 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -104,7 +104,7 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnV1Args) -> Pro process_burn(ctx.accounts.asset, ctx.accounts.payer)?; if let Some(mut collection) = collection { - collection.decrement()?; + collection.decrement_size()?; collection.save(ctx.accounts.collection.unwrap(), 0)?; }; Ok(()) diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 6abf89e5..b548ce46 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -268,7 +268,8 @@ pub(crate) fn process_create<'a>( } if let Some(mut collection) = collection { - collection.increment()?; + collection.increment_minted()?; + collection.increment_size()?; collection.save(ctx.accounts.collection.unwrap(), 0)?; }; diff --git a/programs/mpl-core/src/processor/mod.rs b/programs/mpl-core/src/processor/mod.rs index 476d2629..e3177a48 100644 --- a/programs/mpl-core/src/processor/mod.rs +++ b/programs/mpl-core/src/processor/mod.rs @@ -109,7 +109,7 @@ pub fn process_instruction<'a>( } MplAssetInstruction::UpdateV1(args) => { msg!("Instruction: Update"); - update(accounts, args) + update_v1(accounts, args) } MplAssetInstruction::UpdateCollectionV1(args) => { msg!("Instruction: UpdateCollection"); @@ -164,5 +164,9 @@ pub fn process_instruction<'a>( msg!("Instruction: WriteCollectionExternalPluginAdapterDataV1"); write_collection_external_plugin_adapter_data(accounts, args) } + MplAssetInstruction::UpdateV2(args) => { + msg!("Instruction: UpdateV2"); + update_v2(accounts, args) + } } } diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index 2ab1459f..e51f3d38 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -6,15 +6,17 @@ use solana_program::{ use crate::{ error::MplCoreError, - instruction::accounts::{UpdateCollectionV1Accounts, UpdateV1Accounts}, + instruction::accounts::{ + Context, UpdateCollectionV1Accounts, UpdateV1Accounts, UpdateV2Accounts, + }, plugins::{ - ExternalPluginAdapter, HookableLifecycleEvent, Plugin, PluginHeaderV1, PluginRegistryV1, - PluginType, + fetch_plugin, ExternalPluginAdapter, HookableLifecycleEvent, Plugin, PluginHeaderV1, + PluginRegistryV1, PluginType, UpdateDelegate, }, state::{AssetV1, CollectionV1, DataBlob, Key, SolanaAccount, UpdateAuthority}, utils::{ - load_key, resize_or_reallocate_account, resolve_authority, validate_asset_permissions, - validate_collection_permissions, + assert_collection_authority, load_key, resize_or_reallocate_account, resolve_authority, + validate_asset_permissions, validate_collection_permissions, }, }; @@ -26,10 +28,65 @@ pub(crate) struct UpdateV1Args { pub new_update_authority: Option, } -pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateV1Args) -> ProgramResult { - // Accounts. +#[repr(C)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +pub(crate) struct UpdateV2Args { + pub new_name: Option, + pub new_uri: Option, + pub new_update_authority: Option, +} + +impl From for UpdateV2Args { + fn from(item: UpdateV1Args) -> Self { + UpdateV2Args { + new_name: item.new_name, + new_uri: item.new_uri, + new_update_authority: item.new_update_authority, + } + } +} + +enum InstructionVersion { + V1, + V2, +} + +pub(crate) fn update_v1<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateV1Args) -> ProgramResult { let ctx = UpdateV1Accounts::context(accounts)?; + let v2_accounts = UpdateV2Accounts { + asset: ctx.accounts.asset, + collection: ctx.accounts.collection, + payer: ctx.accounts.payer, + authority: ctx.accounts.authority, + new_collection: None, + system_program: ctx.accounts.system_program, + log_wrapper: ctx.accounts.log_wrapper, + }; + + let v2_ctx = Context { + accounts: v2_accounts, + remaining_accounts: ctx.remaining_accounts, + }; + + update( + v2_ctx, + accounts, + UpdateV2Args::from(args), + InstructionVersion::V1, + ) +} +pub(crate) fn update_v2<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateV2Args) -> ProgramResult { + let ctx = UpdateV2Accounts::context(accounts)?; + update(ctx, accounts, args, InstructionVersion::V2) +} + +fn update<'a>( + ctx: Context>, + accounts: &'a [AccountInfo<'a>], + args: UpdateV2Args, + ix_version: InstructionVersion, +) -> ProgramResult { // Guards. assert_signer(ctx.accounts.payer)?; let authority = resolve_authority(ctx.accounts.payer, ctx.accounts.authority)?; @@ -75,15 +132,86 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateV1Args) -> let mut dirty = false; if let Some(new_update_authority) = args.new_update_authority { - if let UpdateAuthority::Collection(_collection_address) = new_update_authority { - // Updating collections is not currently available. - return Err(MplCoreError::NotAvailable.into()); - }; - - if let UpdateAuthority::Collection(_collection_address) = asset.update_authority { - // Removing from collection is not currently available. - // will require the collection size to be updated - return Err(MplCoreError::NotAvailable.into()); + // If asset is currently in a collection, remove it from the collection. + // This block is executed when we want to go from collection to no collection, or collection to + // new collection. The permission for this block is established by `validate_asset_permissions`. + if let UpdateAuthority::Collection(_existing_collection_address) = asset.update_authority { + match ix_version { + InstructionVersion::V1 => { + // Removing from or changing collection requires collection size to be updated + // and is not supported by `UpdateV1`. + msg!("Error: Use UpdateV2 to remove asset from or change collection"); + return Err(MplCoreError::NotAvailable.into()); + } + InstructionVersion::V2 => { + let existing_collection_account = ctx + .accounts + .collection + .ok_or(MplCoreError::MissingCollection)?; + + let mut existing_collection = + CollectionV1::load(existing_collection_account, 0)?; + + existing_collection.decrement_size()?; + existing_collection.save(existing_collection_account, 0)?; + } + } + } + + // If new update authority is a collection, add the asset to the new collection. + // This block is executed when we want to go from no collection to collection, or collection + // to new collection. The permission for this block is established in the code block. It is + // similar to creating an asset, in that the new collection's authority or an update delegate + // for the new collecton can approve adding the asset to the new collection. + if let UpdateAuthority::Collection(new_collection_address) = new_update_authority { + match ix_version { + InstructionVersion::V1 => { + msg!("Error: Use UpdateV2 to add asset to a new collection"); + return Err(MplCoreError::NotAvailable.into()); + } + InstructionVersion::V2 => { + // Make sure the account was provided. + let new_collection_account = ctx + .accounts + .new_collection + .ok_or(MplCoreError::MissingCollection)?; + + // Make sure account matches what was provided in the args. + if new_collection_account.key != &new_collection_address { + return Err(MplCoreError::InvalidCollection.into()); + } + + // Deserialize the collection. + let mut new_collection = CollectionV1::load(new_collection_account, 0)?; + + // See if there is an update delegate on the new collection. + let maybe_update_delegate = fetch_plugin::( + new_collection_account, + PluginType::UpdateDelegate, + ); + + // Make sure the authority has authority to add the asset to the new collection. + if let Ok((plugin_authority, _, _)) = maybe_update_delegate { + if assert_collection_authority( + &new_collection, + authority, + &plugin_authority, + ) + .is_err() + && authority.key != &new_collection.update_authority + { + solana_program::msg!("UA: Rejected"); + return Err(MplCoreError::InvalidAuthority.into()); + } + } else if authority.key != &new_collection.update_authority { + solana_program::msg!("UA: Rejected"); + return Err(MplCoreError::InvalidAuthority.into()); + } + + new_collection.increment_size()?; + new_collection.save(new_collection_account, 0)?; + } + }; } asset.update_authority = new_update_authority; diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index 1b4767fd..969293e6 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -304,13 +304,18 @@ impl CollectionV1 { } } - /// Increment size of the Collection - pub fn increment(&mut self) -> Result<(), ProgramError> { + /// Increment number of minted items of the Collection + pub fn increment_minted(&mut self) -> Result<(), ProgramError> { self.num_minted = self .num_minted .checked_add(1) .ok_or(MplCoreError::NumericalOverflowError)?; + Ok(()) + } + + /// Increment current size of the Collection + pub fn increment_size(&mut self) -> Result<(), ProgramError> { self.current_size = self .current_size .checked_add(1) @@ -319,8 +324,8 @@ impl CollectionV1 { Ok(()) } - /// Decrements size of the Collection - pub fn decrement(&mut self) -> Result<(), ProgramError> { + /// Decrement current size of the Collection + pub fn decrement_size(&mut self) -> Result<(), ProgramError> { self.current_size = self .current_size .checked_sub(1) diff --git a/programs/mpl-core/src/state/update_authority.rs b/programs/mpl-core/src/state/update_authority.rs index c23493fc..7e119c70 100644 --- a/programs/mpl-core/src/state/update_authority.rs +++ b/programs/mpl-core/src/state/update_authority.rs @@ -6,14 +6,13 @@ use crate::{ error::MplCoreError, instruction::accounts::{ BurnV1Accounts, CompressV1Accounts, CreateV2Accounts, DecompressV1Accounts, - TransferV1Accounts, UpdateV1Accounts, + TransferV1Accounts, }, plugins::{ - abstain, approve, fetch_plugin, reject, CheckResult, PluginType, UpdateDelegate, - ValidationResult, + abstain, fetch_plugin, reject, CheckResult, PluginType, UpdateDelegate, ValidationResult, }, processor::CreateV2Args, - state::{Authority, CollectionV1, SolanaAccount}, + state::{CollectionV1, SolanaAccount}, utils::assert_collection_authority, }; @@ -77,12 +76,7 @@ impl UpdateAuthority { if let Ok((authority, update_delegate_plugin, _)) = maybe_update_delegate { if assert_collection_authority(&collection, authority_info, &authority).is_err() - && assert_collection_authority( - &collection, - authority_info, - &Authority::UpdateAuthority, - ) - .is_err() + && authority_info.key != &collection.update_authority && !update_delegate_plugin .additional_delegates .contains(authority_info.key) @@ -104,24 +98,6 @@ impl UpdateAuthority { } } - /// Validate the update lifecycle event. - pub fn validate_update( - &self, - ctx: &UpdateV1Accounts, - ) -> Result { - let authority = match self { - Self::None => return reject!(), - Self::Address(address) => address, - Self::Collection(address) => address, - }; - - if ctx.authority.unwrap_or(ctx.payer).key == authority { - approve!() - } else { - abstain!() - } - } - /// Validate the burn lifecycle event. pub fn validate_burn(&self, _ctx: &BurnV1Accounts) -> Result { abstain!() diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs index 49b4755d..340b6dea 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -63,14 +63,14 @@ pub fn assert_authority( /// Assert that the account info address is the same as the authority. pub fn assert_collection_authority( - asset: &CollectionV1, + collection: &CollectionV1, authority_info: &AccountInfo, authority: &Authority, ) -> ProgramResult { match authority { Authority::None | Authority::Owner => (), Authority::UpdateAuthority => { - if &asset.update_authority == authority_info.key { + if &collection.update_authority == authority_info.key { return Ok(()); } }