diff --git a/clients/js/src/generated/instructions/collect.ts b/clients/js/src/generated/instructions/collect.ts new file mode 100644 index 00000000..c23ccb55 --- /dev/null +++ b/clients/js/src/generated/instructions/collect.ts @@ -0,0 +1,88 @@ +/** + * 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, + TransactionBuilder, + transactionBuilder, +} from '@metaplex-foundation/umi'; +import { + Serializer, + mapSerializer, + struct, + u8, +} from '@metaplex-foundation/umi/serializers'; +import { + ResolvedAccount, + ResolvedAccountsWithIndices, + getAccountMetasAndSigners, +} from '../shared'; + +// Accounts. +export type CollectInstructionAccounts = { + /** The address of the recipient */ + recipient: PublicKey | Pda; +}; + +// Data. +export type CollectInstructionData = { discriminator: number }; + +export type CollectInstructionDataArgs = {}; + +export function getCollectInstructionDataSerializer(): Serializer< + CollectInstructionDataArgs, + CollectInstructionData +> { + return mapSerializer( + struct([['discriminator', u8()]], { + description: 'CollectInstructionData', + }), + (value) => ({ ...value, discriminator: 12 }) + ) as Serializer; +} + +// Instruction. +export function collect( + context: Pick, + input: CollectInstructionAccounts +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey( + 'mplCore', + 'CoREzp6dAdLVRKf3EM5tWrsXM2jQwRFeu5uhzsAyjYXL' + ); + + // Accounts. + const resolvedAccounts: ResolvedAccountsWithIndices = { + recipient: { index: 0, isWritable: true, value: input.recipient ?? null }, + }; + + // 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 = getCollectInstructionDataSerializer().serialize({}); + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0; + + return transactionBuilder([ + { instruction: { keys, programId, data }, signers, bytesCreatedOnChain }, + ]); +} diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 6a0cbbb8..acbc29b8 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -9,6 +9,7 @@ export * from './addAuthority'; export * from './addPlugin'; export * from './burn'; +export * from './collect'; export * from './compress'; export * from './create'; export * from './createCollection'; diff --git a/clients/js/test/collect.test.ts b/clients/js/test/collect.test.ts new file mode 100644 index 00000000..b350b5e3 --- /dev/null +++ b/clients/js/test/collect.test.ts @@ -0,0 +1,99 @@ +import { PublicKey, Umi, generateSigner, sol } from '@metaplex-foundation/umi'; +import test from 'ava'; + +import { + AssetWithPlugins, + DataState, + PluginType, + addPlugin, + create, + fetchAssetWithPlugins, + plugin, + removePlugin, + updateAuthority, +} from '../src'; +import { createUmi } from './_setup'; + +const hasCollectAmount = async (umi: Umi, address: PublicKey) => { + const account = await umi.rpc.getAccount(address); + if (account.exists) { + const rent = await umi.rpc.getRent(account.data.length) + const diff = account.lamports.basisPoints - rent.basisPoints + return diff === sol(0.0015).basisPoints + } + return false +} + +test('it can create a new asset with collect amount', 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: [] + }).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: updateAuthority('Address', [umi.identity.publicKey]), + owner: umi.identity.publicKey, + name: 'Test Bread', + uri: 'https://example.com/bread', + }); + + t.assert(await hasCollectAmount(umi, assetAddress.publicKey), 'Collect amount not found') +}); + +test('it can add asset plugin with collect amount', 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: [] + }).sendAndConfirm(umi); + + await addPlugin(umi, { + assetAddress: assetAddress.publicKey, + plugin: plugin('Freeze', [{ frozen: true }]) + }).sendAndConfirm(umi); + + t.assert(await hasCollectAmount(umi, assetAddress.publicKey), 'Collect amount not found') +}); + +test('it can add remove asset plugin with collect amount', 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: [ + plugin('Freeze', [{ frozen: false }]) + ] + }).sendAndConfirm(umi); + t.assert(await hasCollectAmount(umi, assetAddress.publicKey), 'Collect amount not found') + + await removePlugin(umi, { + assetAddress: assetAddress.publicKey, + pluginType: PluginType.Freeze, + }).sendAndConfirm(umi); + t.assert(await hasCollectAmount(umi, assetAddress.publicKey), 'Collect amount not found') +}); \ No newline at end of file diff --git a/clients/rust/src/generated/instructions/collect.rs b/clients/rust/src/generated/instructions/collect.rs new file mode 100644 index 00000000..0d11c135 --- /dev/null +++ b/clients/rust/src/generated/instructions/collect.rs @@ -0,0 +1,274 @@ +//! 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 borsh::BorshDeserialize; +use borsh::BorshSerialize; + +/// Accounts. +pub struct Collect { + /// The address of the recipient + pub recipient: solana_program::pubkey::Pubkey, +} + +impl Collect { + pub fn instruction(&self) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(&[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(1 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.recipient, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let data = CollectInstructionData::new().try_to_vec().unwrap(); + + solana_program::instruction::Instruction { + program_id: crate::MPL_CORE_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct CollectInstructionData { + discriminator: u8, +} + +impl CollectInstructionData { + fn new() -> Self { + Self { discriminator: 12 } + } +} + +/// Instruction builder. +#[derive(Default)] +pub struct CollectBuilder { + recipient: Option, + __remaining_accounts: Vec, +} + +impl CollectBuilder { + pub fn new() -> Self { + Self::default() + } + /// The address of the recipient + #[inline(always)] + pub fn recipient(&mut self, recipient: solana_program::pubkey::Pubkey) -> &mut Self { + self.recipient = Some(recipient); + 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 = Collect { + recipient: self.recipient.expect("recipient is not set"), + }; + + accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) + } +} + +/// `collect` CPI accounts. +pub struct CollectCpiAccounts<'a, 'b> { + /// The address of the recipient + pub recipient: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `collect` CPI instruction. +pub struct CollectCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + /// The address of the recipient + pub recipient: &'b solana_program::account_info::AccountInfo<'a>, +} + +impl<'a, 'b> CollectCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: CollectCpiAccounts<'a, 'b>, + ) -> Self { + Self { + __program: program, + recipient: accounts.recipient, + } + } + #[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(1 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.recipient.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 data = CollectInstructionData::new().try_to_vec().unwrap(); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::MPL_CORE_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(1 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.recipient.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) + } + } +} + +/// `collect` CPI instruction builder. +pub struct CollectCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> CollectCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(CollectCpiBuilderInstruction { + __program: program, + recipient: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + /// The address of the recipient + #[inline(always)] + pub fn recipient( + &mut self, + recipient: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.recipient = Some(recipient); + 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 instruction = CollectCpi { + __program: self.instruction.__program, + + recipient: self.instruction.recipient.expect("recipient is not set"), + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +struct CollectCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + recipient: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// 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/mod.rs b/clients/rust/src/generated/instructions/mod.rs index cb69ec97..6e284066 100644 --- a/clients/rust/src/generated/instructions/mod.rs +++ b/clients/rust/src/generated/instructions/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod add_authority; pub(crate) mod add_plugin; pub(crate) mod burn; +pub(crate) mod collect; pub(crate) mod compress; pub(crate) mod create; pub(crate) mod create_collection; @@ -21,6 +22,7 @@ pub(crate) mod update_plugin; pub use self::add_authority::*; pub use self::add_plugin::*; pub use self::burn::*; +pub use self::collect::*; pub use self::compress::*; pub use self::create::*; pub use self::create_collection::*; diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 94e7af54..da54e5f6 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -818,6 +818,24 @@ "type": "u8", "value": 11 } + }, + { + "name": "Collect", + "accounts": [ + { + "name": "recipient", + "isMut": true, + "isSigner": false, + "docs": [ + "The address of the recipient" + ] + } + ], + "args": [], + "discriminant": { + "type": "u8", + "value": 12 + } } ], "accounts": [ diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index 5cf046df..8c65d461 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -124,4 +124,9 @@ pub enum MplAssetInstruction { #[account(4, name="system_program", desc = "The system program")] #[account(5, optional, name="log_wrapper", desc = "The SPL Noop Program")] Decompress(DecompressArgs), + + /// Collect + /// This function creates the initial mpl-core + #[account(0, writable, name="recipient", desc = "The address of the recipient")] + Collect, } diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index a1a70ede..4290bf37 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -1,5 +1,4 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use mpl_utils::resize_or_reallocate_account_raw; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, program_memory::sol_memcpy, @@ -8,7 +7,7 @@ use solana_program::{ use crate::{ error::MplCoreError, state::{Asset, Authority, CollectionData, CoreAsset, DataBlob, Key, SolanaAccount}, - utils::{assert_authority, load_key, resolve_authority_to_default}, + utils::{assert_authority, load_key, resize_or_reallocate_account, resolve_authority_to_default}, }; use super::{Plugin, PluginHeader, PluginRegistry, PluginType, RegistryRecord}; @@ -52,7 +51,7 @@ pub fn create_meta_idempotent<'a>( external_plugins: vec![], }; - resize_or_reallocate_account_raw( + resize_or_reallocate_account( account, payer, system_program, @@ -214,7 +213,7 @@ pub fn initialize_plugin<'a>( .checked_add(size_increase) .ok_or(MplCoreError::NumericalOverflow)?; - resize_or_reallocate_account_raw(account, payer, system_program, new_size)?; + resize_or_reallocate_account(account, payer, system_program, new_size)?; header.save(account, header_offset)?; plugin.save(account, old_registry_offset)?; plugin_registry.save(account, new_registry_offset)?; @@ -290,7 +289,7 @@ pub fn delete_plugin<'a>( plugin_registry.save(account, new_offset)?; - resize_or_reallocate_account_raw(account, payer, system_program, new_size)?; + resize_or_reallocate_account(account, payer, system_program, new_size)?; } else { return Err(MplCoreError::PluginNotFound.into()); } @@ -328,7 +327,7 @@ pub fn add_authority_to_plugin<'a, T: CoreAsset>( .data_len() .checked_add(authority_bytes.len()) .ok_or(MplCoreError::NumericalOverflow)?; - resize_or_reallocate_account_raw(account, payer, system_program, new_size)?; + resize_or_reallocate_account(account, payer, system_program, new_size)?; plugin_registry.save(account, plugin_header.plugin_registry_offset)?; @@ -384,7 +383,7 @@ pub fn remove_authority_from_plugin<'a>( .data_len() .checked_sub(authority_bytes.len()) .ok_or(MplCoreError::NumericalOverflow)?; - resize_or_reallocate_account_raw(account, payer, system_program, new_size)?; + resize_or_reallocate_account(account, payer, system_program, new_size)?; plugin_registry.save(account, plugin_header.plugin_registry_offset)?; } diff --git a/programs/mpl-core/src/processor/collect.rs b/programs/mpl-core/src/processor/collect.rs new file mode 100644 index 00000000..c76f7bc0 --- /dev/null +++ b/programs/mpl-core/src/processor/collect.rs @@ -0,0 +1,90 @@ +use mpl_utils::assert_signer; +use solana_program::{account_info::next_account_info, rent::Rent, system_program, sysvar::Sysvar}; + +use super::*; +use crate::state::{DataBlob, COLLECT_RECIPIENT}; + +use crate::{ + error::MplCoreError, + instruction::accounts::CollectAccounts, + state::{Asset, HashedAsset, Key}, + utils::{fetch_core_data, load_key}, + ID, +}; + +pub(crate) fn collect<'a>(accounts: &'a [AccountInfo<'a>]) -> ProgramResult { + // Accounts. + let ctx = CollectAccounts::context(accounts)?; + + let account_info_iter = &mut accounts.iter(); + + let recipient_info = next_account_info(account_info_iter)?; + + assert_signer(recipient_info)?; + + if *ctx.accounts.recipient.key != COLLECT_RECIPIENT { + return Err(MplCoreError::IncorrectAccount.into()); + } + + for account_info in ctx.remaining_accounts { + if account_info.owner != &ID { + return Err(MplCoreError::IncorrectAccount.into()); + } + + collect_from_account(account_info, recipient_info)?; + } + + Ok(()) +} + +fn collect_from_account(account_info: &AccountInfo, dest_info: &AccountInfo) -> ProgramResult { + let rent = Rent::get()?; + + let (fee_amount, rent_amount) = match load_key(account_info, 0)? { + Key::Uninitialized => { + account_info.assign(&system_program::ID); + + (account_info.lamports(), 0) + } + Key::Asset => { + let (asset, header, registry) = fetch_core_data::(account_info)?; + let header_size = match header { + Some(header) => header.get_size(), + None => 0, + }; + + let registry_size = match registry { + Some(registry) => registry.get_size(), + None => 0, + }; + + // TODO overflow? + let asset_rent = rent.minimum_balance(asset.get_size() + header_size + registry_size); + let fee_amount = account_info + .lamports() + .checked_sub(asset_rent) + .ok_or(MplCoreError::NumericalOverflowError)?; + + (fee_amount, asset_rent) + } + Key::HashedAsset => { + // TODO use DataBlob trait instead? + let hashed_rent = rent.minimum_balance(HashedAsset::LENGTH); + let fee_amount = account_info + .lamports() + .checked_sub(hashed_rent) + .ok_or(MplCoreError::NumericalOverflowError)?; + + (fee_amount, hashed_rent) + } + _ => return Err(MplCoreError::IncorrectAccount.into()), + }; + + let dest_starting_lamports = dest_info.lamports(); + **dest_info.lamports.borrow_mut() = dest_starting_lamports + .checked_add(fee_amount) + .ok_or(MplCoreError::NumericalOverflowError)?; + **account_info.lamports.borrow_mut() = rent_amount; + + Ok(()) +} diff --git a/programs/mpl-core/src/processor/compress.rs b/programs/mpl-core/src/processor/compress.rs index b9dbc3ac..abd1c1fa 100644 --- a/programs/mpl-core/src/processor/compress.rs +++ b/programs/mpl-core/src/processor/compress.rs @@ -1,6 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; -use mpl_utils::resize_or_reallocate_account_raw; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, }; @@ -10,7 +9,7 @@ use crate::{ instruction::accounts::CompressAccounts, plugins::{CheckResult, Plugin, RegistryRecord, ValidationResult}, state::{Asset, Compressible, HashablePluginSchema, HashedAsset, HashedAssetSchema, Key}, - utils::{fetch_core_data, load_key}, + utils::{fetch_core_data, load_key, resize_or_reallocate_account}, }; #[repr(C)] @@ -103,7 +102,7 @@ pub(crate) fn compress<'a>(accounts: &'a [AccountInfo<'a>], args: CompressArgs) let hashed_asset = HashedAsset::new(hashed_asset_schema.hash()?); let serialized_data = hashed_asset.try_to_vec()?; - resize_or_reallocate_account_raw( + resize_or_reallocate_account( ctx.accounts.asset_address, payer, ctx.accounts.system_program, diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index bf215c65..64548fc8 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -9,7 +9,7 @@ use crate::{ error::MplCoreError, instruction::accounts::CreateAccounts, plugins::{create_meta_idempotent, initialize_plugin, CheckResult, Plugin, ValidationResult}, - state::{Asset, Compressible, DataState, HashedAsset, Key, UpdateAuthority}, + state::{Asset, Compressible, DataState, HashedAsset, Key, UpdateAuthority, COLLECT_AMOUNT}, utils::fetch_core_data, }; @@ -72,7 +72,7 @@ pub(crate) fn create<'a>(accounts: &'a [AccountInfo<'a>], args: CreateArgs) -> P } }; - let lamports = rent.minimum_balance(serialized_data.len()); + let lamports = rent.minimum_balance(serialized_data.len()) + COLLECT_AMOUNT; // CPI to the System Program. invoke( diff --git a/programs/mpl-core/src/processor/decompress.rs b/programs/mpl-core/src/processor/decompress.rs index b9e60dc7..0d1f7d9a 100644 --- a/programs/mpl-core/src/processor/decompress.rs +++ b/programs/mpl-core/src/processor/decompress.rs @@ -1,6 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; -use mpl_utils::resize_or_reallocate_account_raw; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, system_program, @@ -11,8 +10,7 @@ use crate::{ instruction::accounts::DecompressAccounts, plugins::{create_meta_idempotent, initialize_plugin, CheckResult, Plugin, ValidationResult}, state::{Asset, CompressionProof, Key}, - utils::fetch_core_data, - utils::{load_key, verify_proof}, + utils::{fetch_core_data, load_key, resize_or_reallocate_account, verify_proof}, }; #[repr(C)] @@ -47,7 +45,7 @@ pub(crate) fn decompress<'a>( verify_proof(ctx.accounts.asset_address, &args.compression_proof)?; let serialized_data = asset.try_to_vec()?; - resize_or_reallocate_account_raw( + resize_or_reallocate_account( ctx.accounts.asset_address, payer, ctx.accounts.system_program, diff --git a/programs/mpl-core/src/processor/mod.rs b/programs/mpl-core/src/processor/mod.rs index efbacb7f..f4315a76 100644 --- a/programs/mpl-core/src/processor/mod.rs +++ b/programs/mpl-core/src/processor/mod.rs @@ -38,6 +38,9 @@ pub(crate) use decompress::*; mod update_plugin; pub(crate) use update_plugin::*; +mod collect; +pub(crate) use collect::*; + /// Standard processor that deserializes and instruction and routes it to the appropriate handler. pub fn process_instruction<'a>( _program_id: &Pubkey, @@ -94,5 +97,9 @@ pub fn process_instruction<'a>( msg!("Instruction: Decompress"); decompress(accounts, args) } + + MplAssetInstruction::Collect => { + collect(accounts) + } } } diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index 3278827b..309f15cb 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use mpl_utils::{assert_signer, resize_or_reallocate_account_raw}; +use mpl_utils::assert_signer; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, }; @@ -9,7 +9,7 @@ use crate::{ instruction::accounts::UpdateAccounts, plugins::{CheckResult, Plugin, RegistryRecord, ValidationResult}, state::{Asset, DataBlob, SolanaAccount, UpdateAuthority}, - utils::fetch_core_data, + utils::{fetch_core_data, resize_or_reallocate_account}, }; #[repr(C)] @@ -111,7 +111,7 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateArgs) -> P [(plugin_offset as usize)..registry_offset] .to_vec(); - resize_or_reallocate_account_raw( + resize_or_reallocate_account( ctx.accounts.asset_address, payer, ctx.accounts.system_program, @@ -141,7 +141,7 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateArgs) -> P .collect::, MplCoreError>>()?; plugin_registry.save(ctx.accounts.asset_address, new_registry_offset as usize)?; } else { - resize_or_reallocate_account_raw( + resize_or_reallocate_account( ctx.accounts.asset_address, payer, ctx.accounts.system_program, diff --git a/programs/mpl-core/src/state/collect.rs b/programs/mpl-core/src/state/collect.rs new file mode 100644 index 00000000..a8156a58 --- /dev/null +++ b/programs/mpl-core/src/state/collect.rs @@ -0,0 +1,5 @@ +use solana_program::pubkey::Pubkey; + +pub(crate) const COLLECT_RECIPIENT: Pubkey = solana_program::pubkey!("Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7"); + +pub(crate) const COLLECT_AMOUNT: u64 = 1_500_000; diff --git a/programs/mpl-core/src/state/mod.rs b/programs/mpl-core/src/state/mod.rs index 37d68076..5785a4c8 100644 --- a/programs/mpl-core/src/state/mod.rs +++ b/programs/mpl-core/src/state/mod.rs @@ -13,6 +13,9 @@ pub use traits::*; mod collection; pub use collection::*; +mod collect; +pub use collect::*; + mod update_authority; pub use update_authority::*; diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs index 41f262fe..427beb05 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -1,7 +1,7 @@ use num_traits::{FromPrimitive, ToPrimitive}; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, rent::Rent, - sysvar::Sysvar, + account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, + program_error::ProgramError, rent::Rent, system_instruction, sysvar::Sysvar, }; use crate::{ @@ -170,3 +170,38 @@ pub(crate) fn close_program_account<'a>( Ok(()) } + +/// Resize an account using realloc and retain any lamport overages, modified from Solana Cookbook +pub(crate) fn resize_or_reallocate_account<'a>( + target_account: &AccountInfo<'a>, + funding_account: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + new_size: usize, +) -> ProgramResult { + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(new_size); + let current_minimum_balance = rent.minimum_balance(target_account.data_len()); + let account_infos = &[ + funding_account.clone(), + target_account.clone(), + system_program.clone(), + ]; + + if new_minimum_balance >= current_minimum_balance { + let lamports_diff = new_minimum_balance.saturating_sub(current_minimum_balance); + invoke( + &system_instruction::transfer(funding_account.key, target_account.key, lamports_diff), + account_infos + )?; + } else { + // return lamports to the compressor + let lamports_diff = current_minimum_balance.saturating_sub(new_minimum_balance); + + **funding_account.try_borrow_mut_lamports()? += lamports_diff; + **target_account.try_borrow_mut_lamports()? -= lamports_diff + } + + target_account.realloc(new_size, false)?; + + Ok(()) +}