diff --git a/clients/js/src/generated/instructions/create.ts b/clients/js/src/generated/instructions/create.ts index 81676844..a2bb664c 100644 --- a/clients/js/src/generated/instructions/create.ts +++ b/clients/js/src/generated/instructions/create.ts @@ -16,6 +16,7 @@ import { } from '@metaplex-foundation/umi'; import { Serializer, + array, mapSerializer, string, struct, @@ -26,7 +27,14 @@ import { ResolvedAccountsWithIndices, getAccountMetasAndSigners, } from '../shared'; -import { DataState, DataStateArgs, getDataStateSerializer } from '../types'; +import { + DataState, + DataStateArgs, + Plugin, + PluginArgs, + getDataStateSerializer, + getPluginSerializer, +} from '../types'; // Accounts. export type CreateInstructionAccounts = { @@ -52,12 +60,14 @@ export type CreateInstructionData = { dataState: DataState; name: string; uri: string; + plugins: Array; }; export type CreateInstructionDataArgs = { dataState: DataStateArgs; name: string; uri: string; + plugins: Array; }; export function getCreateInstructionDataSerializer(): Serializer< @@ -71,6 +81,7 @@ export function getCreateInstructionDataSerializer(): Serializer< ['dataState', getDataStateSerializer()], ['name', string()], ['uri', string()], + ['plugins', array(getPluginSerializer())], ], { description: 'CreateInstructionData' } ), diff --git a/clients/js/test/addAuthority.test.ts b/clients/js/test/addAuthority.test.ts index 637d5374..866561a9 100644 --- a/clients/js/test/addAuthority.test.ts +++ b/clients/js/test/addAuthority.test.ts @@ -26,6 +26,7 @@ test('it can add an authority to a plugin', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/addPlugin.test.ts b/clients/js/test/addPlugin.test.ts index 837fc958..f26ef17a 100644 --- a/clients/js/test/addPlugin.test.ts +++ b/clients/js/test/addPlugin.test.ts @@ -23,6 +23,7 @@ test('it can add a plugin to an asset', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/burn.test.ts b/clients/js/test/burn.test.ts index e2cedb6e..4a0faac6 100644 --- a/clients/js/test/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -19,6 +19,7 @@ test('it can burn an asset as the owner', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. @@ -58,6 +59,7 @@ test('it cannot burn an asset if not the owner', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/compress.test.ts b/clients/js/test/compress.test.ts index afe96179..f4d39493 100644 --- a/clients/js/test/compress.test.ts +++ b/clients/js/test/compress.test.ts @@ -9,11 +9,11 @@ import { fetchHashedAsset, getAssetAccountDataSerializer, getHashedAssetSchemaSerializer, + hash, HashedAssetSchema, } from '../src'; import { createUmi } from './_setup'; -//import bs58 from 'bs58'; -import { hash } from '../src'; +// import bs58 from 'bs58'; test('it can compress an asset without any plugins as the owner', async (t) => { // Given a Umi instance and a new signer. @@ -26,11 +26,12 @@ test('it can compress an asset without any plugins as the owner', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. const beforeAsset = await fetchAsset(umi, assetAddress.publicKey); - //console.log("Account State:", beforeAsset); + // console.log("Account State:", beforeAsset); t.like(beforeAsset, { publicKey: assetAddress.publicKey, updateAuthority: umi.identity.publicKey, @@ -44,19 +45,19 @@ test('it can compress an asset without any plugins as the owner', async (t) => { assetAddress: assetAddress.publicKey, owner: umi.identity, }).sendAndConfirm(umi); - //console.log('Compress signature: ', bs58.encode(tx.signature)); + // console.log('Compress signature: ', bs58.encode(tx.signature)); // And the asset is now compressed as a hashed asset. const afterAsset = await fetchHashedAsset(umi, assetAddress.publicKey); - //console.log("Account State:", afterAsset); + // console.log("Account State:", afterAsset); // And the hash matches the expected value. - let hashedAssetSchema: HashedAssetSchema = { + const hashedAssetSchema: HashedAssetSchema = { assetHash: hash(getAssetAccountDataSerializer().serialize(beforeAsset)), pluginHashes: [], }; - let hashedAsset = hash( + const hashedAsset = hash( getHashedAssetSchemaSerializer().serialize(hashedAssetSchema) ); t.deepEqual(afterAsset.hash, hashedAsset); @@ -74,6 +75,7 @@ test('it cannot compress an asset if not the owner', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/create.test.ts b/clients/js/test/create.test.ts index 552edce4..eacbc0e8 100644 --- a/clients/js/test/create.test.ts +++ b/clients/js/test/create.test.ts @@ -3,9 +3,11 @@ import test from 'ava'; // import { base58 } from '@metaplex-foundation/umi/serializers'; import { Asset, + AssetWithPlugins, DataState, create, fetchAsset, + fetchAssetWithPlugins, fetchHashedAsset, getAssetAccountDataSerializer, } from '../src'; @@ -22,6 +24,7 @@ test('it can create a new asset in account state', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [] }).sendAndConfirm(umi); // Then an account was created with the correct data. @@ -48,6 +51,7 @@ test('it can create a new asset in ledger state', async (t) => { name: 'Test Bread', uri: 'https://example.com/bread', logWrapper: publicKey('noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV'), + plugins: [] }).sendAndConfirm(umi); // Then an account was created with the correct data. @@ -72,3 +76,54 @@ test('it can create a new asset in ledger state', async (t) => { }); } }); + +test('it can create a new asset with plugins', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + plugins: [{ __kind: 'Freeze', fields: [{ frozen: false }] }] + }).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(119), + }, + pluginRegistry: { + key: 4, + registry: [ + { + pluginType: 2, + data: { + offset: BigInt(117), + authorities: [{ __kind: 'Owner' }], + }, + }, + ], + }, + plugins: [ + { + authorities: [{ __kind: 'Owner' }], + plugin: { + __kind: 'Freeze', + fields: [{ frozen: false }], + }, + }, + ], + }); +}); \ No newline at end of file diff --git a/clients/js/test/decompress.test.ts b/clients/js/test/decompress.test.ts index 694f8a86..83f66ae0 100644 --- a/clients/js/test/decompress.test.ts +++ b/clients/js/test/decompress.test.ts @@ -10,11 +10,11 @@ import { fetchHashedAsset, getAssetAccountDataSerializer, getHashedAssetSchemaSerializer, + hash, HashedAssetSchema, Key, } from '../src'; import { createUmi } from './_setup'; -import { hash } from '../src'; test('it can decompress a previously compressed asset as the owner', async (t) => { // Given a Umi instance and a new signer. @@ -27,11 +27,12 @@ test('it can decompress a previously compressed asset as the owner', async (t) = assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. const beforeAsset = await fetchAsset(umi, assetAddress.publicKey); - //console.log("Account State:", beforeAsset); + // console.log("Account State:", beforeAsset); t.like(beforeAsset, { publicKey: assetAddress.publicKey, updateAuthority: umi.identity.publicKey, @@ -53,12 +54,12 @@ test('it can decompress a previously compressed asset as the owner', async (t) = ); // And the hash matches the expected value. - let hashedAssetSchema: HashedAssetSchema = { + const hashedAssetSchema: HashedAssetSchema = { assetHash: hash(getAssetAccountDataSerializer().serialize(beforeAsset)), pluginHashes: [], }; - let hashedAsset = hash( + const hashedAsset = hash( getHashedAssetSchemaSerializer().serialize(hashedAssetSchema) ); t.deepEqual(afterCompressedAsset.hash, hashedAsset); diff --git a/clients/js/test/delegate.test.ts b/clients/js/test/delegate.test.ts index 944f78de..26338540 100644 --- a/clients/js/test/delegate.test.ts +++ b/clients/js/test/delegate.test.ts @@ -25,6 +25,7 @@ test('it can delegate a new authority', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await addPlugin(umi, { @@ -98,6 +99,7 @@ test('a delegate can freeze the token', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await addPlugin(umi, { diff --git a/clients/js/test/delegateTransfer.test.ts b/clients/js/test/delegateTransfer.test.ts index f4a1615f..4de8f1eb 100644 --- a/clients/js/test/delegateTransfer.test.ts +++ b/clients/js/test/delegateTransfer.test.ts @@ -25,6 +25,7 @@ test('a delegate can transfer the asset', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await addPlugin(umi, { diff --git a/clients/js/test/info.test.ts b/clients/js/test/info.test.ts index a1a222c5..bdea9c2c 100644 --- a/clients/js/test/info.test.ts +++ b/clients/js/test/info.test.ts @@ -14,6 +14,7 @@ test('fetch account info for account state', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Print the size of the account. @@ -41,6 +42,7 @@ test('fetch account info for ledger state', async (t) => { name: 'Test Bread', uri: 'https://example.com/bread', logWrapper: publicKey('noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV'), + plugins: [], }).sendAndConfirm(umi); // Print the size of the account. diff --git a/clients/js/test/removeAuthority.test.ts b/clients/js/test/removeAuthority.test.ts index d7092f82..6bc130d7 100644 --- a/clients/js/test/removeAuthority.test.ts +++ b/clients/js/test/removeAuthority.test.ts @@ -27,6 +27,7 @@ test('it can remove an authority from a plugin', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. @@ -156,6 +157,7 @@ test('it can remove the default authority from a plugin to make it immutable', a assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/removePlugin.test.ts b/clients/js/test/removePlugin.test.ts index c707ee5e..276f6c79 100644 --- a/clients/js/test/removePlugin.test.ts +++ b/clients/js/test/removePlugin.test.ts @@ -25,6 +25,7 @@ test('it can remove a plugin from an asset', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/transfer.test.ts b/clients/js/test/transfer.test.ts index 6d7ed984..3085f075 100644 --- a/clients/js/test/transfer.test.ts +++ b/clients/js/test/transfer.test.ts @@ -16,6 +16,7 @@ test('it can transfer an asset as the owner', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. @@ -59,6 +60,7 @@ test('it cannot transfer an asset if not the owner', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); // Then an account was created with the correct data. diff --git a/clients/js/test/update.test.ts b/clients/js/test/update.test.ts index b92cb89a..01e70cd4 100644 --- a/clients/js/test/update.test.ts +++ b/clients/js/test/update.test.ts @@ -23,6 +23,7 @@ test('it can update an asset to be larger', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await update(umi, { @@ -53,6 +54,7 @@ test('it can update an asset to be smaller', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await update(umi, { @@ -83,6 +85,7 @@ test('it can update an asset with plugins to be larger', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await addPlugin(umi, { @@ -147,6 +150,7 @@ test('it can update an asset with plugins to be smaller', async (t) => { assetAddress, name: 'Test Bread', uri: 'https://example.com/bread', + plugins: [], }).sendAndConfirm(umi); await addPlugin(umi, { diff --git a/clients/rust/src/generated/instructions/create.rs b/clients/rust/src/generated/instructions/create.rs index fb33bbdd..0b9d5bde 100644 --- a/clients/rust/src/generated/instructions/create.rs +++ b/clients/rust/src/generated/instructions/create.rs @@ -6,6 +6,7 @@ //! use crate::generated::types::DataState; +use crate::generated::types::Plugin; use borsh::BorshDeserialize; use borsh::BorshSerialize; @@ -124,6 +125,7 @@ pub struct CreateInstructionArgs { pub data_state: DataState, pub name: String, pub uri: String, + pub plugins: Vec, } /// Instruction builder. @@ -139,6 +141,7 @@ pub struct CreateBuilder { data_state: Option, name: Option, uri: Option, + plugins: Option>, __remaining_accounts: Vec, } @@ -214,6 +217,11 @@ impl CreateBuilder { 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( @@ -249,6 +257,7 @@ impl CreateBuilder { data_state: self.data_state.clone().expect("data_state is not set"), 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) @@ -466,6 +475,7 @@ impl<'a, 'b> CreateCpiBuilder<'a, 'b> { data_state: None, name: None, uri: None, + plugins: None, __remaining_accounts: Vec::new(), }); Self { instruction } @@ -549,6 +559,11 @@ impl<'a, 'b> CreateCpiBuilder<'a, 'b> { 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( @@ -598,6 +613,11 @@ impl<'a, 'b> CreateCpiBuilder<'a, 'b> { .expect("data_state is not set"), 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 = CreateCpi { __program: self.instruction.__program, @@ -642,6 +662,7 @@ struct CreateCpiBuilderInstruction<'a, 'b> { data_state: Option, name: Option, uri: Option, + plugins: Option>, /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. __remaining_accounts: Vec<( &'b solana_program::account_info::AccountInfo<'a>, diff --git a/idls/mpl_core_program.json b/idls/mpl_core_program.json index be705bf4..9e404cc1 100644 --- a/idls/mpl_core_program.json +++ b/idls/mpl_core_program.json @@ -1057,6 +1057,14 @@ { "name": "uri", "type": "string" + }, + { + "name": "plugins", + "type": { + "vec": { + "defined": "Plugin" + } + } } ] } diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index 26c329cb..644514bc 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -1,15 +1,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, -}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; use crate::{ error::MplCoreError, instruction::accounts::BurnAccounts, - plugins::{fetch_plugin, Plugin, PluginType}, - state::{Asset, Compressible, CompressionProof, DataBlob, Key, SolanaAccount}, - utils::{assert_authority, close_program_account, load_key}, + plugins::{CheckResult, Plugin, ValidationResult}, + state::{Asset, Compressible, CompressionProof, Key}, + utils::{close_program_account, fetch_core_data, load_key}, }; #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] @@ -43,33 +41,44 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnArgs) -> Progr asset.wrap()?; } Key::Asset => { - let asset = Asset::load(ctx.accounts.asset_address, 0)?; - - let mut authority_check: Result<(), ProgramError> = - Err(MplCoreError::InvalidAuthority.into()); - if asset.get_size() != ctx.accounts.asset_address.data_len() { - let (authorities, plugin, _) = - fetch_plugin(ctx.accounts.asset_address, PluginType::Freeze)?; + let (asset, _, plugin_registry) = fetch_core_data(ctx.accounts.asset_address)?; - authority_check = assert_authority(&asset, ctx.accounts.authority, &authorities); - - if let Plugin::Freeze(delegate) = plugin { - if delegate.frozen { - return Err(MplCoreError::AssetIsFrozen.into()); + let mut approved = false; + match Asset::check_transfer() { + CheckResult::CanApprove | CheckResult::CanReject => { + match asset.validate_burn(&ctx.accounts)? { + ValidationResult::Approved => { + approved = true; + } + ValidationResult::Rejected => { + return Err(MplCoreError::InvalidAuthority.into()) + } + ValidationResult::Pass => (), } } - } + CheckResult::None => (), + }; - match authority_check { - Ok(_) => Ok::<(), ProgramError>(()), - Err(_) => { - if ctx.accounts.authority.key != &asset.owner { - Err(MplCoreError::InvalidAuthority.into()) - } else { - Ok(()) + 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.data.offset)? + .validate_burn(&ctx.accounts, &args, &record.data.authorities)?; + if result == ValidationResult::Rejected { + return Err(MplCoreError::InvalidAuthority.into()); + } else if result == ValidationResult::Approved { + approved = true; + } } } - }?; + }; + + if !approved { + return Err(MplCoreError::InvalidAuthority.into()); + } } _ => return Err(MplCoreError::IncorrectAccount.into()), } diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index efabd0a7..09af1c44 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -8,6 +8,7 @@ use solana_program::{ use crate::{ error::MplCoreError, instruction::accounts::CreateAccounts, + plugins::{create_meta_idempotent, initialize_plugin, Plugin}, state::{Asset, Compressible, DataState, HashedAsset, Key}, }; @@ -17,6 +18,7 @@ pub struct CreateArgs { pub data_state: DataState, pub name: String, pub uri: String, + pub plugins: Vec, } pub(crate) fn create<'a>(accounts: &'a [AccountInfo<'a>], args: CreateArgs) -> ProgramResult { @@ -83,5 +85,24 @@ pub(crate) fn create<'a>(accounts: &'a [AccountInfo<'a>], args: CreateArgs) -> P serialized_data.len(), ); + //TODO: Do compressed state + if args.data_state == DataState::AccountState { + create_meta_idempotent( + ctx.accounts.asset_address, + ctx.accounts.payer, + ctx.accounts.system_program, + )?; + + for plugin in args.plugins { + initialize_plugin( + &plugin, + &plugin.default_authority()?, + ctx.accounts.asset_address, + ctx.accounts.payer, + ctx.accounts.system_program, + )?; + } + } + Ok(()) }