From 5dd5cba57b33a14a0b054d51a8314c4abecbd95d Mon Sep 17 00:00:00 2001 From: blockiosaurus Date: Mon, 26 Feb 2024 23:01:42 -0500 Subject: [PATCH] Adding basic collections. --- .../src/generated/accounts/collectionData.ts | 159 +++++ clients/js/src/generated/accounts/index.ts | 1 + .../generated/instructions/addAuthority.ts | 2 +- .../src/generated/instructions/addPlugin.ts | 2 +- clients/js/src/generated/instructions/burn.ts | 2 +- .../js/src/generated/instructions/compress.ts | 2 +- .../instructions/createCollection.ts | 159 +++++ .../src/generated/instructions/decompress.ts | 2 +- .../js/src/generated/instructions/index.ts | 1 + .../generated/instructions/removeAuthority.ts | 2 +- .../generated/instructions/removePlugin.ts | 2 +- .../js/src/generated/instructions/transfer.ts | 2 +- .../js/src/generated/instructions/update.ts | 2 +- .../generated/instructions/updatePlugin.ts | 2 +- clients/js/src/generated/types/key.ts | 1 + .../js/src/hooked/fetchAssetWithPlugins.ts | 14 +- .../src/hooked/fetchCollectionWithPlugins.ts | 65 +++ clients/js/src/hooked/index.ts | 14 + clients/js/test/createCollection.test.ts | 148 +++++ .../src/generated/accounts/collection_data.rs | 45 ++ clients/rust/src/generated/accounts/mod.rs | 2 + .../generated/instructions/add_authority.rs | 2 +- .../src/generated/instructions/add_plugin.rs | 2 +- .../rust/src/generated/instructions/burn.rs | 2 +- .../src/generated/instructions/compress.rs | 2 +- .../instructions/create_collection.rs | 544 ++++++++++++++++++ .../src/generated/instructions/decompress.rs | 2 +- .../rust/src/generated/instructions/mod.rs | 2 + .../instructions/remove_authority.rs | 2 +- .../generated/instructions/remove_plugin.rs | 2 +- .../src/generated/instructions/transfer.rs | 2 +- .../rust/src/generated/instructions/update.rs | 2 +- .../generated/instructions/update_plugin.rs | 2 +- clients/rust/src/generated/types/key.rs | 1 + idls/mpl_core_program.json | 140 ++++- programs/mpl-core/src/instruction.rs | 18 +- programs/mpl-core/src/plugins/collection.rs | 40 +- programs/mpl-core/src/plugins/lifecycle.rs | 1 + programs/mpl-core/src/plugins/utils.rs | 56 +- programs/mpl-core/src/processor/create.rs | 48 +- .../src/processor/create_collection.rs | 102 ++++ programs/mpl-core/src/processor/mod.rs | 7 + programs/mpl-core/src/state/collection.rs | 61 +- programs/mpl-core/src/state/mod.rs | 5 + 44 files changed, 1597 insertions(+), 77 deletions(-) create mode 100644 clients/js/src/generated/accounts/collectionData.ts create mode 100644 clients/js/src/generated/instructions/createCollection.ts create mode 100644 clients/js/src/hooked/fetchCollectionWithPlugins.ts create mode 100644 clients/js/test/createCollection.test.ts create mode 100644 clients/rust/src/generated/accounts/collection_data.rs create mode 100644 clients/rust/src/generated/instructions/create_collection.rs create mode 100644 programs/mpl-core/src/processor/create_collection.rs diff --git a/clients/js/src/generated/accounts/collectionData.ts b/clients/js/src/generated/accounts/collectionData.ts new file mode 100644 index 00000000..1c5815ac --- /dev/null +++ b/clients/js/src/generated/accounts/collectionData.ts @@ -0,0 +1,159 @@ +/** + * 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 { + Account, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountOptions, + RpcGetAccountsOptions, + assertAccountExists, + deserializeAccount, + gpaBuilder, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; +import { + Serializer, + publicKey as publicKeySerializer, + string, + struct, + u32, +} from '@metaplex-foundation/umi/serializers'; +import { Key, KeyArgs, getKeySerializer } from '../types'; + +export type CollectionData = Account; + +export type CollectionDataAccountData = { + key: Key; + updateAuthority: PublicKey; + name: string; + uri: string; + numMinted: number; + currentSize: number; +}; + +export type CollectionDataAccountDataArgs = { + key: KeyArgs; + updateAuthority: PublicKey; + name: string; + uri: string; + numMinted: number; + currentSize: number; +}; + +export function getCollectionDataAccountDataSerializer(): Serializer< + CollectionDataAccountDataArgs, + CollectionDataAccountData +> { + return struct( + [ + ['key', getKeySerializer()], + ['updateAuthority', publicKeySerializer()], + ['name', string()], + ['uri', string()], + ['numMinted', u32()], + ['currentSize', u32()], + ], + { description: 'CollectionDataAccountData' } + ) as Serializer; +} + +export function deserializeCollectionData( + rawAccount: RpcAccount +): CollectionData { + return deserializeAccount( + rawAccount, + getCollectionDataAccountDataSerializer() + ); +} + +export async function fetchCollectionData( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount( + toPublicKey(publicKey, false), + options + ); + assertAccountExists(maybeAccount, 'CollectionData'); + return deserializeCollectionData(maybeAccount); +} + +export async function safeFetchCollectionData( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount( + toPublicKey(publicKey, false), + options + ); + return maybeAccount.exists ? deserializeCollectionData(maybeAccount) : null; +} + +export async function fetchAllCollectionData( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount, 'CollectionData'); + return deserializeCollectionData(maybeAccount); + }); +} + +export async function safeFetchAllCollectionData( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts + .filter((maybeAccount) => maybeAccount.exists) + .map((maybeAccount) => + deserializeCollectionData(maybeAccount as RpcAccount) + ); +} + +export function getCollectionDataGpaBuilder( + context: Pick +) { + const programId = context.programs.getPublicKey( + 'mplCoreProgram', + 'CoREzp6dAdLVRKf3EM5tWrsXM2jQwRFeu5uhzsAyjYXL' + ); + return gpaBuilder(context, programId) + .registerFields<{ + key: KeyArgs; + updateAuthority: PublicKey; + name: string; + uri: string; + numMinted: number; + currentSize: number; + }>({ + key: [0, getKeySerializer()], + updateAuthority: [1, publicKeySerializer()], + name: [33, string()], + uri: [null, string()], + numMinted: [null, u32()], + currentSize: [null, u32()], + }) + .deserializeUsing((account) => + deserializeCollectionData(account) + ); +} diff --git a/clients/js/src/generated/accounts/index.ts b/clients/js/src/generated/accounts/index.ts index 2419a1f2..8e62c9ad 100644 --- a/clients/js/src/generated/accounts/index.ts +++ b/clients/js/src/generated/accounts/index.ts @@ -7,6 +7,7 @@ */ export * from './asset'; +export * from './collectionData'; export * from './hashedAsset'; export * from './pluginHeader'; export * from './pluginRegistry'; diff --git a/clients/js/src/generated/instructions/addAuthority.ts b/clients/js/src/generated/instructions/addAuthority.ts index 1aa29cae..bc191a4c 100644 --- a/clients/js/src/generated/instructions/addAuthority.ts +++ b/clients/js/src/generated/instructions/addAuthority.ts @@ -79,7 +79,7 @@ export function getAddAuthorityInstructionDataSerializer(): Serializer< ], { description: 'AddAuthorityInstructionData' } ), - (value) => ({ ...value, discriminator: 4 }) + (value) => ({ ...value, discriminator: 5 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/addPlugin.ts b/clients/js/src/generated/instructions/addPlugin.ts index b4a93622..7b78a10b 100644 --- a/clients/js/src/generated/instructions/addPlugin.ts +++ b/clients/js/src/generated/instructions/addPlugin.ts @@ -67,7 +67,7 @@ export function getAddPluginInstructionDataSerializer(): Serializer< ], { description: 'AddPluginInstructionData' } ), - (value) => ({ ...value, discriminator: 1 }) + (value) => ({ ...value, discriminator: 2 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/burn.ts b/clients/js/src/generated/instructions/burn.ts index cb0d9ed2..0bc71039 100644 --- a/clients/js/src/generated/instructions/burn.ts +++ b/clients/js/src/generated/instructions/burn.ts @@ -70,7 +70,7 @@ export function getBurnInstructionDataSerializer(): Serializer< ], { description: 'BurnInstructionData' } ), - (value) => ({ ...value, discriminator: 6 }) + (value) => ({ ...value, discriminator: 7 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/compress.ts b/clients/js/src/generated/instructions/compress.ts index c34df4b6..5185a42a 100644 --- a/clients/js/src/generated/instructions/compress.ts +++ b/clients/js/src/generated/instructions/compress.ts @@ -57,7 +57,7 @@ export function getCompressInstructionDataSerializer(): Serializer< struct([['discriminator', u8()]], { description: 'CompressInstructionData', }), - (value) => ({ ...value, discriminator: 9 }) + (value) => ({ ...value, discriminator: 10 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/createCollection.ts b/clients/js/src/generated/instructions/createCollection.ts new file mode 100644 index 00000000..693bd49b --- /dev/null +++ b/clients/js/src/generated/instructions/createCollection.ts @@ -0,0 +1,159 @@ +/** + * 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, + Pda, + PublicKey, + Signer, + TransactionBuilder, + transactionBuilder, +} from '@metaplex-foundation/umi'; +import { + Serializer, + array, + mapSerializer, + string, + struct, + u8, +} from '@metaplex-foundation/umi/serializers'; +import { + ResolvedAccount, + ResolvedAccountsWithIndices, + getAccountMetasAndSigners, +} from '../shared'; +import { Plugin, PluginArgs, getPluginSerializer } from '../types'; + +// Accounts. +export type CreateCollectionInstructionAccounts = { + /** The address of the new asset */ + collectionAddress: Signer; + /** The authority of the new asset */ + updateAuthority?: PublicKey | Pda; + /** The account paying for the storage fees */ + payer?: Signer; + /** The owner of the new asset. Defaults to the authority if not present. */ + owner?: PublicKey | Pda; + /** The system program */ + systemProgram?: PublicKey | Pda; +}; + +// Data. +export type CreateCollectionInstructionData = { + discriminator: number; + name: string; + uri: string; + plugins: Array; +}; + +export type CreateCollectionInstructionDataArgs = { + name: string; + uri: string; + plugins: Array; +}; + +export function getCreateCollectionInstructionDataSerializer(): Serializer< + CreateCollectionInstructionDataArgs, + CreateCollectionInstructionData +> { + return mapSerializer< + CreateCollectionInstructionDataArgs, + any, + CreateCollectionInstructionData + >( + struct( + [ + ['discriminator', u8()], + ['name', string()], + ['uri', string()], + ['plugins', array(getPluginSerializer())], + ], + { description: 'CreateCollectionInstructionData' } + ), + (value) => ({ ...value, discriminator: 1 }) + ) as Serializer< + CreateCollectionInstructionDataArgs, + CreateCollectionInstructionData + >; +} + +// Args. +export type CreateCollectionInstructionArgs = + CreateCollectionInstructionDataArgs; + +// Instruction. +export function createCollection( + context: Pick, + input: CreateCollectionInstructionAccounts & CreateCollectionInstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey( + 'mplCoreProgram', + 'CoREzp6dAdLVRKf3EM5tWrsXM2jQwRFeu5uhzsAyjYXL' + ); + + // Accounts. + const resolvedAccounts: ResolvedAccountsWithIndices = { + collectionAddress: { + index: 0, + isWritable: true, + value: input.collectionAddress ?? null, + }, + updateAuthority: { + index: 1, + isWritable: false, + value: input.updateAuthority ?? null, + }, + payer: { index: 2, isWritable: true, value: input.payer ?? null }, + owner: { index: 3, isWritable: false, value: input.owner ?? null }, + systemProgram: { + index: 4, + isWritable: false, + value: input.systemProgram ?? null, + }, + }; + + // Arguments. + const resolvedArgs: CreateCollectionInstructionArgs = { ...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 = getCreateCollectionInstructionDataSerializer().serialize( + resolvedArgs as CreateCollectionInstructionDataArgs + ); + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0; + + return transactionBuilder([ + { instruction: { keys, programId, data }, signers, bytesCreatedOnChain }, + ]); +} diff --git a/clients/js/src/generated/instructions/decompress.ts b/clients/js/src/generated/instructions/decompress.ts index 43fdd5ea..d9b736db 100644 --- a/clients/js/src/generated/instructions/decompress.ts +++ b/clients/js/src/generated/instructions/decompress.ts @@ -71,7 +71,7 @@ export function getDecompressInstructionDataSerializer(): Serializer< ], { description: 'DecompressInstructionData' } ), - (value) => ({ ...value, discriminator: 10 }) + (value) => ({ ...value, discriminator: 11 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 5c97d748..6a0cbbb8 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -11,6 +11,7 @@ export * from './addPlugin'; export * from './burn'; export * from './compress'; export * from './create'; +export * from './createCollection'; export * from './decompress'; export * from './removeAuthority'; export * from './removePlugin'; diff --git a/clients/js/src/generated/instructions/removeAuthority.ts b/clients/js/src/generated/instructions/removeAuthority.ts index b2a0b1bc..e6513009 100644 --- a/clients/js/src/generated/instructions/removeAuthority.ts +++ b/clients/js/src/generated/instructions/removeAuthority.ts @@ -79,7 +79,7 @@ export function getRemoveAuthorityInstructionDataSerializer(): Serializer< ], { description: 'RemoveAuthorityInstructionData' } ), - (value) => ({ ...value, discriminator: 5 }) + (value) => ({ ...value, discriminator: 6 }) ) as Serializer< RemoveAuthorityInstructionDataArgs, RemoveAuthorityInstructionData diff --git a/clients/js/src/generated/instructions/removePlugin.ts b/clients/js/src/generated/instructions/removePlugin.ts index 694f5eff..8abf7c75 100644 --- a/clients/js/src/generated/instructions/removePlugin.ts +++ b/clients/js/src/generated/instructions/removePlugin.ts @@ -67,7 +67,7 @@ export function getRemovePluginInstructionDataSerializer(): Serializer< ], { description: 'RemovePluginInstructionData' } ), - (value) => ({ ...value, discriminator: 2 }) + (value) => ({ ...value, discriminator: 3 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/transfer.ts b/clients/js/src/generated/instructions/transfer.ts index 0f183342..6bf594c4 100644 --- a/clients/js/src/generated/instructions/transfer.ts +++ b/clients/js/src/generated/instructions/transfer.ts @@ -76,7 +76,7 @@ export function getTransferInstructionDataSerializer(): Serializer< ], { description: 'TransferInstructionData' } ), - (value) => ({ ...value, discriminator: 7 }) + (value) => ({ ...value, discriminator: 8 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/update.ts b/clients/js/src/generated/instructions/update.ts index de05407b..3ddfab56 100644 --- a/clients/js/src/generated/instructions/update.ts +++ b/clients/js/src/generated/instructions/update.ts @@ -71,7 +71,7 @@ export function getUpdateInstructionDataSerializer(): Serializer< ], { description: 'UpdateInstructionData' } ), - (value) => ({ ...value, discriminator: 8 }) + (value) => ({ ...value, discriminator: 9 }) ) as Serializer; } diff --git a/clients/js/src/generated/instructions/updatePlugin.ts b/clients/js/src/generated/instructions/updatePlugin.ts index 1443808e..7339068e 100644 --- a/clients/js/src/generated/instructions/updatePlugin.ts +++ b/clients/js/src/generated/instructions/updatePlugin.ts @@ -67,7 +67,7 @@ export function getUpdatePluginInstructionDataSerializer(): Serializer< ], { description: 'UpdatePluginInstructionData' } ), - (value) => ({ ...value, discriminator: 3 }) + (value) => ({ ...value, discriminator: 4 }) ) as Serializer; } diff --git a/clients/js/src/generated/types/key.ts b/clients/js/src/generated/types/key.ts index e7d0fac8..ee903dc0 100644 --- a/clients/js/src/generated/types/key.ts +++ b/clients/js/src/generated/types/key.ts @@ -14,6 +14,7 @@ export enum Key { HashedAsset, PluginHeader, PluginRegistry, + Collection, } export type KeyArgs = Key; diff --git a/clients/js/src/hooked/fetchAssetWithPlugins.ts b/clients/js/src/hooked/fetchAssetWithPlugins.ts index 3c1b22ce..25407573 100644 --- a/clients/js/src/hooked/fetchAssetWithPlugins.ts +++ b/clients/js/src/hooked/fetchAssetWithPlugins.ts @@ -8,11 +8,7 @@ import { } from '@metaplex-foundation/umi'; import { Asset, - Authority, - Plugin, - PluginHeader, PluginHeaderAccountData, - PluginRegistry, PluginRegistryAccountData, deserializeAsset, getAssetAccountDataSerializer, @@ -20,17 +16,9 @@ import { getPluginRegistryAccountDataSerializer, getPluginSerializer, } from '../generated'; +import { PluginList, PluginWithAuthorities } from '.'; -export type PluginWithAuthorities = { - plugin: Plugin; - authorities: Authority[]; -}; -export type PluginList = { - pluginHeader?: Omit; - plugins?: PluginWithAuthorities[]; - pluginRegistry?: Omit; -}; export type AssetWithPlugins = Asset & PluginList; export async function fetchAssetWithPlugins( diff --git a/clients/js/src/hooked/fetchCollectionWithPlugins.ts b/clients/js/src/hooked/fetchCollectionWithPlugins.ts new file mode 100644 index 00000000..3a319198 --- /dev/null +++ b/clients/js/src/hooked/fetchCollectionWithPlugins.ts @@ -0,0 +1,65 @@ +import { + Context, + Pda, + PublicKey, + RpcGetAccountOptions, + assertAccountExists, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; +import { + CollectionData, + PluginHeaderAccountData, + PluginRegistryAccountData, + deserializeCollectionData, + getCollectionDataAccountDataSerializer, + getPluginHeaderAccountDataSerializer, + getPluginRegistryAccountDataSerializer, + getPluginSerializer, +} from '../generated'; +import { PluginList, PluginWithAuthorities } from '.'; + +export type CollectionWithPlugins = CollectionData & PluginList; + +export async function fetchCollectionWithPlugins( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount( + toPublicKey(publicKey, false), + options + ); + assertAccountExists(maybeAccount, 'Collection'); + const collection = deserializeCollectionData(maybeAccount); + const collectionData = getCollectionDataAccountDataSerializer().serialize(collection); + + let pluginHeader: PluginHeaderAccountData | undefined; + let pluginRegistry: PluginRegistryAccountData | undefined; + let plugins: PluginWithAuthorities[] | undefined; + if (maybeAccount.data.length !== collectionData.length) { + [pluginHeader] = getPluginHeaderAccountDataSerializer().deserialize( + maybeAccount.data, + collectionData.length + ); + [pluginRegistry] = getPluginRegistryAccountDataSerializer().deserialize( + maybeAccount.data, + Number(pluginHeader.pluginRegistryOffset) + ); + plugins = pluginRegistry.registry.map((record) => ({ + plugin: getPluginSerializer().deserialize( + maybeAccount.data, + Number(record.offset) + )[0], + authorities: record.authorities, + })); + } + + const collectionWithPlugins: CollectionWithPlugins = { + pluginHeader, + plugins, + pluginRegistry, + ...collection, + }; + + return collectionWithPlugins; +} diff --git a/clients/js/src/hooked/index.ts b/clients/js/src/hooked/index.ts index 9ef22b6a..1b2d2acc 100644 --- a/clients/js/src/hooked/index.ts +++ b/clients/js/src/hooked/index.ts @@ -1 +1,15 @@ +import { Authority, PluginHeader, PluginRegistry, Plugin } from 'src/generated'; + export * from './fetchAssetWithPlugins'; +export * from './fetchCollectionWithPlugins'; + +export type PluginWithAuthorities = { + plugin: Plugin; + authorities: Authority[]; +}; + +export type PluginList = { + pluginHeader?: Omit; + plugins?: PluginWithAuthorities[]; + pluginRegistry?: Omit; +}; \ No newline at end of file diff --git a/clients/js/test/createCollection.test.ts b/clients/js/test/createCollection.test.ts new file mode 100644 index 00000000..ab1385ae --- /dev/null +++ b/clients/js/test/createCollection.test.ts @@ -0,0 +1,148 @@ +import { generateSigner } from '@metaplex-foundation/umi'; +import test from 'ava'; +import { + AssetWithPlugins, + CollectionData, + CollectionWithPlugins, + DataState, + create, + createCollection, + fetchAssetWithPlugins, + fetchCollectionData, + fetchCollectionWithPlugins, +} from '../src'; +import { createUmi } from './_setup'; + +test('it can create a new collection', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const collectionAddress = generateSigner(umi); + + // When we create a new account. + await createCollection(umi, { + collectionAddress, + name: 'Test Bread Collection', + uri: 'https://example.com/bread', + plugins: [] + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const collection = await fetchCollectionData(umi, collectionAddress.publicKey); + // console.log("Account State:", collection); + t.like(collection, { + publicKey: collectionAddress.publicKey, + updateAuthority: umi.identity.publicKey, + name: 'Test Bread Collection', + uri: 'https://example.com/bread', + }); +}); + +test('it can create a new collection with plugins', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const collectionAddress = generateSigner(umi); + + // When we create a new account. + await createCollection(umi, { + collectionAddress, + name: 'Test Bread Collection', + uri: 'https://example.com/bread', + plugins: [{ __kind: 'Freeze', fields: [{ frozen: false }] }] + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const collection = await fetchCollectionWithPlugins(umi, collectionAddress.publicKey); + // console.log("Account State:", collection); + t.like(collection, { + publicKey: collectionAddress.publicKey, + updateAuthority: umi.identity.publicKey, + name: 'Test Bread Collection', + uri: 'https://example.com/bread', + pluginHeader: { + key: 3, + pluginRegistryOffset: BigInt(106), + }, + pluginRegistry: { + key: 4, + registry: [ + { + pluginType: 2, + offset: BigInt(104), + authorities: [{ __kind: 'Owner' }], + }, + ], + }, + plugins: [ + { + authorities: [{ __kind: 'Owner' }], + plugin: { + __kind: 'Freeze', + fields: [{ frozen: false }], + }, + }, + ], + }); +}); + +test('it can create a new asset with a collection', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const collectionAddress = generateSigner(umi); + const assetAddress = generateSigner(umi); + + // When we create a new account. + await createCollection(umi, { + collectionAddress, + name: 'Test Bread Collection', + uri: 'https://example.com/bread', + plugins: [{ __kind: 'Freeze', fields: [{ frozen: false }] }] + }).sendAndConfirm(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + plugins: [{ + __kind: 'Collection', fields: [{ + collectionAddress: collectionAddress.publicKey, + managed: true + }] + }], + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const asset = await fetchAssetWithPlugins(umi, assetAddress.publicKey); + // console.log("Account State:", asset); + t.like(asset, { + publicKey: assetAddress.publicKey, + updateAuthority: umi.identity.publicKey, + owner: umi.identity.publicKey, + name: 'Test Bread', + uri: 'https://example.com/bread', + pluginHeader: { + key: 3, + pluginRegistryOffset: BigInt(151), + }, + pluginRegistry: { + key: 4, + registry: [ + { + pluginType: 5, + offset: BigInt(117), + authorities: [{ __kind: 'UpdateAuthority' }], + }, + ], + }, + plugins: [ + { + authorities: [{ __kind: 'UpdateAuthority' }], + plugin: { + __kind: 'Collection', + fields: [{ collectionAddress: collectionAddress.publicKey, managed: true }], + }, + }, + ], + }); +}); \ No newline at end of file diff --git a/clients/rust/src/generated/accounts/collection_data.rs b/clients/rust/src/generated/accounts/collection_data.rs new file mode 100644 index 00000000..3fd21140 --- /dev/null +++ b/clients/rust/src/generated/accounts/collection_data.rs @@ -0,0 +1,45 @@ +//! 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::Key; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; +use solana_program::pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CollectionData { + pub key: Key, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub update_authority: Pubkey, + pub name: String, + pub uri: String, + pub num_minted: u32, + pub current_size: u32, +} + +impl CollectionData { + #[inline(always)] + pub fn from_bytes(data: &[u8]) -> Result { + let mut data = data; + Self::deserialize(&mut data) + } +} + +impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for CollectionData { + type Error = std::io::Error; + + fn try_from( + account_info: &solana_program::account_info::AccountInfo<'a>, + ) -> Result { + let mut data: &[u8] = &(*account_info.data).borrow(); + Self::deserialize(&mut data) + } +} diff --git a/clients/rust/src/generated/accounts/mod.rs b/clients/rust/src/generated/accounts/mod.rs index 6f60f76e..8c790ac5 100644 --- a/clients/rust/src/generated/accounts/mod.rs +++ b/clients/rust/src/generated/accounts/mod.rs @@ -6,11 +6,13 @@ //! pub(crate) mod asset; +pub(crate) mod collection_data; pub(crate) mod hashed_asset; pub(crate) mod plugin_header; pub(crate) mod plugin_registry; pub use self::asset::*; +pub use self::collection_data::*; pub use self::hashed_asset::*; pub use self::plugin_header::*; pub use self::plugin_registry::*; diff --git a/clients/rust/src/generated/instructions/add_authority.rs b/clients/rust/src/generated/instructions/add_authority.rs index 52f86514..cff4c423 100644 --- a/clients/rust/src/generated/instructions/add_authority.rs +++ b/clients/rust/src/generated/instructions/add_authority.rs @@ -101,7 +101,7 @@ struct AddAuthorityInstructionData { impl AddAuthorityInstructionData { fn new() -> Self { - Self { discriminator: 4 } + Self { discriminator: 5 } } } diff --git a/clients/rust/src/generated/instructions/add_plugin.rs b/clients/rust/src/generated/instructions/add_plugin.rs index 8f24d7cf..0604bf83 100644 --- a/clients/rust/src/generated/instructions/add_plugin.rs +++ b/clients/rust/src/generated/instructions/add_plugin.rs @@ -100,7 +100,7 @@ struct AddPluginInstructionData { impl AddPluginInstructionData { fn new() -> Self { - Self { discriminator: 1 } + Self { discriminator: 2 } } } diff --git a/clients/rust/src/generated/instructions/burn.rs b/clients/rust/src/generated/instructions/burn.rs index a8fcf2e8..57d21b8e 100644 --- a/clients/rust/src/generated/instructions/burn.rs +++ b/clients/rust/src/generated/instructions/burn.rs @@ -94,7 +94,7 @@ struct BurnInstructionData { impl BurnInstructionData { fn new() -> Self { - Self { discriminator: 6 } + Self { discriminator: 7 } } } diff --git a/clients/rust/src/generated/instructions/compress.rs b/clients/rust/src/generated/instructions/compress.rs index 2f76134b..332ddfcd 100644 --- a/clients/rust/src/generated/instructions/compress.rs +++ b/clients/rust/src/generated/instructions/compress.rs @@ -80,7 +80,7 @@ struct CompressInstructionData { impl CompressInstructionData { fn new() -> Self { - Self { discriminator: 9 } + Self { discriminator: 10 } } } diff --git a/clients/rust/src/generated/instructions/create_collection.rs b/clients/rust/src/generated/instructions/create_collection.rs new file mode 100644 index 00000000..ff43c470 --- /dev/null +++ b/clients/rust/src/generated/instructions/create_collection.rs @@ -0,0 +1,544 @@ +//! 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::Plugin; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +/// Accounts. +pub struct CreateCollection { + /// The address of the new asset + pub collection_address: solana_program::pubkey::Pubkey, + /// The authority of the new asset + pub update_authority: Option, + /// The account paying for the storage fees + pub payer: solana_program::pubkey::Pubkey, + /// The owner of the new asset. Defaults to the authority if not present. + pub owner: Option, + /// The system program + pub system_program: solana_program::pubkey::Pubkey, +} + +impl CreateCollection { + pub fn instruction( + &self, + args: CreateCollectionInstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: CreateCollectionInstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.collection_address, + true, + )); + if let Some(update_authority) = self.update_authority { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + update_authority, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_PROGRAM_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + if let Some(owner) = self.owner { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + owner, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_PROGRAM_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = CreateCollectionInstructionData::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_PROGRAM_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct CreateCollectionInstructionData { + discriminator: u8, +} + +impl CreateCollectionInstructionData { + fn new() -> Self { + Self { discriminator: 1 } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CreateCollectionInstructionArgs { + pub name: String, + pub uri: String, + pub plugins: Vec, +} + +/// Instruction builder. +#[derive(Default)] +pub struct CreateCollectionBuilder { + collection_address: Option, + update_authority: Option, + payer: Option, + owner: Option, + system_program: Option, + name: Option, + uri: Option, + plugins: Option>, + __remaining_accounts: Vec, +} + +impl CreateCollectionBuilder { + pub fn new() -> Self { + Self::default() + } + /// The address of the new asset + #[inline(always)] + pub fn collection_address( + &mut self, + collection_address: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.collection_address = Some(collection_address); + self + } + /// `[optional account]` + /// The authority of the new asset + #[inline(always)] + pub fn update_authority( + &mut self, + update_authority: Option, + ) -> &mut Self { + self.update_authority = update_authority; + 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 owner of the new asset. Defaults to the authority if not present. + #[inline(always)] + pub fn owner(&mut self, owner: Option) -> &mut Self { + self.owner = owner; + 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 + } + #[inline(always)] + pub fn name(&mut self, name: String) -> &mut Self { + self.name = Some(name); + self + } + #[inline(always)] + pub fn uri(&mut self, uri: String) -> &mut Self { + self.uri = Some(uri); + self + } + #[inline(always)] + pub fn plugins(&mut self, plugins: Vec) -> &mut Self { + self.plugins = Some(plugins); + 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 = CreateCollection { + collection_address: self + .collection_address + .expect("collection_address is not set"), + update_authority: self.update_authority, + payer: self.payer.expect("payer is not set"), + owner: self.owner, + system_program: self + .system_program + .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), + }; + let args = CreateCollectionInstructionArgs { + name: self.name.clone().expect("name is not set"), + uri: self.uri.clone().expect("uri is not set"), + plugins: self.plugins.clone().expect("plugins is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `create_collection` CPI accounts. +pub struct CreateCollectionCpiAccounts<'a, 'b> { + /// The address of the new asset + pub collection_address: &'b solana_program::account_info::AccountInfo<'a>, + /// The authority of the new asset + pub update_authority: 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 owner of the new asset. Defaults to the authority if not present. + pub owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `create_collection` CPI instruction. +pub struct CreateCollectionCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + /// The address of the new asset + pub collection_address: &'b solana_program::account_info::AccountInfo<'a>, + /// The authority of the new asset + pub update_authority: 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 owner of the new asset. Defaults to the authority if not present. + pub owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: CreateCollectionInstructionArgs, +} + +impl<'a, 'b> CreateCollectionCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: CreateCollectionCpiAccounts<'a, 'b>, + args: CreateCollectionInstructionArgs, + ) -> Self { + Self { + __program: program, + collection_address: accounts.collection_address, + update_authority: accounts.update_authority, + payer: accounts.payer, + owner: accounts.owner, + system_program: accounts.system_program, + __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(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.collection_address.key, + true, + )); + if let Some(update_authority) = self.update_authority { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *update_authority.key, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_PROGRAM_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new( + *self.payer.key, + true, + )); + if let Some(owner) = self.owner { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *owner.key, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_PROGRAM_ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.system_program.key, + 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 = CreateCollectionInstructionData::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_PROGRAM_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(5 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.collection_address.clone()); + if let Some(update_authority) = self.update_authority { + account_infos.push(update_authority.clone()); + } + account_infos.push(self.payer.clone()); + if let Some(owner) = self.owner { + account_infos.push(owner.clone()); + } + account_infos.push(self.system_program.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) + } + } +} + +/// `create_collection` CPI instruction builder. +pub struct CreateCollectionCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> CreateCollectionCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(CreateCollectionCpiBuilderInstruction { + __program: program, + collection_address: None, + update_authority: None, + payer: None, + owner: None, + system_program: None, + name: None, + uri: None, + plugins: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + /// The address of the new asset + #[inline(always)] + pub fn collection_address( + &mut self, + collection_address: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.collection_address = Some(collection_address); + self + } + /// `[optional account]` + /// The authority of the new asset + #[inline(always)] + pub fn update_authority( + &mut self, + update_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.update_authority = update_authority; + 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 owner of the new asset. Defaults to the authority if not present. + #[inline(always)] + pub fn owner( + &mut self, + owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.owner = owner; + 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 + } + #[inline(always)] + pub fn name(&mut self, name: String) -> &mut Self { + self.instruction.name = Some(name); + self + } + #[inline(always)] + pub fn uri(&mut self, uri: String) -> &mut Self { + self.instruction.uri = Some(uri); + self + } + #[inline(always)] + pub fn plugins(&mut self, plugins: Vec) -> &mut Self { + self.instruction.plugins = Some(plugins); + 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 = CreateCollectionInstructionArgs { + name: self.instruction.name.clone().expect("name is not set"), + uri: self.instruction.uri.clone().expect("uri is not set"), + plugins: self + .instruction + .plugins + .clone() + .expect("plugins is not set"), + }; + let instruction = CreateCollectionCpi { + __program: self.instruction.__program, + + collection_address: self + .instruction + .collection_address + .expect("collection_address is not set"), + + update_authority: self.instruction.update_authority, + + payer: self.instruction.payer.expect("payer is not set"), + + owner: self.instruction.owner, + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +struct CreateCollectionCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + collection_address: Option<&'b solana_program::account_info::AccountInfo<'a>>, + update_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, + system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + name: Option, + uri: Option, + plugins: Option>, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust/src/generated/instructions/decompress.rs b/clients/rust/src/generated/instructions/decompress.rs index a793736c..6935fb1c 100644 --- a/clients/rust/src/generated/instructions/decompress.rs +++ b/clients/rust/src/generated/instructions/decompress.rs @@ -87,7 +87,7 @@ struct DecompressInstructionData { impl DecompressInstructionData { fn new() -> Self { - Self { discriminator: 10 } + Self { discriminator: 11 } } } diff --git a/clients/rust/src/generated/instructions/mod.rs b/clients/rust/src/generated/instructions/mod.rs index ada86437..cb69ec97 100644 --- a/clients/rust/src/generated/instructions/mod.rs +++ b/clients/rust/src/generated/instructions/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod add_plugin; pub(crate) mod burn; pub(crate) mod compress; pub(crate) mod create; +pub(crate) mod create_collection; pub(crate) mod decompress; pub(crate) mod remove_authority; pub(crate) mod remove_plugin; @@ -22,6 +23,7 @@ pub use self::add_plugin::*; pub use self::burn::*; pub use self::compress::*; pub use self::create::*; +pub use self::create_collection::*; pub use self::decompress::*; pub use self::remove_authority::*; pub use self::remove_plugin::*; diff --git a/clients/rust/src/generated/instructions/remove_authority.rs b/clients/rust/src/generated/instructions/remove_authority.rs index 8dae6330..e838249a 100644 --- a/clients/rust/src/generated/instructions/remove_authority.rs +++ b/clients/rust/src/generated/instructions/remove_authority.rs @@ -101,7 +101,7 @@ struct RemoveAuthorityInstructionData { impl RemoveAuthorityInstructionData { fn new() -> Self { - Self { discriminator: 5 } + Self { discriminator: 6 } } } diff --git a/clients/rust/src/generated/instructions/remove_plugin.rs b/clients/rust/src/generated/instructions/remove_plugin.rs index 4d7282cf..0fbba0c3 100644 --- a/clients/rust/src/generated/instructions/remove_plugin.rs +++ b/clients/rust/src/generated/instructions/remove_plugin.rs @@ -100,7 +100,7 @@ struct RemovePluginInstructionData { impl RemovePluginInstructionData { fn new() -> Self { - Self { discriminator: 2 } + Self { discriminator: 3 } } } diff --git a/clients/rust/src/generated/instructions/transfer.rs b/clients/rust/src/generated/instructions/transfer.rs index 8d62812a..c3520dce 100644 --- a/clients/rust/src/generated/instructions/transfer.rs +++ b/clients/rust/src/generated/instructions/transfer.rs @@ -100,7 +100,7 @@ struct TransferInstructionData { impl TransferInstructionData { fn new() -> Self { - Self { discriminator: 7 } + Self { discriminator: 8 } } } diff --git a/clients/rust/src/generated/instructions/update.rs b/clients/rust/src/generated/instructions/update.rs index 02042427..9f188bea 100644 --- a/clients/rust/src/generated/instructions/update.rs +++ b/clients/rust/src/generated/instructions/update.rs @@ -100,7 +100,7 @@ struct UpdateInstructionData { impl UpdateInstructionData { fn new() -> Self { - Self { discriminator: 8 } + Self { discriminator: 9 } } } diff --git a/clients/rust/src/generated/instructions/update_plugin.rs b/clients/rust/src/generated/instructions/update_plugin.rs index 64a4d1be..c15d1892 100644 --- a/clients/rust/src/generated/instructions/update_plugin.rs +++ b/clients/rust/src/generated/instructions/update_plugin.rs @@ -100,7 +100,7 @@ struct UpdatePluginInstructionData { impl UpdatePluginInstructionData { fn new() -> Self { - Self { discriminator: 3 } + Self { discriminator: 4 } } } diff --git a/clients/rust/src/generated/types/key.rs b/clients/rust/src/generated/types/key.rs index fac43a60..d5fe51b3 100644 --- a/clients/rust/src/generated/types/key.rs +++ b/clients/rust/src/generated/types/key.rs @@ -16,4 +16,5 @@ pub enum Key { HashedAsset, PluginHeader, PluginRegistry, + Collection, } diff --git a/idls/mpl_core_program.json b/idls/mpl_core_program.json index 27d106fc..82201e69 100644 --- a/idls/mpl_core_program.json +++ b/idls/mpl_core_program.json @@ -79,6 +79,65 @@ "value": 0 } }, + { + "name": "CreateCollection", + "accounts": [ + { + "name": "collectionAddress", + "isMut": true, + "isSigner": true, + "docs": [ + "The address of the new asset" + ] + }, + { + "name": "updateAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "The authority of the new asset" + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "The account paying for the storage fees" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "The owner of the new asset. Defaults to the authority if not present." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The system program" + ] + } + ], + "args": [ + { + "name": "createCollectionArgs", + "type": { + "defined": "CreateCollectionArgs" + } + } + ], + "discriminant": { + "type": "u8", + "value": 1 + } + }, { "name": "AddPlugin", "accounts": [ @@ -144,7 +203,7 @@ ], "discriminant": { "type": "u8", - "value": 1 + "value": 2 } }, { @@ -212,7 +271,7 @@ ], "discriminant": { "type": "u8", - "value": 2 + "value": 3 } }, { @@ -280,7 +339,7 @@ ], "discriminant": { "type": "u8", - "value": 3 + "value": 4 } }, { @@ -348,7 +407,7 @@ ], "discriminant": { "type": "u8", - "value": 4 + "value": 5 } }, { @@ -416,7 +475,7 @@ ], "discriminant": { "type": "u8", - "value": 5 + "value": 6 } }, { @@ -476,7 +535,7 @@ ], "discriminant": { "type": "u8", - "value": 6 + "value": 7 } }, { @@ -544,7 +603,7 @@ ], "discriminant": { "type": "u8", - "value": 7 + "value": 8 } }, { @@ -612,7 +671,7 @@ ], "discriminant": { "type": "u8", - "value": 8 + "value": 9 } }, { @@ -671,7 +730,7 @@ ], "discriminant": { "type": "u8", - "value": 9 + "value": 10 } }, { @@ -730,7 +789,7 @@ ], "discriminant": { "type": "u8", - "value": 10 + "value": 11 } } ], @@ -813,6 +872,40 @@ ] } }, + { + "name": "CollectionData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "key", + "type": { + "defined": "Key" + } + }, + { + "name": "updateAuthority", + "type": "publicKey" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "numMinted", + "type": "u32" + }, + { + "name": "currentSize", + "type": "u32" + } + ] + } + }, { "name": "HashedAsset", "type": { @@ -1053,6 +1146,30 @@ ] } }, + { + "name": "CreateCollectionArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "plugins", + "type": { + "vec": { + "defined": "Plugin" + } + } + } + ] + } + }, { "name": "DecompressArgs", "type": { @@ -1444,6 +1561,9 @@ }, { "name": "PluginRegistry" + }, + { + "name": "Collection" } ] } diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index b1af6df9..0b1f8991 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -3,16 +3,17 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::{ShankContext, ShankInstruction}; use crate::processor::{ - AddAuthorityArgs, AddPluginArgs, BurnArgs, CompressArgs, CreateArgs, DecompressArgs, - RemoveAuthorityArgs, RemovePluginArgs, TransferArgs, UpdateArgs, UpdatePluginArgs, + AddAuthorityArgs, AddPluginArgs, BurnArgs, CompressArgs, CreateArgs, CreateCollectionArgs, + DecompressArgs, RemoveAuthorityArgs, RemovePluginArgs, TransferArgs, UpdateArgs, + UpdatePluginArgs, }; /// Instructions supported by the mpl-core program. #[derive(BorshDeserialize, BorshSerialize, Clone, Debug, ShankContext, ShankInstruction)] #[rustfmt::skip] pub enum MplAssetInstruction { - /// Create a new mpl-core. - /// This function creates the initial mpl-core + /// Create a new mpl-core Asset. + /// This function creates the initial Asset, with or without plugins. #[account(0, writable, signer, name="asset_address", desc = "The address of the new asset")] #[account(1, optional, writable, name="collection", desc = "The collection to which the asset belongs")] #[account(2, optional, name="update_authority", desc = "The authority of the new asset")] @@ -22,6 +23,15 @@ pub enum MplAssetInstruction { #[account(6, optional, name="log_wrapper", desc = "The SPL Noop Program")] Create(CreateArgs), + /// Create a new mpl-core Collection. + /// This function creates the initial Collection, with or without plugins. + #[account(0, writable, signer, name="collection_address", desc = "The address of the new asset")] + #[account(1, optional, name="update_authority", desc = "The authority of the new asset")] + #[account(2, writable, signer, name="payer", desc = "The account paying for the storage fees")] + #[account(3, optional, name="owner", desc = "The owner of the new asset. Defaults to the authority if not present.")] + #[account(4, name="system_program", desc = "The system program")] + CreateCollection(CreateCollectionArgs), + /// Add a plugin to an mpl-core. #[account(0, writable, name="asset_address", desc = "The address of the asset")] #[account(1, optional, writable, name="collection", desc = "The collection to which the asset belongs")] diff --git a/programs/mpl-core/src/plugins/collection.rs b/programs/mpl-core/src/plugins/collection.rs index 93d73c88..252d2cf5 100644 --- a/programs/mpl-core/src/plugins/collection.rs +++ b/programs/mpl-core/src/plugins/collection.rs @@ -7,7 +7,7 @@ use crate::{ UpdateAccounts, }, processor::{BurnArgs, CompressArgs, CreateArgs, DecompressArgs, TransferArgs, UpdateArgs}, - state::Authority, + state::{Authority, CollectionData, DataBlob, SolanaAccount}, }; use super::{PluginValidation, ValidationResult}; @@ -16,22 +16,52 @@ use super::{PluginValidation, ValidationResult}; #[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Eq, PartialEq)] pub struct Collection { /// A pointer to the collection which the asset is a part of. - collection_address: Pubkey, + collection_address: Pubkey, // 32 /// This flag indicates if the collection is required when operating on the asset. /// Managed collections use the Collection as a parent which can store plugins that /// are applied to all assets in the collection by default. Plugins on the asset itself /// can override the collection plugins. - managed: bool, + managed: bool, // 1 +} + +impl DataBlob for Collection { + fn get_initial_size() -> usize { + 33 + } + + fn get_size(&self) -> usize { + 33 + } } impl PluginValidation for Collection { fn validate_create( &self, - _ctx: &CreateAccounts, + ctx: &CreateAccounts, _args: &CreateArgs, _authorities: &[Authority], ) -> Result { - Ok(ValidationResult::Pass) + match ctx.collection { + Some(collection) => { + let collection = CollectionData::load(collection, 0)?; + if ctx.payer.is_signer + || (ctx.update_authority.is_some() && ctx.update_authority.unwrap().is_signer) + { + if collection.update_authority != *ctx.payer.key { + return Ok(ValidationResult::Rejected); + } + } else { + return Ok(ValidationResult::Rejected); + } + Ok(ValidationResult::Pass) + } + None => { + if self.managed { + return Ok(ValidationResult::Rejected); + } + Ok(ValidationResult::Pass) + } + } } fn validate_update( diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 7f9aa613..ad537712 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -32,6 +32,7 @@ impl PluginType { pub fn check_create(&self) -> CheckResult { #[allow(clippy::match_single_binding)] match self { + PluginType::Collection => CheckResult::CanReject, _ => CheckResult::None, } } diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index e2ebb0e2..cdf99ab6 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -7,8 +7,8 @@ use solana_program::{ use crate::{ error::MplCoreError, - state::{Asset, Authority, DataBlob, Key, SolanaAccount}, - utils::{assert_authority, resolve_authority_to_default}, + state::{Asset, Authority, CollectionData, DataBlob, Key, SolanaAccount}, + utils::{assert_authority, load_key, resolve_authority_to_default}, }; use super::{Plugin, PluginHeader, PluginRegistry, PluginType, RegistryRecord}; @@ -19,17 +19,32 @@ pub fn create_meta_idempotent<'a>( payer: &AccountInfo<'a>, system_program: &AccountInfo<'a>, ) -> ProgramResult { - let asset = { - let mut bytes: &[u8] = &(*account.data).borrow(); - Asset::deserialize(&mut bytes)? + let header_offset = match load_key(account, 0)? { + Key::Asset => { + let asset = { + let mut bytes: &[u8] = &(*account.data).borrow(); + Asset::deserialize(&mut bytes)? + }; + + asset.get_size() + } + Key::Collection => { + let collection = { + let mut bytes: &[u8] = &(*account.data).borrow(); + CollectionData::deserialize(&mut bytes)? + }; + + collection.get_size() + } + _ => return Err(MplCoreError::IncorrectAccount.into()), }; // Check if the plugin header and registry exist. - if asset.get_size() == account.data_len() { + if header_offset == account.data_len() { // They don't exist, so create them. let header = PluginHeader { key: Key::PluginHeader, - plugin_registry_offset: asset.get_size() + PluginHeader::get_initial_size(), + plugin_registry_offset: header_offset + PluginHeader::get_initial_size(), }; let registry = PluginRegistry { key: Key::PluginRegistry, @@ -44,7 +59,7 @@ pub fn create_meta_idempotent<'a>( header.plugin_registry_offset + PluginRegistry::get_initial_size(), )?; - header.save(account, asset.get_size())?; + header.save(account, header_offset)?; registry.save(account, header.plugin_registry_offset)?; } @@ -124,13 +139,28 @@ pub fn initialize_plugin<'a>( payer: &AccountInfo<'a>, system_program: &AccountInfo<'a>, ) -> ProgramResult { - let asset = { - let mut bytes: &[u8] = &(*account.data).borrow(); - Asset::deserialize(&mut bytes)? + let header_offset = match load_key(account, 0)? { + Key::Asset => { + let asset = { + let mut bytes: &[u8] = &(*account.data).borrow(); + Asset::deserialize(&mut bytes)? + }; + + asset.get_size() + } + Key::Collection => { + let collection = { + let mut bytes: &[u8] = &(*account.data).borrow(); + CollectionData::deserialize(&mut bytes)? + }; + + collection.get_size() + } + _ => return Err(MplCoreError::IncorrectAccount.into()), }; //TODO: Bytemuck this. - let mut header = PluginHeader::load(account, asset.get_size())?; + let mut header = PluginHeader::load(account, header_offset)?; let mut plugin_registry = PluginRegistry::load(account, header.plugin_registry_offset)?; let plugin_type = plugin.into(); @@ -174,7 +204,7 @@ pub fn initialize_plugin<'a>( .ok_or(MplCoreError::NumericalOverflow)?; resize_or_reallocate_account_raw(account, payer, system_program, new_size)?; - header.save(account, asset.get_size())?; + header.save(account, header_offset)?; plugin.save(account, old_registry_offset)?; plugin_registry.save(account, new_registry_offset)?; diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 09af1c44..7550d77c 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -8,8 +8,9 @@ use solana_program::{ use crate::{ error::MplCoreError, instruction::accounts::CreateAccounts, - plugins::{create_meta_idempotent, initialize_plugin, Plugin}, + plugins::{create_meta_idempotent, initialize_plugin, CheckResult, Plugin, ValidationResult}, state::{Asset, Compressible, DataState, HashedAsset, Key}, + utils::fetch_core_data, }; #[repr(C)] @@ -46,8 +47,8 @@ pub(crate) fn create<'a>(accounts: &'a [AccountInfo<'a>], args: CreateArgs) -> P .owner .unwrap_or(ctx.accounts.update_authority.unwrap_or(ctx.accounts.payer)) .key, - name: args.name, - uri: args.uri, + name: args.name.clone(), + uri: args.uri.clone(), }; let serialized_data = new_asset.try_to_vec()?; @@ -93,15 +94,52 @@ pub(crate) fn create<'a>(accounts: &'a [AccountInfo<'a>], args: CreateArgs) -> P ctx.accounts.system_program, )?; - for plugin in args.plugins { + for plugin in &args.plugins { initialize_plugin( - &plugin, + plugin, &plugin.default_authority()?, ctx.accounts.asset_address, ctx.accounts.payer, ctx.accounts.system_program, )?; } + + let (_, _, plugin_registry) = fetch_core_data(ctx.accounts.asset_address)?; + + let mut approved = true; + // match Asset::check_create() { + // CheckResult::CanApprove | CheckResult::CanReject => { + // match asset.validate_create(&ctx.accounts)? { + // ValidationResult::Approved => { + // approved = true; + // } + // ValidationResult::Rejected => return Err(MplCoreError::InvalidAuthority.into()), + // ValidationResult::Pass => (), + // } + // } + // CheckResult::None => (), + // }; + + if let Some(plugin_registry) = plugin_registry { + for record in plugin_registry.registry { + if matches!( + record.plugin_type.check_transfer(), + CheckResult::CanApprove | CheckResult::CanReject + ) { + let result = Plugin::load(ctx.accounts.asset_address, record.offset)? + .validate_create(&ctx.accounts, &args, &record.authorities)?; + if result == ValidationResult::Rejected { + return Err(MplCoreError::InvalidAuthority.into()); + } else if result == ValidationResult::Approved { + approved = true; + } + } + } + }; + + if !approved { + return Err(MplCoreError::InvalidAuthority.into()); + } } Ok(()) diff --git a/programs/mpl-core/src/processor/create_collection.rs b/programs/mpl-core/src/processor/create_collection.rs new file mode 100644 index 00000000..c6d4fb09 --- /dev/null +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -0,0 +1,102 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use mpl_utils::assert_signer; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, + program_memory::sol_memcpy, rent::Rent, system_instruction, system_program, sysvar::Sysvar, +}; + +use crate::{ + error::MplCoreError, + instruction::accounts::CreateCollectionAccounts, + plugins::{create_meta_idempotent, initialize_plugin, Plugin}, + state::{CollectionData, Key}, +}; + +#[repr(C)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] +pub struct CreateCollectionArgs { + pub name: String, + pub uri: String, + pub plugins: Vec, +} + +pub(crate) fn create_collection<'a>( + accounts: &'a [AccountInfo<'a>], + args: CreateCollectionArgs, +) -> ProgramResult { + // Accounts. + let ctx = CreateCollectionAccounts::context(accounts)?; + let rent = Rent::get()?; + + // Guards. + assert_signer(ctx.accounts.collection_address)?; + assert_signer(ctx.accounts.payer)?; + + if *ctx.accounts.system_program.key != system_program::id() { + return Err(MplCoreError::InvalidSystemProgram.into()); + } + + let new_collection = CollectionData { + key: Key::Collection, + update_authority: *ctx + .accounts + .update_authority + .unwrap_or(ctx.accounts.payer) + .key, + name: args.name, + uri: args.uri, + num_minted: 0, + current_size: 0, + }; + + let serialized_data = new_collection.try_to_vec()?; + + let lamports = rent.minimum_balance(serialized_data.len()); + + // CPI to the System Program. + invoke( + &system_instruction::create_account( + ctx.accounts.payer.key, + ctx.accounts.collection_address.key, + lamports, + serialized_data.len() as u64, + &crate::id(), + ), + &[ + ctx.accounts.payer.clone(), + ctx.accounts.collection_address.clone(), + ctx.accounts.system_program.clone(), + ], + )?; + + sol_memcpy( + &mut ctx.accounts.collection_address.try_borrow_mut_data()?, + &serialized_data, + serialized_data.len(), + ); + + drop(serialized_data); + + solana_program::msg!("Collection created."); + create_meta_idempotent( + ctx.accounts.collection_address, + ctx.accounts.payer, + ctx.accounts.system_program, + )?; + + solana_program::msg!("Meta created."); + + for plugin in args.plugins { + initialize_plugin( + &plugin, + &plugin.default_authority()?, + ctx.accounts.collection_address, + ctx.accounts.payer, + ctx.accounts.system_program, + )?; + } + + solana_program::msg!("Plugins initialized."); + + Ok(()) +} diff --git a/programs/mpl-core/src/processor/mod.rs b/programs/mpl-core/src/processor/mod.rs index 8335eed8..efbacb7f 100644 --- a/programs/mpl-core/src/processor/mod.rs +++ b/programs/mpl-core/src/processor/mod.rs @@ -5,6 +5,9 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, mod create; pub(crate) use create::*; +mod create_collection; +pub(crate) use create_collection::*; + mod add_plugin; pub(crate) use add_plugin::*; @@ -47,6 +50,10 @@ pub fn process_instruction<'a>( msg!("Instruction: Create"); create(accounts, args) } + MplAssetInstruction::CreateCollection(args) => { + msg!("Instruction: CreateCollection"); + create_collection(accounts, args) + } MplAssetInstruction::AddPlugin(args) => { msg!("Instruction: AddPlugin"); add_plugin(accounts, args) diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index 781792aa..765d8b1d 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -1,14 +1,61 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use shank::ShankAccount; use solana_program::pubkey::Pubkey; +use super::{DataBlob, Key, SolanaAccount}; + +/// The representation of a collection of assets. #[derive(Clone, BorshSerialize, BorshDeserialize, Debug, ShankAccount)] pub struct CollectionData { - pub key: Key, //1 + /// The account discriminator. + pub key: Key, //1 + /// The update authority of the collection. pub update_authority: Pubkey, //32 - pub owner: Pubkey, //32 - pub name: String, //4 - pub uri: String, //4 - pub num_minted: u64, //8 - pub num_migrated: u64, //8 - pub current_size: u64, //8 + /// The name of the collection. + pub name: String, //4 + /// The URI that links to what data to show for the collection. + pub uri: String, //4 + /// The number of assets minted in the collection. + pub num_minted: u32, //4 + /// The number of assets currently in the collection. + pub current_size: u32, //4 +} + +impl CollectionData { + /// The base length of the collection account with an empty name and uri. + pub const BASE_LENGTH: usize = 1 + 32 + 4 + 4 + 4 + 4; + + /// Create a new collection. + pub fn new( + update_authority: Pubkey, + name: String, + uri: String, + num_minted: u32, + current_size: u32, + ) -> Self { + Self { + key: Key::Collection, + update_authority, + name, + uri, + num_minted, + current_size, + } + } +} + +impl DataBlob for CollectionData { + fn get_initial_size() -> usize { + Self::BASE_LENGTH + } + + fn get_size(&self) -> usize { + Self::BASE_LENGTH + self.name.len() + self.uri.len() + } +} + +impl SolanaAccount for CollectionData { + fn key() -> Key { + Key::Collection + } } diff --git a/programs/mpl-core/src/state/mod.rs b/programs/mpl-core/src/state/mod.rs index 74edd8d0..84c38523 100644 --- a/programs/mpl-core/src/state/mod.rs +++ b/programs/mpl-core/src/state/mod.rs @@ -10,6 +10,9 @@ pub use hashed_asset_schema::*; mod traits; pub use traits::*; +mod collection; +pub use collection::*; + use borsh::{BorshDeserialize, BorshSerialize}; use num_derive::{FromPrimitive, ToPrimitive}; use solana_program::pubkey::Pubkey; @@ -87,6 +90,8 @@ pub enum Key { PluginHeader, /// A discriminator indicating the plugin registry. PluginRegistry, + /// A discriminator indicating the collection. + Collection, } impl Key {