diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..0cc233c7 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + path_instructions: + [ + { + path: "programs/mpl-core/src/processor/*", + instructions: "Make sure the owner is checked on all accounts somehow", + }, + { + path: "programs/mpl-core/*", + instructions: "Carefully consider performance implications and make sure there are no performance inefficiencies", + }, + { + path: "**/*.rs", + instructions: "Make duplicated logic between the rust client and program are consistent", + }, + ] + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true diff --git a/clients/js/src/generated/types/index.ts b/clients/js/src/generated/types/index.ts index 86c9b374..8d6ac317 100644 --- a/clients/js/src/generated/types/index.ts +++ b/clients/js/src/generated/types/index.ts @@ -66,6 +66,7 @@ export * from './plugin'; export * from './pluginAuthorityPair'; export * from './pluginType'; export * from './registryRecord'; +export * from './transferCount'; export * from './transferDelegate'; export * from './updateDelegate'; export * from './validationResult'; diff --git a/clients/js/src/generated/types/plugin.ts b/clients/js/src/generated/types/plugin.ts index b5e2a9bb..a17f0204 100644 --- a/clients/js/src/generated/types/plugin.ts +++ b/clients/js/src/generated/types/plugin.ts @@ -39,6 +39,8 @@ import { PermanentFreezeDelegateArgs, PermanentTransferDelegate, PermanentTransferDelegateArgs, + TransferCount, + TransferCountArgs, TransferDelegate, TransferDelegateArgs, UpdateDelegate, @@ -57,6 +59,7 @@ import { getPermanentBurnDelegateSerializer, getPermanentFreezeDelegateSerializer, getPermanentTransferDelegateSerializer, + getTransferCountSerializer, getTransferDelegateSerializer, getUpdateDelegateSerializer, getVerifiedCreatorsSerializer, @@ -77,7 +80,8 @@ export type Plugin = | { __kind: 'AddBlocker'; fields: [AddBlocker] } | { __kind: 'ImmutableMetadata'; fields: [ImmutableMetadata] } | { __kind: 'VerifiedCreators'; fields: [VerifiedCreators] } - | { __kind: 'Autograph'; fields: [Autograph] }; + | { __kind: 'Autograph'; fields: [Autograph] } + | { __kind: 'TransferCount'; fields: [TransferCount] }; export type PluginArgs = | { __kind: 'Royalties'; fields: [BaseRoyaltiesArgs] } @@ -97,7 +101,8 @@ export type PluginArgs = | { __kind: 'AddBlocker'; fields: [AddBlockerArgs] } | { __kind: 'ImmutableMetadata'; fields: [ImmutableMetadataArgs] } | { __kind: 'VerifiedCreators'; fields: [VerifiedCreatorsArgs] } - | { __kind: 'Autograph'; fields: [AutographArgs] }; + | { __kind: 'Autograph'; fields: [AutographArgs] } + | { __kind: 'TransferCount'; fields: [TransferCountArgs] }; export function getPluginSerializer(): Serializer { return dataEnum( @@ -192,6 +197,12 @@ export function getPluginSerializer(): Serializer { ['fields', tuple([getAutographSerializer()])], ]), ], + [ + 'TransferCount', + struct>([ + ['fields', tuple([getTransferCountSerializer()])], + ]), + ], ], { description: 'Plugin' } ) as Serializer; @@ -261,6 +272,10 @@ export function plugin( kind: 'Autograph', data: GetDataEnumKindContent['fields'] ): GetDataEnumKind; +export function plugin( + kind: 'TransferCount', + data: GetDataEnumKindContent['fields'] +): GetDataEnumKind; export function plugin( kind: K, data?: any diff --git a/clients/js/src/generated/types/pluginType.ts b/clients/js/src/generated/types/pluginType.ts index a001175d..1cecd53c 100644 --- a/clients/js/src/generated/types/pluginType.ts +++ b/clients/js/src/generated/types/pluginType.ts @@ -24,6 +24,7 @@ export enum PluginType { ImmutableMetadata, VerifiedCreators, Autograph, + TransferCount, } export type PluginTypeArgs = PluginType; diff --git a/clients/js/src/generated/types/transferCount.ts b/clients/js/src/generated/types/transferCount.ts new file mode 100644 index 00000000..c354fad2 --- /dev/null +++ b/clients/js/src/generated/types/transferCount.ts @@ -0,0 +1,22 @@ +/** + * 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 { Serializer, struct, u64 } from '@metaplex-foundation/umi/serializers'; + +export type TransferCount = { count: bigint }; + +export type TransferCountArgs = { count: number | bigint }; + +export function getTransferCountSerializer(): Serializer< + TransferCountArgs, + TransferCount +> { + return struct([['count', u64()]], { + description: 'TransferCount', + }) as Serializer; +} diff --git a/clients/js/src/plugins/types.ts b/clients/js/src/plugins/types.ts index bc5d4a1f..143cf4b3 100644 --- a/clients/js/src/plugins/types.ts +++ b/clients/js/src/plugins/types.ts @@ -25,6 +25,8 @@ import { VerifiedCreatorsArgs, Autograph, VerifiedCreators, + TransferCount, + TransferCountArgs, } from '../generated'; import { RoyaltiesArgs, RoyaltiesPlugin } from './royalties'; import { PluginAuthority } from './pluginAuthority'; @@ -88,6 +90,9 @@ export type CreatePluginArgs = } | { type: 'AddBlocker'; + } + | { + type: 'TransferCount'; }; export type AuthorityArgsV2 = { @@ -143,7 +148,10 @@ export type AuthorityManagedPluginArgsV2 = } | ({ type: 'VerifiedCreators'; - } & VerifiedCreatorsArgs); + } & VerifiedCreatorsArgs) + | ({ + type: 'TransferCount'; + } & TransferCountArgs); export type AssetAddablePluginArgsV2 = | OwnerManagedPluginArgsV2 @@ -181,7 +189,7 @@ export type AddBlockerPlugin = BasePlugin & AddBlocker; export type ImmutableMetadataPlugin = BasePlugin & ImmutableMetadata; export type VerifiedCreatorsPlugin = BasePlugin & VerifiedCreators; export type AutographPlugin = BasePlugin & Autograph; - +export type TransferCountPlugin = BasePlugin & TransferCount; export type CommonPluginsList = { attributes?: AttributesPlugin; royalties?: RoyaltiesPlugin; @@ -193,6 +201,7 @@ export type CommonPluginsList = { immutableMetadata?: ImmutableMetadataPlugin; autograph?: AutographPlugin; verifiedCreators?: VerifiedCreatorsPlugin; + transferCount?: TransferCountPlugin; }; export type AssetPluginsList = { diff --git a/clients/js/test/plugins/asset/transferCount.test.ts b/clients/js/test/plugins/asset/transferCount.test.ts new file mode 100644 index 00000000..c186d7f1 --- /dev/null +++ b/clients/js/test/plugins/asset/transferCount.test.ts @@ -0,0 +1,40 @@ +import test from 'ava'; +import { generateSigner } from '@metaplex-foundation/umi'; +import { transferV1 } from '../../../src'; +import { assertAsset, createUmi } from '../../_setupRaw'; +import { createAsset } from '../../_setupSdk'; + +test('it can transfer an asset as the owner', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const newOwner = generateSigner(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'TransferCount', + count: 0, + }, + ], + }); + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + transferCount: { count: 0n, authority: { type: 'UpdateAuthority' } }, + }); + + const tx = await transferV1(umi, { + asset: asset.publicKey, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + console.log((await umi.rpc.getTransaction(tx.signature))?.meta?.logs); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: newOwner.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + transferCount: { count: 1n, authority: { type: 'UpdateAuthority' } }, + }); +}); diff --git a/clients/rust/src/generated/types/mod.rs b/clients/rust/src/generated/types/mod.rs index 755acf21..daadba9b 100644 --- a/clients/rust/src/generated/types/mod.rs +++ b/clients/rust/src/generated/types/mod.rs @@ -63,6 +63,7 @@ pub(crate) mod r#registry_record; pub(crate) mod r#royalties; pub(crate) mod r#rule_set; pub(crate) mod r#seed; +pub(crate) mod r#transfer_count; pub(crate) mod r#transfer_delegate; pub(crate) mod r#update_authority; pub(crate) mod r#update_delegate; @@ -129,6 +130,7 @@ pub use self::r#registry_record::*; pub use self::r#royalties::*; pub use self::r#rule_set::*; pub use self::r#seed::*; +pub use self::r#transfer_count::*; pub use self::r#transfer_delegate::*; pub use self::r#update_authority::*; pub use self::r#update_delegate::*; diff --git a/clients/rust/src/generated/types/plugin.rs b/clients/rust/src/generated/types/plugin.rs index 98a923a9..d88b2848 100644 --- a/clients/rust/src/generated/types/plugin.rs +++ b/clients/rust/src/generated/types/plugin.rs @@ -17,6 +17,7 @@ use crate::generated::types::PermanentBurnDelegate; use crate::generated::types::PermanentFreezeDelegate; use crate::generated::types::PermanentTransferDelegate; use crate::generated::types::Royalties; +use crate::generated::types::TransferCount; use crate::generated::types::TransferDelegate; use crate::generated::types::UpdateDelegate; use crate::generated::types::VerifiedCreators; @@ -45,4 +46,5 @@ pub enum Plugin { ImmutableMetadata(ImmutableMetadata), VerifiedCreators(VerifiedCreators), Autograph(Autograph), + TransferCount(TransferCount), } diff --git a/clients/rust/src/generated/types/plugin_type.rs b/clients/rust/src/generated/types/plugin_type.rs index 7eb36292..f11e777f 100644 --- a/clients/rust/src/generated/types/plugin_type.rs +++ b/clients/rust/src/generated/types/plugin_type.rs @@ -31,4 +31,5 @@ pub enum PluginType { ImmutableMetadata, VerifiedCreators, Autograph, + TransferCount, } diff --git a/clients/rust/src/generated/types/transfer_count.rs b/clients/rust/src/generated/types/transfer_count.rs new file mode 100644 index 00000000..9a88d2da --- /dev/null +++ b/clients/rust/src/generated/types/transfer_count.rs @@ -0,0 +1,19 @@ +//! 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] +//! + +#[cfg(feature = "anchor")] +use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransferCount { + pub count: u64, +} diff --git a/idls/mpl_core.json b/idls/mpl_core.json index bed7f27d..9f4d114a 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -2942,6 +2942,18 @@ ] } }, + { + "name": "TransferCount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "count", + "type": "u64" + } + ] + } + }, { "name": "UpdateDelegate", "type": { @@ -3984,6 +3996,14 @@ "defined": "Autograph" } ] + }, + { + "name": "TransferCount", + "fields": [ + { + "defined": "TransferCount" + } + ] } ] } @@ -4037,6 +4057,9 @@ }, { "name": "Autograph" + }, + { + "name": "TransferCount" } ] } diff --git a/programs/mpl-core/src/plugins/internal/authority_managed/mod.rs b/programs/mpl-core/src/plugins/internal/authority_managed/mod.rs index 55ecd552..1188351a 100644 --- a/programs/mpl-core/src/plugins/internal/authority_managed/mod.rs +++ b/programs/mpl-core/src/plugins/internal/authority_managed/mod.rs @@ -3,6 +3,7 @@ mod attributes; mod immutable_metadata; mod master_edition; mod royalties; +mod transfer_count; mod update_delegate; mod verified_creators; @@ -11,5 +12,6 @@ pub use attributes::*; pub use immutable_metadata::*; pub use master_edition::*; pub use royalties::*; +pub use transfer_count::*; pub use update_delegate::*; pub use verified_creators::*; diff --git a/programs/mpl-core/src/plugins/internal/authority_managed/transfer_count.rs b/programs/mpl-core/src/plugins/internal/authority_managed/transfer_count.rs new file mode 100644 index 00000000..c05690f2 --- /dev/null +++ b/programs/mpl-core/src/plugins/internal/authority_managed/transfer_count.rs @@ -0,0 +1,58 @@ +use crate::{ + plugins::{Plugin, PluginValidation}, + state::DataBlob, +}; +use borsh::{BorshDeserialize, BorshSerialize}; + +/// The TransferCount plugin tracks the number of times an asset has been transferred. +#[repr(C)] +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Default)] +pub struct TransferCount { + /// The number of times the asset has been transferred. + pub count: u64, +} + +impl TransferCount { + const BASE_LEN: usize = 8; // The size of u64 + + /// Initialize the TransferCount plugin with a count of 0. + pub fn new() -> Self { + Self::default() + } +} + +impl DataBlob for TransferCount { + fn len(&self) -> usize { + Self::BASE_LEN + } +} + +impl PluginValidation for TransferCount { + fn side_effects_transfer( + &self, + _ctx: &crate::plugins::PluginSideEffectContext, + ) -> Result { + Ok(Plugin::TransferCount(TransferCount { + count: self.count + 1, + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transfer_count_len() { + let transfer_count = TransferCount { count: 0 }; + let serialized = transfer_count.try_to_vec().unwrap(); + assert_eq!(serialized.len(), transfer_count.len()); + } + + #[test] + fn test_transfer_count_default_len() { + let transfer_count = TransferCount::new(); + let serialized = transfer_count.try_to_vec().unwrap(); + assert_eq!(serialized.len(), transfer_count.len()); + } +} diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index aa37c21a..2d79e329 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -1,6 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use modular_bitfield::{bitfield, specifiers::B29}; -use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use modular_bitfield::{bitfield, prelude::B3, specifiers::B29}; +use solana_program::{ + account_info::AccountInfo, program_error::ProgramError, program_memory::sol_memcpy, + pubkey::Pubkey, +}; use std::collections::BTreeMap; use crate::{ @@ -15,16 +18,27 @@ use crate::{ /// Lifecycle permissions /// Plugins use this field to indicate their permission to approve or deny /// a lifecycle action. +#[bitfield(bits = 8)] #[derive(Eq, PartialEq, Copy, Clone, Debug)] -pub enum CheckResult { +pub struct CheckResult { /// A plugin is permitted to approve a lifecycle action. - CanApprove, + pub can_approve: bool, /// A plugin is permitted to reject a lifecycle action. - CanReject, + pub can_reject: bool, /// A plugin is not permitted to approve or reject a lifecycle action. - None, + pub none: bool, /// Certain plugins can force approve a lifecycle action. - CanForceApprove, + pub can_force_approve: bool, + /// A plugin has side effects that should be run. + pub side_effect: bool, + /// Remaining bits are reserved for future use. + pub reserved: B3, +} + +impl Default for CheckResult { + fn default() -> Self { + Self::new() + } } /// Lifecycle permissions for adapter, third party plugins. @@ -82,16 +96,16 @@ impl PluginType { /// Check permissions for the add plugin lifecycle event. pub fn check_add_plugin(plugin_type: &PluginType) -> CheckResult { match plugin_type { - PluginType::AddBlocker => CheckResult::CanReject, - PluginType::Royalties => CheckResult::CanReject, - PluginType::UpdateDelegate => CheckResult::CanApprove, - PluginType::PermanentFreezeDelegate => CheckResult::CanReject, - PluginType::PermanentTransferDelegate => CheckResult::CanReject, - PluginType::PermanentBurnDelegate => CheckResult::CanReject, - PluginType::Edition => CheckResult::CanReject, - PluginType::Autograph => CheckResult::CanReject, - PluginType::VerifiedCreators => CheckResult::CanReject, - _ => CheckResult::None, + PluginType::AddBlocker => CheckResult::new().with_can_reject(true), + PluginType::Royalties => CheckResult::new().with_can_reject(true), + PluginType::UpdateDelegate => CheckResult::new().with_can_approve(true), + PluginType::PermanentFreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::PermanentTransferDelegate => CheckResult::new().with_can_reject(true), + PluginType::PermanentBurnDelegate => CheckResult::new().with_can_reject(true), + PluginType::Edition => CheckResult::new().with_can_reject(true), + PluginType::Autograph => CheckResult::new().with_can_reject(true), + PluginType::VerifiedCreators => CheckResult::new().with_can_reject(true), + _ => CheckResult::new().with_none(true), } } @@ -99,12 +113,12 @@ impl PluginType { pub fn check_remove_plugin(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - PluginType::UpdateDelegate => CheckResult::CanApprove, - PluginType::FreezeDelegate => CheckResult::CanReject, - PluginType::PermanentFreezeDelegate => CheckResult::CanReject, - PluginType::Edition => CheckResult::CanReject, + PluginType::UpdateDelegate => CheckResult::new().with_can_approve(true), + PluginType::FreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::PermanentFreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::Edition => CheckResult::new().with_can_reject(true), // We default to CanReject because Plugins with Authority::None cannot be removed. - _ => CheckResult::CanReject, + _ => CheckResult::new().with_can_reject(true), } } @@ -112,7 +126,7 @@ impl PluginType { pub fn check_update_plugin(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::CanApprove, + _ => CheckResult::new().with_can_approve(true), } } @@ -120,7 +134,7 @@ impl PluginType { pub fn check_approve_plugin_authority(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::CanApprove, + _ => CheckResult::new().with_can_approve(true), } } @@ -130,7 +144,7 @@ impl PluginType { match plugin_type { //TODO: This isn't very efficient because it requires every plugin to be deserialized // to check if it's the plugin whose authority is being revoked. - _ => CheckResult::CanApprove, + _ => CheckResult::new().with_can_approve(true), } } @@ -138,11 +152,11 @@ impl PluginType { pub fn check_create(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - PluginType::Royalties => CheckResult::CanReject, - PluginType::UpdateDelegate => CheckResult::CanApprove, - PluginType::Autograph => CheckResult::CanReject, - PluginType::VerifiedCreators => CheckResult::CanReject, - _ => CheckResult::None, + PluginType::Royalties => CheckResult::new().with_can_reject(true), + PluginType::UpdateDelegate => CheckResult::new().with_can_approve(true), + PluginType::Autograph => CheckResult::new().with_can_reject(true), + PluginType::VerifiedCreators => CheckResult::new().with_can_reject(true), + _ => CheckResult::new().with_none(true), } } @@ -150,32 +164,33 @@ impl PluginType { pub fn check_update(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - PluginType::ImmutableMetadata => CheckResult::CanReject, - PluginType::UpdateDelegate => CheckResult::CanApprove, - _ => CheckResult::None, + PluginType::ImmutableMetadata => CheckResult::new().with_can_reject(true), + PluginType::UpdateDelegate => CheckResult::new().with_can_approve(true), + _ => CheckResult::new().with_none(true), } } /// Check if a plugin is permitted to approve or deny a burn action. pub fn check_burn(plugin_type: &PluginType) -> CheckResult { match plugin_type { - PluginType::FreezeDelegate => CheckResult::CanReject, - PluginType::BurnDelegate => CheckResult::CanApprove, - PluginType::PermanentFreezeDelegate => CheckResult::CanReject, - PluginType::PermanentBurnDelegate => CheckResult::CanApprove, - _ => CheckResult::None, + PluginType::FreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::BurnDelegate => CheckResult::new().with_can_approve(true), + PluginType::PermanentFreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::PermanentBurnDelegate => CheckResult::new().with_can_approve(true), + _ => CheckResult::new().with_none(true), } } /// Check if a plugin is permitted to approve or deny a transfer action. pub fn check_transfer(plugin_type: &PluginType) -> CheckResult { match plugin_type { - PluginType::Royalties => CheckResult::CanReject, - PluginType::FreezeDelegate => CheckResult::CanReject, - PluginType::TransferDelegate => CheckResult::CanApprove, - PluginType::PermanentFreezeDelegate => CheckResult::CanReject, - PluginType::PermanentTransferDelegate => CheckResult::CanApprove, - _ => CheckResult::None, + PluginType::Royalties => CheckResult::new().with_can_reject(true), + PluginType::FreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::TransferDelegate => CheckResult::new().with_can_approve(true), + PluginType::PermanentFreezeDelegate => CheckResult::new().with_can_reject(true), + PluginType::PermanentTransferDelegate => CheckResult::new().with_can_approve(true), + PluginType::TransferCount => CheckResult::new().with_none(true).with_side_effect(true), + _ => CheckResult::new().with_none(true), } } @@ -183,7 +198,7 @@ impl PluginType { pub fn check_compress(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::None, + _ => CheckResult::new().with_none(true), } } @@ -191,7 +206,7 @@ impl PluginType { pub fn check_decompress(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::None, + _ => CheckResult::new().with_none(true), } } @@ -199,7 +214,7 @@ impl PluginType { pub fn check_execute(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::None, + _ => CheckResult::new().with_none(true), } } @@ -207,7 +222,7 @@ impl PluginType { pub fn check_add_external_plugin_adapter(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::None, + _ => CheckResult::new().with_none(true), } } @@ -215,7 +230,7 @@ impl PluginType { pub fn check_remove_external_plugin_adapter(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::None, + _ => CheckResult::new().with_none(true), } } @@ -223,7 +238,7 @@ impl PluginType { pub fn check_update_external_plugin_adapter(plugin_type: &PluginType) -> CheckResult { #[allow(clippy::match_single_binding)] match plugin_type { - _ => CheckResult::None, + _ => CheckResult::new().with_none(true), } } } @@ -420,6 +435,140 @@ impl Plugin { ) -> Result { plugin.inner().validate_remove_external_plugin_adapter(ctx) } + + /// Process side effects for the add plugin lifecycle action. + /// This gets called on all existing plugins when a new plugin is added. + pub(crate) fn side_effects_add_plugin( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_add_plugin(ctx) + } + + /// Process side effects for the remove plugin lifecycle action. + /// This gets called on all existing plugins when the target plugin is removed. + pub(crate) fn side_effects_remove_plugin( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_remove_plugin(ctx) + } + + /// Process side effects for the add plugin lifecycle action. + /// This gets called on all existing plugins when a new external plugin is added. + pub(crate) fn side_effects_add_external_plugin_adapter( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_add_external_plugin_adapter(ctx) + } + + /// Process side effects for the remove plugin lifecycle action. + /// This gets called on all existing plugins when a new external plugin is removed. + pub(crate) fn side_effects_remove_external_plugin_adapter( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin + .inner() + .side_effects_remove_external_plugin_adapter(ctx) + } + + /// Process side effects for the approve plugin authority lifecycle action. + pub(crate) fn side_effects_approve_plugin_authority( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_approve_plugin_authority(ctx) + } + + /// Process side effects for the revoke plugin authority lifecycle action. + pub(crate) fn side_effects_revoke_plugin_authority( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_revoke_plugin_authority(ctx) + } + + /// Process side effects for the create lifecycle action. + /// This ONLY gets called to side_effects the self plugin + pub(crate) fn side_effects_create( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_create(ctx) + } + + /// Process side effects for the update lifecycle action. + /// This gets called on all existing plugins when an asset or collection is updated. + pub(crate) fn side_effects_update( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_update(ctx) + } + + /// Process side effects for the update_plugin lifecycle action. + /// This gets called on all existing plugins when a plugin is updated. + pub(crate) fn side_effects_update_plugin( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_update_plugin(ctx) + } + + /// Process side effects for the burn lifecycle action. + /// This gets called on all existing plugins when an asset is burned. + pub(crate) fn side_effects_burn( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_burn(ctx) + } + + /// Process side effects for the transfer lifecycle action. + /// This gets called on all existing plugins when an asset is transferred. + pub(crate) fn side_effects_transfer( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_transfer(ctx) + } + + /// Process side effects for the compress lifecycle action. + pub(crate) fn side_effects_compress( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_compress(ctx) + } + + /// Process side effects for the decompress lifecycle action. + pub(crate) fn side_effects_decompress( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_decompress(ctx) + } + + /// Process side effects for the execute lifecycle action. + pub(crate) fn side_effects_execute( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin.inner().side_effects_execute(ctx) + } + + /// Process side effects for the update_plugin lifecycle action. + #[allow(unused)] + pub(crate) fn side_effects_update_external_plugin_adapter( + plugin: &Plugin, + ctx: &PluginSideEffectContext, + ) -> Result { + plugin + .inner() + .side_effects_update_external_plugin_adapter(ctx) + } } /// Lifecycle validations @@ -524,6 +673,10 @@ pub(crate) struct PluginValidationContext<'a, 'b> { pub target_external_plugin_authority: Option<&'b Authority>, } +#[allow(dead_code)] +/// The required context for a plugin validation. +pub(crate) struct PluginSideEffectContext {} + /// Plugin validation trait which is implemented by each plugin. pub(crate) trait PluginValidation { /// Validate the add plugin lifecycle action. @@ -654,6 +807,124 @@ pub(crate) trait PluginValidation { ) -> Result { abstain!() } + + /// Process side effects for the add plugin lifecycle action. + /// This gets called on all existing plugins when a new plugin is added. + fn side_effects_add_plugin( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the remove plugin lifecycle action. + /// This gets called on all existing plugins when the target plugin is removed. + fn side_effects_remove_plugin( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the add plugin lifecycle action. + /// This gets called on all existing plugins when a new external plugin is added. + fn side_effects_add_external_plugin_adapter( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the remove plugin lifecycle action. + /// This gets called on all existing plugins when a new external plugin is removed. + fn side_effects_remove_external_plugin_adapter( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the approve plugin authority lifecycle action. + fn side_effects_approve_plugin_authority( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the revoke plugin authority lifecycle action. + fn side_effects_revoke_plugin_authority( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the create lifecycle action. + /// This ONLY gets called to side_effects the self plugin + fn side_effects_create(&self, _ctx: &PluginSideEffectContext) -> Result { + unreachable!() + } + + /// Process side effects for the update lifecycle action. + /// This gets called on all existing plugins when an asset or collection is updated. + fn side_effects_update(&self, _ctx: &PluginSideEffectContext) -> Result { + unreachable!() + } + + /// Process side effects for the update_plugin lifecycle action. + /// This gets called on all existing plugins when a plugin is updated. + fn side_effects_update_plugin( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the burn lifecycle action. + /// This gets called on all existing plugins when an asset is burned. + fn side_effects_burn(&self, _ctx: &PluginSideEffectContext) -> Result { + unreachable!() + } + + /// Process side effects for the transfer lifecycle action. + /// This gets called on all existing plugins when an asset is transferred. + fn side_effects_transfer( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the compress lifecycle action. + fn side_effects_compress( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the decompress lifecycle action. + fn side_effects_decompress( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } + + /// Process side effects for the execute lifecycle action. + fn side_effects_execute(&self, _ctx: &PluginSideEffectContext) -> Result { + unreachable!() + } + + /// Process side effects for the update_plugin lifecycle action. + #[allow(unused)] + fn side_effects_update_external_plugin_adapter( + &self, + _ctx: &PluginSideEffectContext, + ) -> Result { + unreachable!() + } } /// This function iterates through all plugin checks passed in and performs the validation @@ -683,12 +954,7 @@ pub(crate) fn validate_plugin_checks<'a>( let mut approved = false; let mut rejected = false; for (check_key, check_result, registry_record) in checks.values() { - if *check_key == key - && matches!( - check_result, - CheckResult::CanApprove | CheckResult::CanReject - ) - { + if *check_key == key && (check_result.can_approve() || check_result.can_reject()) { let account = match key { Key::CollectionV1 => collection.ok_or(MplCoreError::InvalidCollection)?, Key::AssetV1 => asset.ok_or(MplCoreError::InvalidAsset)?, @@ -818,6 +1084,38 @@ pub(crate) fn validate_external_plugin_adapter_checks<'a>( } } +/// This function iterates through all plugin checks passed in and performs the validation +/// by deserializing and calling validate on the plugin. +/// The STRONGEST result is returned. +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +pub(crate) fn run_plugin_side_effects<'a>( + checks: &BTreeMap, + asset: Option<&'a AccountInfo<'a>>, + collection: Option<&'a AccountInfo<'a>>, + plugin_side_effect_fp: fn(&Plugin, &PluginSideEffectContext) -> Result, +) -> Result<(), ProgramError> { + for (check_key, check_result, registry_record) in checks.values() { + if check_result.side_effect() { + let account = match check_key { + Key::CollectionV1 => collection.ok_or(MplCoreError::InvalidCollection)?, + Key::AssetV1 => asset.ok_or(MplCoreError::InvalidAsset)?, + _ => unreachable!(), + }; + + let side_effect_ctx = PluginSideEffectContext {}; + + let new_plugin = plugin_side_effect_fp( + &Plugin::load(account, registry_record.offset)?, + &side_effect_ctx, + )?; + + new_plugin.save(account, registry_record.offset)?; + } + } + + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/programs/mpl-core/src/plugins/mod.rs b/programs/mpl-core/src/plugins/mod.rs index deda88f2..5009dfb9 100644 --- a/programs/mpl-core/src/plugins/mod.rs +++ b/programs/mpl-core/src/plugins/mod.rs @@ -60,6 +60,8 @@ pub enum Plugin { VerifiedCreators(VerifiedCreators), /// Autograph plugin allows anybody to add their signature to the asset with an optional message Autograph(Autograph), + /// TransferCount plugin tracks the number of times an asset has been transferred + TransferCount(TransferCount), } impl Plugin { @@ -103,6 +105,7 @@ impl Plugin { Plugin::ImmutableMetadata(inner) => inner, Plugin::VerifiedCreators(inner) => inner, Plugin::Autograph(inner) => inner, + Plugin::TransferCount(inner) => inner, } } } @@ -134,6 +137,7 @@ impl DataBlob for Plugin { Plugin::ImmutableMetadata(immutable_metadata) => immutable_metadata.len(), Plugin::VerifiedCreators(verified_creators) => verified_creators.len(), Plugin::Autograph(autograph) => autograph.len(), + Plugin::TransferCount(transfer_count) => transfer_count.len(), } } } @@ -186,6 +190,8 @@ pub enum PluginType { VerifiedCreators, /// Autograph plugin. Autograph, + /// TransferCount plugin. + TransferCount, } impl PluginType { @@ -224,6 +230,7 @@ impl From<&Plugin> for PluginType { Plugin::MasterEdition(_) => PluginType::MasterEdition, Plugin::VerifiedCreators(_) => PluginType::VerifiedCreators, Plugin::Autograph(_) => PluginType::Autograph, + Plugin::TransferCount(_) => PluginType::TransferCount, } } } @@ -247,6 +254,7 @@ impl PluginType { PluginType::MasterEdition => Authority::UpdateAuthority, PluginType::VerifiedCreators => Authority::UpdateAuthority, PluginType::Autograph => Authority::Owner, + PluginType::TransferCount => Authority::UpdateAuthority, } } } @@ -297,6 +305,7 @@ mod test { Plugin::ImmutableMetadata(ImmutableMetadata {}), Plugin::VerifiedCreators(VerifiedCreators { signatures: vec![] }), Plugin::Autograph(Autograph { signatures: vec![] }), + Plugin::TransferCount(TransferCount { count: 0 }), ]; assert_eq!( @@ -412,6 +421,7 @@ mod test { message: "test".to_string(), }], })], + vec![Plugin::TransferCount(TransferCount { count: 1 })], ]; assert_eq!( diff --git a/programs/mpl-core/src/processor/add_external_plugin_adapter.rs b/programs/mpl-core/src/processor/add_external_plugin_adapter.rs index 4d68474f..b3394628 100644 --- a/programs/mpl-core/src/processor/add_external_plugin_adapter.rs +++ b/programs/mpl-core/src/processor/add_external_plugin_adapter.rs @@ -119,6 +119,7 @@ pub(crate) fn add_external_plugin_adapter<'a>( AssetV1::validate_add_external_plugin_adapter, CollectionV1::validate_add_external_plugin_adapter, Plugin::validate_add_external_plugin_adapter, + Plugin::side_effects_add_external_plugin_adapter, None, None, )?; diff --git a/programs/mpl-core/src/processor/add_plugin.rs b/programs/mpl-core/src/processor/add_plugin.rs index 8587f2fe..b2c9eac4 100644 --- a/programs/mpl-core/src/processor/add_plugin.rs +++ b/programs/mpl-core/src/processor/add_plugin.rs @@ -94,6 +94,7 @@ pub(crate) fn add_plugin<'a>( AssetV1::validate_add_plugin, CollectionV1::validate_add_plugin, Plugin::validate_add_plugin, + Plugin::side_effects_add_plugin, None, None, )?; diff --git a/programs/mpl-core/src/processor/approve_plugin_authority.rs b/programs/mpl-core/src/processor/approve_plugin_authority.rs index 715c0de3..7044ab8e 100644 --- a/programs/mpl-core/src/processor/approve_plugin_authority.rs +++ b/programs/mpl-core/src/processor/approve_plugin_authority.rs @@ -68,6 +68,7 @@ pub(crate) fn approve_plugin_authority<'a>( AssetV1::validate_approve_plugin_authority, CollectionV1::validate_approve_plugin_authority, Plugin::validate_approve_plugin_authority, + Plugin::side_effects_approve_plugin_authority, None, None, )?; diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index fe61bde8..1674022e 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -100,6 +100,7 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnV1Args) -> Pro AssetV1::validate_burn, CollectionV1::validate_burn, Plugin::validate_burn, + Plugin::side_effects_burn, Some(ExternalPluginAdapter::validate_burn), Some(HookableLifecycleEvent::Burn), )?; diff --git a/programs/mpl-core/src/processor/compress.rs b/programs/mpl-core/src/processor/compress.rs index 5267ffc0..cfba1588 100644 --- a/programs/mpl-core/src/processor/compress.rs +++ b/programs/mpl-core/src/processor/compress.rs @@ -60,6 +60,7 @@ pub(crate) fn compress<'a>( AssetV1::validate_compress, CollectionV1::validate_compress, Plugin::validate_compress, + Plugin::side_effects_compress, None, None, )?; diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 42ce938f..9779b425 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -10,7 +10,7 @@ use crate::{ instruction::accounts::CreateV2Accounts, plugins::{ create_meta_idempotent, create_plugin_meta, initialize_external_plugin_adapter, - initialize_plugin, CheckResult, ExternalCheckResultBits, ExternalPluginAdapter, + initialize_plugin, ExternalCheckResultBits, ExternalPluginAdapter, ExternalPluginAdapterInitInfo, HookableLifecycleEvent, Plugin, PluginAuthorityPair, PluginType, PluginValidationContext, ValidationResult, }, @@ -165,6 +165,7 @@ pub(crate) fn process_create<'a>( AssetV1::validate_create, CollectionV1::validate_create, Plugin::validate_create, + Plugin::side_effects_create, Some(ExternalPluginAdapter::validate_create), Some(HookableLifecycleEvent::Create), )?; @@ -188,9 +189,7 @@ pub(crate) fn process_create<'a>( if plugin_type == PluginType::MasterEdition { return Err(MplCoreError::InvalidPlugin.into()); } - if PluginType::check_create(&PluginType::from(&plugin.plugin)) - != CheckResult::None - { + if !PluginType::check_create(&PluginType::from(&plugin.plugin)).none() { let validation_ctx = PluginValidationContext { accounts, asset_info: Some(ctx.accounts.asset), diff --git a/programs/mpl-core/src/processor/create_collection.rs b/programs/mpl-core/src/processor/create_collection.rs index c084452f..a62f18df 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -10,8 +10,8 @@ use crate::{ instruction::accounts::CreateCollectionV2Accounts, plugins::{ create_meta_idempotent, create_plugin_meta, initialize_external_plugin_adapter, - initialize_plugin, CheckResult, ExternalPluginAdapterInitInfo, Plugin, PluginAuthorityPair, - PluginType, PluginValidationContext, ValidationResult, + initialize_plugin, ExternalPluginAdapterInitInfo, Plugin, PluginAuthorityPair, PluginType, + PluginValidationContext, ValidationResult, }, state::{Authority, CollectionV1, Key}, }; @@ -132,7 +132,7 @@ pub(crate) fn process_create_collection<'a>( return Err(MplCoreError::InvalidPlugin.into()); } - if PluginType::check_create(&plugin_type) != CheckResult::None { + if !PluginType::check_create(&plugin_type).none() { let validation_ctx = PluginValidationContext { accounts, asset_info: None, diff --git a/programs/mpl-core/src/processor/decompress.rs b/programs/mpl-core/src/processor/decompress.rs index ad286b6a..8c2997da 100644 --- a/programs/mpl-core/src/processor/decompress.rs +++ b/programs/mpl-core/src/processor/decompress.rs @@ -76,6 +76,7 @@ pub(crate) fn decompress<'a>( AssetV1::validate_decompress, CollectionV1::validate_decompress, Plugin::validate_decompress, + Plugin::side_effects_decompress, None, None, )?; diff --git a/programs/mpl-core/src/processor/execute.rs b/programs/mpl-core/src/processor/execute.rs index f4923dfa..bb9fe29e 100644 --- a/programs/mpl-core/src/processor/execute.rs +++ b/programs/mpl-core/src/processor/execute.rs @@ -64,6 +64,7 @@ pub(crate) fn execute<'a>(accounts: &'a [AccountInfo<'a>], args: ExecuteV1Args) AssetV1::validate_execute, CollectionV1::validate_execute, Plugin::validate_execute, + Plugin::side_effects_execute, None, None, )?; diff --git a/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs b/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs index 4d1b7eed..73779f64 100644 --- a/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs +++ b/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs @@ -81,6 +81,7 @@ pub(crate) fn remove_external_plugin_adapter<'a>( AssetV1::validate_remove_external_plugin_adapter, CollectionV1::validate_remove_external_plugin_adapter, Plugin::validate_remove_external_plugin_adapter, + Plugin::side_effects_remove_external_plugin_adapter, None, None, )?; diff --git a/programs/mpl-core/src/processor/remove_plugin.rs b/programs/mpl-core/src/processor/remove_plugin.rs index 9ad623c6..b5b6294f 100644 --- a/programs/mpl-core/src/processor/remove_plugin.rs +++ b/programs/mpl-core/src/processor/remove_plugin.rs @@ -73,6 +73,7 @@ pub(crate) fn remove_plugin<'a>( AssetV1::validate_remove_plugin, CollectionV1::validate_remove_plugin, Plugin::validate_remove_plugin, + Plugin::side_effects_remove_plugin, None, None, )?; diff --git a/programs/mpl-core/src/processor/revoke_plugin_authority.rs b/programs/mpl-core/src/processor/revoke_plugin_authority.rs index a5e8bfc3..43b920c4 100644 --- a/programs/mpl-core/src/processor/revoke_plugin_authority.rs +++ b/programs/mpl-core/src/processor/revoke_plugin_authority.rs @@ -74,6 +74,7 @@ pub(crate) fn revoke_plugin_authority<'a>( AssetV1::validate_revoke_plugin_authority, CollectionV1::validate_revoke_plugin_authority, Plugin::validate_revoke_plugin_authority, + Plugin::side_effects_revoke_plugin_authority, None, None, )?; diff --git a/programs/mpl-core/src/processor/transfer.rs b/programs/mpl-core/src/processor/transfer.rs index 1f4e58bb..e76293b0 100644 --- a/programs/mpl-core/src/processor/transfer.rs +++ b/programs/mpl-core/src/processor/transfer.rs @@ -93,6 +93,7 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferV1Args AssetV1::validate_transfer, CollectionV1::validate_transfer, Plugin::validate_transfer, + Plugin::side_effects_transfer, Some(ExternalPluginAdapter::validate_transfer), Some(HookableLifecycleEvent::Transfer), )?; diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index 99728da6..0a55f051 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -124,6 +124,7 @@ fn update<'a>( AssetV1::validate_update, CollectionV1::validate_update, Plugin::validate_update, + Plugin::side_effects_update, Some(ExternalPluginAdapter::validate_update), Some(HookableLifecycleEvent::Update), )?; diff --git a/programs/mpl-core/src/processor/update_plugin.rs b/programs/mpl-core/src/processor/update_plugin.rs index 91bd7329..725a1dba 100644 --- a/programs/mpl-core/src/processor/update_plugin.rs +++ b/programs/mpl-core/src/processor/update_plugin.rs @@ -67,6 +67,7 @@ pub(crate) fn update_plugin<'a>( AssetV1::validate_update_plugin, CollectionV1::validate_update_plugin, Plugin::validate_update_plugin, + Plugin::side_effects_update_plugin, None, None, )?; diff --git a/programs/mpl-core/src/state/asset.rs b/programs/mpl-core/src/state/asset.rs index 1b89c984..b9511086 100644 --- a/programs/mpl-core/src/state/asset.rs +++ b/programs/mpl-core/src/state/asset.rs @@ -69,77 +69,77 @@ impl AssetV1 { /// Check permissions for the create lifecycle event. pub fn check_create() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the add plugin lifecycle event. pub fn check_add_plugin() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the remove plugin lifecycle event. pub fn check_remove_plugin() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the update plugin lifecycle event. pub fn check_update_plugin() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the approve plugin authority lifecycle event. pub fn check_approve_plugin_authority() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the revoke plugin authority lifecycle event. pub fn check_revoke_plugin_authority() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the transfer lifecycle event. pub fn check_transfer() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the burn lifecycle event. pub fn check_burn() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the update lifecycle event. pub fn check_update() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the compress lifecycle event. pub fn check_compress() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the decompress lifecycle event. pub fn check_decompress() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the add external plugin adapter lifecycle event. pub fn check_add_external_plugin_adapter() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the remove external plugin adapter lifecycle event. pub fn check_remove_external_plugin_adapter() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the update external plugin adapter lifecycle event. pub fn check_update_external_plugin_adapter() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the execute lifecycle event. pub fn check_execute() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Validate the create lifecycle event. diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index c0be3612..8dd677f8 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -55,77 +55,77 @@ impl CollectionV1 { /// Check permissions for the create lifecycle event. pub fn check_create() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the add plugin lifecycle event. pub fn check_add_plugin() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the remove plugin lifecycle event. pub fn check_remove_plugin() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the update plugin lifecycle event. pub fn check_update_plugin() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the approve plugin authority lifecycle event. pub fn check_approve_plugin_authority() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the revoke plugin authority lifecycle event. pub fn check_revoke_plugin_authority() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the transfer lifecycle event. pub fn check_transfer() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the burn lifecycle event. pub fn check_burn() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the update lifecycle event. pub fn check_update() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the compress lifecycle event. pub fn check_compress() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the decompress lifecycle event. pub fn check_decompress() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the add external plugin adapter lifecycle event. pub fn check_add_external_plugin_adapter() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the remove external plugin adapter lifecycle event. pub fn check_remove_external_plugin_adapter() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Check permissions for the update external plugin adapter lifecycle event. pub fn check_update_external_plugin_adapter() -> CheckResult { - CheckResult::None + CheckResult::new().with_none(true) } /// Check permissions for the execute lifecycle event. pub fn check_execute() -> CheckResult { - CheckResult::CanApprove + CheckResult::new().with_can_approve(true) } /// Validate the create lifecycle event. diff --git a/programs/mpl-core/src/utils/mod.rs b/programs/mpl-core/src/utils/mod.rs index 21577551..55a9f01f 100644 --- a/programs/mpl-core/src/utils/mod.rs +++ b/programs/mpl-core/src/utils/mod.rs @@ -7,10 +7,11 @@ pub(crate) use compression::*; use crate::{ error::MplCoreError, plugins::{ - validate_external_plugin_adapter_checks, validate_plugin_checks, CheckResult, - ExternalCheckResultBits, ExternalPluginAdapter, ExternalPluginAdapterKey, + run_plugin_side_effects, validate_external_plugin_adapter_checks, validate_plugin_checks, + CheckResult, ExternalCheckResultBits, ExternalPluginAdapter, ExternalPluginAdapterKey, ExternalRegistryRecord, HookableLifecycleEvent, Plugin, PluginHeaderV1, PluginRegistryV1, - PluginType, PluginValidationContext, RegistryRecord, ValidationResult, + PluginSideEffectContext, PluginType, PluginValidationContext, RegistryRecord, + ValidationResult, }, state::{ AssetV1, Authority, CollectionV1, CoreAsset, DataBlob, Key, SolanaAccount, UpdateAuthority, @@ -132,6 +133,7 @@ pub(crate) fn validate_asset_permissions<'a>( &Plugin, &PluginValidationContext, ) -> Result, + plugin_side_effect_fp: fn(&Plugin, &PluginSideEffectContext) -> Result, external_plugin_adapter_validate_fp: Option< fn( &ExternalPluginAdapter, @@ -172,7 +174,7 @@ pub(crate) fn validate_asset_permissions<'a>( let collection_check = if collection.is_some() { collection_check_fp() } else { - CheckResult::None + CheckResult::new().with_none(true) }; // Check the collection plugins first. @@ -210,7 +212,7 @@ pub(crate) fn validate_asset_permissions<'a>( // Do the core validation. let mut approved = false; let mut rejected = false; - if asset_check != CheckResult::None { + if !asset_check.none() { match asset_validate_fp( &deserialized_asset, authority_info, @@ -226,7 +228,7 @@ pub(crate) fn validate_asset_permissions<'a>( } }; - if collection_check != CheckResult::None { + if !collection_check.none() { match collection_validate_fp( &CollectionV1::load(collection.ok_or(MplCoreError::MissingCollection)?, 0)?, authority_info, @@ -292,6 +294,8 @@ pub(crate) fn validate_asset_permissions<'a>( } }; + run_plugin_side_effects(&checks, Some(asset), collection, plugin_side_effect_fp)?; + if let Some(external_plugin_adapter_validate_fp) = external_plugin_adapter_validate_fp { match validate_external_plugin_adapter_checks( Key::CollectionV1, @@ -423,13 +427,11 @@ pub(crate) fn validate_collection_permissions<'a>( // Do the core validation. let mut approved = false; let mut rejected = false; - if matches!( - core_check, - ( - Key::CollectionV1, - CheckResult::CanApprove | CheckResult::CanReject | CheckResult::CanForceApprove - ) - ) { + if core_check.0 == Key::CollectionV1 + && (core_check.1.can_approve() + || core_check.1.can_reject() + || core_check.1.can_force_approve()) + { let result = match core_check.0 { Key::CollectionV1 => collection_validate_fp( &deserialized_collection,