diff --git a/clients/js/test/collect.test.ts b/clients/js/test/collect.test.ts index d1d89679..a2ac44a6 100644 --- a/clients/js/test/collect.test.ts +++ b/clients/js/test/collect.test.ts @@ -8,17 +8,23 @@ import { } from '@metaplex-foundation/umi'; import test from 'ava'; +import { fixedAccountInit } from '@metaplex-foundation/mpl-core-oracle-example'; import { + CheckResult, + ExternalPluginAdapterSchema, + ExternalValidationResult, PluginType, + addPlugin, addPluginV1, burnV1, collect, createPlugin, - pluginAuthorityPair, + removePlugin, removePluginV1, transfer, } from '../src'; -import { assertAsset, createAsset, createUmi } from './_setupRaw'; +import { assertAsset, createUmi } from './_setupRaw'; +import { createAsset } from './_setupSdk'; const recipient1 = publicKey('8AT6o8Qk5T9QnZvPThMrF9bcCQLTGkyGvVZZzHgCw11v'); const recipient2 = publicKey('MmHsqX4LxTfifxoH8BVRLUKrwDn1LPCac6YcCZTHhwt'); @@ -39,6 +45,15 @@ const hasCollectAmount = async (umi: Umi, address: PublicKey) => { return false; }; +const assertNoExcessRent = async (umi: Umi, address: PublicKey) => { + const account = await umi.rpc.getAccount(address); + if (account.exists) { + const rent = await umi.rpc.getRent(account.data.length); + return account.lamports.basisPoints === rent.basisPoints; + } + return false; +}; + test('it can create a new asset with collect amount', async (t) => { const umi = await createUmi(); const asset = await createAsset(umi); @@ -65,14 +80,33 @@ test('it can add asset plugin with collect amount', async (t) => { ); }); -test('it can add remove asset plugin with collect amount', async (t) => { +test('it can add asset external plugin with collect amount', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + + await addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'AppData', + dataAuthority: { type: 'UpdateAuthority' }, + schema: ExternalPluginAdapterSchema.Json, + }, + }).sendAndConfirm(umi); + + t.assert( + await hasCollectAmount(umi, asset.publicKey), + 'Collect amount not found' + ); +}); + +test('it can remove asset plugin with collect amount', async (t) => { const umi = await createUmi(); const asset = await createAsset(umi, { plugins: [ - pluginAuthorityPair({ + { type: 'FreezeDelegate', - data: { frozen: false }, - }), + frozen: false, + }, ], }); @@ -91,6 +125,33 @@ test('it can add remove asset plugin with collect amount', async (t) => { ); }); +test('it can remove asset external plugin with collect amount', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi, { + plugins: [ + { + type: 'AppData', + dataAuthority: { type: 'UpdateAuthority' }, + schema: ExternalPluginAdapterSchema.Json, + }, + ], + }); + + t.assert( + await hasCollectAmount(umi, asset.publicKey), + 'Collect amount not found' + ); + + await removePlugin(umi, { + asset: asset.publicKey, + plugin: { type: 'AppData', dataAuthority: { type: 'UpdateAuthority' } }, + }).sendAndConfirm(umi); + t.assert( + await hasCollectAmount(umi, asset.publicKey), + 'Collect amount not found' + ); +}); + test.serial('it can collect', async (t) => { const umi = await createUmi(); const asset = await createAsset(umi); @@ -110,6 +171,236 @@ test.serial('it can collect', async (t) => { t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); }); +test.serial('it can collect from an asset with plugins', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi, { + plugins: [ + { + type: 'FreezeDelegate', + frozen: false, + }, + { + type: 'Attributes', + attributeList: [ + { + key: 'Test', + value: 'Test', + }, + ], + }, + ], + }); + const balStart1 = await umi.rpc.getBalance(recipient1); + const balStart2 = await umi.rpc.getBalance(recipient2); + await collect(umi, {}) + .addRemainingAccounts({ + isSigner: false, + isWritable: true, + pubkey: asset.publicKey, + }) + .sendAndConfirm(umi); + const balEnd1 = await umi.rpc.getBalance(recipient1); + const balEnd2 = await umi.rpc.getBalance(recipient2); + t.is(await hasCollectAmount(umi, asset.publicKey), false); + t.deepEqual(subtractAmounts(balEnd1, balStart1), sol(0.0015 / 2)); + t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); + + t.assert(await assertNoExcessRent(umi, asset.publicKey), 'Excess rent found'); +}); + +test.serial('it can collect from an asset with external plugins', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'FreezeDelegate', + frozen: false, + }, + { + type: 'Attributes', + attributeList: [ + { + key: 'Test', + value: 'Test', + }, + ], + }, + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: account.publicKey, + }, + ], + }); + const balStart1 = await umi.rpc.getBalance(recipient1); + const balStart2 = await umi.rpc.getBalance(recipient2); + await collect(umi, {}) + .addRemainingAccounts({ + isSigner: false, + isWritable: true, + pubkey: asset.publicKey, + }) + .sendAndConfirm(umi); + const balEnd1 = await umi.rpc.getBalance(recipient1); + const balEnd2 = await umi.rpc.getBalance(recipient2); + t.is(await hasCollectAmount(umi, asset.publicKey), false); + t.deepEqual(subtractAmounts(balEnd1, balStart1), sol(0.0015 / 2)); + t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); + + t.assert(await assertNoExcessRent(umi, asset.publicKey), 'Excess rent found'); +}); + +test.serial( + 'it can collect from an asset with plugins that was burned', + async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi, { + plugins: [ + { + type: 'FreezeDelegate', + frozen: false, + }, + { + type: 'Attributes', + + attributeList: [ + { + key: 'Test', + value: 'Test', + }, + ], + }, + ], + }); + const balStart1 = await umi.rpc.getBalance(recipient1); + const balStart2 = await umi.rpc.getBalance(recipient2); + + await burnV1(umi, { + asset: asset.publicKey, + }).sendAndConfirm(umi); + + await collect(umi, {}) + .addRemainingAccounts({ + isSigner: false, + isWritable: true, + pubkey: asset.publicKey, + }) + .sendAndConfirm(umi); + const balEnd1 = await umi.rpc.getBalance(recipient1); + const balEnd2 = await umi.rpc.getBalance(recipient2); + t.is(await hasCollectAmount(umi, asset.publicKey), false); + t.deepEqual(subtractAmounts(balEnd1, balStart1), sol(0.0015 / 2)); + t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); + + t.assert( + await assertNoExcessRent(umi, asset.publicKey), + 'Excess rent found' + ); + } +); + +test.serial( + 'it can collect from an asset with external plugins that was burned', + async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'FreezeDelegate', + frozen: false, + }, + { + type: 'Attributes', + + attributeList: [ + { + key: 'Test', + value: 'Test', + }, + ], + }, + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: account.publicKey, + }, + ], + }); + const balStart1 = await umi.rpc.getBalance(recipient1); + const balStart2 = await umi.rpc.getBalance(recipient2); + + await burnV1(umi, { + asset: asset.publicKey, + }).sendAndConfirm(umi); + + await collect(umi, {}) + .addRemainingAccounts({ + isSigner: false, + isWritable: true, + pubkey: asset.publicKey, + }) + .sendAndConfirm(umi); + const balEnd1 = await umi.rpc.getBalance(recipient1); + const balEnd2 = await umi.rpc.getBalance(recipient2); + t.is(await hasCollectAmount(umi, asset.publicKey), false); + t.deepEqual(subtractAmounts(balEnd1, balStart1), sol(0.0015 / 2)); + t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); + + t.assert( + await assertNoExcessRent(umi, asset.publicKey), + 'Excess rent found' + ); + } +); + test.serial('it can collect burned asset', async (t) => { const umi = await createUmi(); const asset = await createAsset(umi); @@ -132,6 +423,8 @@ test.serial('it can collect burned asset', async (t) => { t.is(await hasCollectAmount(umi, asset.publicKey), false); t.deepEqual(subtractAmounts(balEnd1, balStart1), sol(0.0015 / 2)); t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); + + t.assert(await assertNoExcessRent(umi, asset.publicKey), 'Excess rent found'); }); test.serial( @@ -165,6 +458,11 @@ test.serial( t.is(await hasCollectAmount(umi, asset.publicKey), false); t.deepEqual(subtractAmounts(balEnd1, balStart1), sol(0.0015 / 2)); t.deepEqual(subtractAmounts(balEnd2, balStart2), sol(0.0015 / 2)); + + t.assert( + await assertNoExcessRent(umi, asset.publicKey), + 'Excess rent found' + ); } ); @@ -201,6 +499,8 @@ test.serial('it can collect multiple assets at once', async (t) => { t.is(await hasCollectAmount(umi, asset3.publicKey), false); t.deepEqual(subtractAmounts(balEnd1, balStart1), sol((0.0015 / 2) * 3)); t.deepEqual(subtractAmounts(balEnd2, balStart2), sol((0.0015 / 2) * 3)); + + t.assert(await assertNoExcessRent(umi, asset.publicKey), 'Excess rent found'); }); test('it can transfer after collecting', async (t) => { @@ -226,4 +526,6 @@ test('it can transfer after collecting', async (t) => { owner: newOwner.publicKey, updateAuthority: { type: 'Address', address: umi.identity.publicKey }, }); + + t.assert(await assertNoExcessRent(umi, asset.publicKey), 'Excess rent found'); }); diff --git a/programs/mpl-core/src/processor/collect.rs b/programs/mpl-core/src/processor/collect.rs index 4b59f008..3099bb32 100644 --- a/programs/mpl-core/src/processor/collect.rs +++ b/programs/mpl-core/src/processor/collect.rs @@ -1,14 +1,10 @@ use solana_program::{rent::Rent, system_program, sysvar::Sysvar}; use super::*; -use crate::state::{DataBlob, COLLECT_RECIPIENT1, COLLECT_RECIPIENT2}; +use crate::state::{COLLECT_RECIPIENT1, COLLECT_RECIPIENT2}; use crate::{ - error::MplCoreError, - instruction::accounts::CollectAccounts, - state::{AssetV1, HashedAssetV1, Key}, - utils::{fetch_core_data, load_key}, - ID, + error::MplCoreError, instruction::accounts::CollectAccounts, state::Key, utils::load_key, ID, }; pub(crate) fn collect<'a>(accounts: &'a [AccountInfo<'a>]) -> ProgramResult { @@ -55,26 +51,8 @@ fn collect_from_account( .ok_or(MplCoreError::NumericalOverflowError)?; (fee_amount, uninitialized_rent) } - Key::AssetV1 => { - 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, - }; - - let asset_rent = rent.minimum_balance( - asset - .get_size() - .checked_add(header_size) - .ok_or(MplCoreError::NumericalOverflowError)? - .checked_add(registry_size) - .ok_or(MplCoreError::NumericalOverflowError)?, - ); + Key::AssetV1 | Key::HashedAssetV1 => { + let asset_rent = rent.minimum_balance(account_info.data_len()); let fee_amount = account_info .lamports() .checked_sub(asset_rent) @@ -82,16 +60,6 @@ fn collect_from_account( (fee_amount, asset_rent) } - Key::HashedAssetV1 => { - // TODO use DataBlob trait instead? - let hashed_rent = rent.minimum_balance(HashedAssetV1::LENGTH); - let fee_amount = account_info - .lamports() - .checked_sub(hashed_rent) - .ok_or(MplCoreError::NumericalOverflowError)?; - - (fee_amount, hashed_rent) - } _ => return Err(MplCoreError::IncorrectAccount.into()), };