From 49b1b1092980211d1ad9b1d2a355fdbb9aa3d1e9 Mon Sep 17 00:00:00 2001
From: Nhan Phan <nhan.phan@gmail.com>
Date: Mon, 4 Mar 2024 16:21:54 -0800
Subject: [PATCH 1/2] add collect

---
 .../js/src/generated/instructions/collect.ts  |  88 ++++++
 .../js/src/generated/instructions/index.ts    |   1 +
 clients/js/test/collect.test.ts               |  99 +++++++
 .../src/generated/instructions/collect.rs     | 274 ++++++++++++++++++
 .../rust/src/generated/instructions/mod.rs    |   2 +
 idls/mpl_core.json                            |  18 ++
 programs/mpl-core/src/instruction.rs          |   5 +
 programs/mpl-core/src/plugins/utils.rs        |  13 +-
 programs/mpl-core/src/processor/collect.rs    |  90 ++++++
 programs/mpl-core/src/processor/compress.rs   |   5 +-
 programs/mpl-core/src/processor/create.rs     |   4 +-
 programs/mpl-core/src/processor/decompress.rs |   6 +-
 programs/mpl-core/src/processor/mod.rs        |   7 +
 programs/mpl-core/src/processor/update.rs     |   8 +-
 programs/mpl-core/src/state/collect.rs        |   5 +
 programs/mpl-core/src/state/mod.rs            |   3 +
 programs/mpl-core/src/utils.rs                |  39 ++-
 17 files changed, 645 insertions(+), 22 deletions(-)
 create mode 100644 clients/js/src/generated/instructions/collect.ts
 create mode 100644 clients/js/test/collect.test.ts
 create mode 100644 clients/rust/src/generated/instructions/collect.rs
 create mode 100644 programs/mpl-core/src/processor/collect.rs
 create mode 100644 programs/mpl-core/src/state/collect.rs

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<CollectInstructionDataArgs, any, CollectInstructionData>(
+    struct<CollectInstructionData>([['discriminator', u8()]], {
+      description: 'CollectInstructionData',
+    }),
+    (value) => ({ ...value, discriminator: 12 })
+  ) as Serializer<CollectInstructionDataArgs, CollectInstructionData>;
+}
+
+// Instruction.
+export function collect(
+  context: Pick<Context, 'programs'>,
+  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, <AssetWithPlugins>{
+    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<solana_program::pubkey::Pubkey>,
+    __remaining_accounts: Vec<solana_program::instruction::AccountMeta>,
+}
+
+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<CollectCpiBuilderInstruction<'a, 'b>>,
+}
+
+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::<Asset>(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::<Result<Vec<_>, 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(())
+}

From 716dacf7f8c775ec726592580d06cb7b89507509 Mon Sep 17 00:00:00 2001
From: Nhan Phan <nhan.phan@gmail.com>
Date: Mon, 4 Mar 2024 19:41:17 -0800
Subject: [PATCH 2/2] fix test, correctly burn

---
 clients/js/test/plugins/asset/burn.test.ts | 2 +-
 programs/mpl-core/src/utils.rs             | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/clients/js/test/plugins/asset/burn.test.ts b/clients/js/test/plugins/asset/burn.test.ts
index 90d13df2..49641e54 100644
--- a/clients/js/test/plugins/asset/burn.test.ts
+++ b/clients/js/test/plugins/asset/burn.test.ts
@@ -50,7 +50,7 @@ test('it can burn an asset as the owner', async (t) => {
   const afterAsset = await umi.rpc.getAccount(assetAddress.publicKey);
   t.true(afterAsset.exists);
   assertAccountExists(afterAsset);
-  t.deepEqual(afterAsset.lamports, sol(0.00089784));
+  t.deepEqual(afterAsset.lamports, sol(0.00089784 + 0.0015));
   t.is(afterAsset.data.length, 1);
   t.is(afterAsset.data[0], Key.Uninitialized);
 });
diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs
index 427beb05..b4ece5ec 100644
--- a/programs/mpl-core/src/utils.rs
+++ b/programs/mpl-core/src/utils.rs
@@ -163,7 +163,7 @@ pub(crate) fn close_program_account<'a>(
     **funds_dest_account_info.lamports.borrow_mut() = dest_starting_lamports
         .checked_add(amount_to_return)
         .ok_or(MplCoreError::NumericalOverflowError)?;
-    **account_to_close_info.lamports.borrow_mut() = one_byte_rent;
+    **account_to_close_info.try_borrow_mut_lamports()? -= amount_to_return;
 
     account_to_close_info.realloc(1, false)?;
     account_to_close_info.data.borrow_mut()[0] = Key::Uninitialized.to_u8().unwrap();