From 86d07b336817134d4c9c07240af10bdc118d2b54 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:19:07 -0700 Subject: [PATCH 01/28] Update ExternalRegistryRecordSafe and fix tests --- clients/rust/src/hooked/advanced_types.rs | 6 ++++++ clients/rust/tests/add_external_plugins.rs | 7 ++----- clients/rust/tests/create_with_external_plugins.rs | 7 ++----- clients/rust/tests/remove_external_plugins.rs | 7 +++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/clients/rust/src/hooked/advanced_types.rs b/clients/rust/src/hooked/advanced_types.rs index 12ba0d53..54b942f1 100644 --- a/clients/rust/src/hooked/advanced_types.rs +++ b/clients/rust/src/hooked/advanced_types.rs @@ -184,6 +184,8 @@ pub struct ExternalRegistryRecordSafe { pub authority: PluginAuthority, pub lifecycle_checks: Option>, pub offset: u64, + pub data_offset: Option, + pub data_len: Option, } impl ExternalRegistryRecordSafe { @@ -235,12 +237,16 @@ impl PluginRegistryV1Safe { let lifecycle_checks = Option::>::deserialize(&mut data)?; let offset = u64::deserialize(&mut data)?; + let data_offset = Option::::deserialize(&mut data)?; + let data_len = Option::::deserialize(&mut data)?; external_registry.push(ExternalRegistryRecordSafe { plugin_type, authority, lifecycle_checks, offset, + data_offset, + data_len, }); } diff --git a/clients/rust/tests/add_external_plugins.rs b/clients/rust/tests/add_external_plugins.rs index 813c145d..9cb5a133 100644 --- a/clients/rust/tests/add_external_plugins.rs +++ b/clients/rust/tests/add_external_plugins.rs @@ -5,7 +5,7 @@ use mpl_core::{ types::{ DataStore, DataStoreInitInfo, ExternalCheckResult, ExternalPlugin, ExternalPluginInitInfo, ExternalPluginSchema, HookableLifecycleEvent, LifecycleHook, LifecycleHookInitInfo, Oracle, - OracleInitInfo, PluginAuthority, UpdateAuthority, + OracleInitInfo, PluginAuthority, UpdateAuthority, ValidationResultsOffset, }, }; pub use setup::*; @@ -95,8 +95,6 @@ async fn test_add_lifecycle_hook() { extra_accounts: None, data_authority: Some(PluginAuthority::UpdateAuthority), schema: ExternalPluginSchema::Binary, - data_offset: 119, - data_len: 0, })], }, ) @@ -179,6 +177,7 @@ async fn test_add_oracle() { external_plugins: vec![ExternalPlugin::Oracle(Oracle { base_address: Pubkey::default(), pda: None, + results_offset: ValidationResultsOffset::NoOffset, })], }, ) @@ -256,8 +255,6 @@ async fn test_add_data_store() { external_plugins: vec![ExternalPlugin::DataStore(DataStore { data_authority: PluginAuthority::UpdateAuthority, schema: ExternalPluginSchema::Binary, - data_offset: 119, - data_len: 0, })], }, ) diff --git a/clients/rust/tests/create_with_external_plugins.rs b/clients/rust/tests/create_with_external_plugins.rs index 4d24b353..d3cbf194 100644 --- a/clients/rust/tests/create_with_external_plugins.rs +++ b/clients/rust/tests/create_with_external_plugins.rs @@ -3,7 +3,7 @@ pub mod setup; use mpl_core::types::{ DataStore, DataStoreInitInfo, ExternalCheckResult, ExternalPlugin, ExternalPluginInitInfo, ExternalPluginSchema, HookableLifecycleEvent, LifecycleHook, LifecycleHookInitInfo, Oracle, - OracleInitInfo, PluginAuthority, UpdateAuthority, + OracleInitInfo, PluginAuthority, UpdateAuthority, ValidationResultsOffset, }; pub use setup::*; @@ -63,8 +63,6 @@ async fn test_create_lifecycle_hook() { extra_accounts: None, data_authority: Some(PluginAuthority::UpdateAuthority), schema: ExternalPluginSchema::Binary, - data_offset: 119, - data_len: 0, })], }, ) @@ -118,6 +116,7 @@ async fn test_create_oracle() { external_plugins: vec![ExternalPlugin::Oracle(Oracle { base_address: Pubkey::default(), pda: None, + results_offset: ValidationResultsOffset::NoOffset, })], }, ) @@ -166,8 +165,6 @@ async fn test_create_data_store() { external_plugins: vec![ExternalPlugin::DataStore(DataStore { data_authority: PluginAuthority::UpdateAuthority, schema: ExternalPluginSchema::Binary, - data_offset: 119, - data_len: 0, })], }, ) diff --git a/clients/rust/tests/remove_external_plugins.rs b/clients/rust/tests/remove_external_plugins.rs index ec696118..cdd9412e 100644 --- a/clients/rust/tests/remove_external_plugins.rs +++ b/clients/rust/tests/remove_external_plugins.rs @@ -6,6 +6,7 @@ use mpl_core::{ DataStore, DataStoreInitInfo, ExternalCheckResult, ExternalPlugin, ExternalPluginInitInfo, ExternalPluginKey, ExternalPluginSchema, HookableLifecycleEvent, LifecycleHook, LifecycleHookInitInfo, Oracle, OracleInitInfo, PluginAuthority, UpdateAuthority, + ValidationResultsOffset, }, }; pub use setup::*; @@ -66,8 +67,6 @@ async fn test_remove_lifecycle_hook() { extra_accounts: None, data_authority: Some(PluginAuthority::UpdateAuthority), schema: ExternalPluginSchema::Binary, - data_offset: 119, - data_len: 0, })], }, ) @@ -131,6 +130,7 @@ async fn test_remove_oracle() { ExternalCheckResult { flags: 1 }, )]), pda: None, + results_offset: None, })], }, ) @@ -151,6 +151,7 @@ async fn test_remove_oracle() { external_plugins: vec![ExternalPlugin::Oracle(Oracle { base_address: Pubkey::default(), pda: None, + results_offset: ValidationResultsOffset::NoOffset, })], }, ) @@ -228,8 +229,6 @@ async fn test_remove_data_store() { external_plugins: vec![ExternalPlugin::DataStore(DataStore { data_authority: PluginAuthority::UpdateAuthority, schema: ExternalPluginSchema::Binary, - data_offset: 119, - data_len: 0, })], }, ) From 604eb0fc7eb60b348c69ec4f3956a2424a39f93e Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:28:20 -0700 Subject: [PATCH 02/28] Add accounts, asset, and collection to PluginValidationContext --- programs/mpl-core/src/plugins/lifecycle.rs | 22 +++++++++++++++---- .../src/processor/add_external_plugin.rs | 8 +++++++ programs/mpl-core/src/processor/add_plugin.rs | 8 +++++++ .../src/processor/approve_plugin_authority.rs | 2 ++ programs/mpl-core/src/processor/burn.rs | 2 ++ programs/mpl-core/src/processor/compress.rs | 1 + programs/mpl-core/src/processor/create.rs | 6 +++++ .../src/processor/create_collection.rs | 6 +++++ programs/mpl-core/src/processor/decompress.rs | 1 + .../src/processor/remove_external_plugin.rs | 2 ++ .../mpl-core/src/processor/remove_plugin.rs | 2 ++ .../src/processor/revoke_plugin_authority.rs | 2 ++ programs/mpl-core/src/processor/transfer.rs | 1 + programs/mpl-core/src/processor/update.rs | 2 ++ .../mpl-core/src/processor/update_plugin.rs | 2 ++ programs/mpl-core/src/utils.rs | 14 +++++++++--- 16 files changed, 74 insertions(+), 7 deletions(-) diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index d1276a46..0bee5388 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -657,6 +657,12 @@ pub enum ExternalValidationResult { /// The required context for a plugin validation. #[allow(dead_code)] pub(crate) struct PluginValidationContext<'a, 'b> { + /// This list of all the accounts passed into the instruction. + pub accounts: &'a [AccountInfo<'a>], + /// The asset account. + pub asset_info: Option<&'a AccountInfo<'a>>, + /// The collection account. + pub collection_info: Option<&'a AccountInfo<'a>>, /// The authority. pub self_authority: &'b Authority, /// The authority account. @@ -806,12 +812,13 @@ pub(crate) trait PluginValidation { #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub(crate) fn validate_plugin_checks<'a>( key: Key, + accounts: &'a [AccountInfo<'a>], checks: &BTreeMap, authority: &'a AccountInfo<'a>, new_owner: Option<&'a AccountInfo<'a>>, new_plugin: Option<&Plugin>, - asset: Option<&AccountInfo<'a>>, - collection: Option<&AccountInfo<'a>>, + asset: Option<&'a AccountInfo<'a>>, + collection: Option<&'a AccountInfo<'a>>, resolved_authorities: &[Authority], plugin_validate_fp: fn( &Plugin, @@ -834,6 +841,9 @@ pub(crate) fn validate_plugin_checks<'a>( }; let ctx = PluginValidationContext { + accounts, + asset_info: asset, + collection_info: collection, self_authority: ®istry_record.authority, authority_info: authority, resolved_authorities: Some(resolved_authorities), @@ -866,6 +876,7 @@ pub(crate) fn validate_plugin_checks<'a>( #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub(crate) fn validate_external_plugin_checks<'a>( key: Key, + accounts: &'a [AccountInfo<'a>], external_checks: &BTreeMap< ExternalPluginKey, (Key, ExternalCheckResultBits, ExternalRegistryRecord), @@ -873,8 +884,8 @@ pub(crate) fn validate_external_plugin_checks<'a>( authority: &'a AccountInfo<'a>, new_owner: Option<&'a AccountInfo<'a>>, new_plugin: Option<&Plugin>, - asset: Option<&AccountInfo<'a>>, - collection: Option<&AccountInfo<'a>>, + asset: Option<&'a AccountInfo<'a>>, + collection: Option<&'a AccountInfo<'a>>, resolved_authorities: &[Authority], external_plugin_validate_fp: fn( &ExternalPlugin, @@ -895,6 +906,9 @@ pub(crate) fn validate_external_plugin_checks<'a>( }; let ctx = PluginValidationContext { + accounts, + asset_info: asset, + collection_info: collection, self_authority: &external_registry_record.authority, authority_info: authority, resolved_authorities: Some(resolved_authorities), diff --git a/programs/mpl-core/src/processor/add_external_plugin.rs b/programs/mpl-core/src/processor/add_external_plugin.rs index f95fc9ba..d6d31a6c 100644 --- a/programs/mpl-core/src/processor/add_external_plugin.rs +++ b/programs/mpl-core/src/processor/add_external_plugin.rs @@ -48,6 +48,9 @@ pub(crate) fn add_external_plugin<'a>( } let validation_ctx = PluginValidationContext { + accounts, + asset_info: Some(ctx.accounts.asset), + collection_info: ctx.accounts.collection, self_authority: &Authority::UpdateAuthority, authority_info: authority, resolved_authorities: None, @@ -67,6 +70,7 @@ pub(crate) fn add_external_plugin<'a>( // Validate asset permissions. let (mut asset, _, _) = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -122,6 +126,9 @@ pub(crate) fn add_collection_external_plugin<'a>( } let validation_ctx = PluginValidationContext { + accounts, + asset_info: None, + collection_info: Some(ctx.accounts.collection), self_authority: &Authority::UpdateAuthority, authority_info: authority, resolved_authorities: None, @@ -141,6 +148,7 @@ pub(crate) fn add_collection_external_plugin<'a>( // Validate collection permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/add_plugin.rs b/programs/mpl-core/src/processor/add_plugin.rs index a250b26b..e365193e 100644 --- a/programs/mpl-core/src/processor/add_plugin.rs +++ b/programs/mpl-core/src/processor/add_plugin.rs @@ -49,6 +49,9 @@ pub(crate) fn add_plugin<'a>( //TODO: Seed with Rejected let validation_ctx = PluginValidationContext { + accounts, + asset_info: Some(ctx.accounts.asset), + collection_info: ctx.accounts.collection, self_authority: &args.init_authority.unwrap_or(args.plugin.manager()), authority_info: authority, resolved_authorities: None, @@ -61,6 +64,7 @@ pub(crate) fn add_plugin<'a>( // Validate asset permissions. let (mut asset, _, _) = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -117,6 +121,9 @@ pub(crate) fn add_collection_plugin<'a>( } let validation_context = PluginValidationContext { + accounts, + asset_info: None, + collection_info: Some(ctx.accounts.collection), self_authority: &args.init_authority.unwrap_or(args.plugin.manager()), authority_info: authority, resolved_authorities: None, @@ -135,6 +142,7 @@ pub(crate) fn add_collection_plugin<'a>( // Validate collection permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, Some(&args.plugin), diff --git a/programs/mpl-core/src/processor/approve_plugin_authority.rs b/programs/mpl-core/src/processor/approve_plugin_authority.rs index 97608dfe..c115c67b 100644 --- a/programs/mpl-core/src/processor/approve_plugin_authority.rs +++ b/programs/mpl-core/src/processor/approve_plugin_authority.rs @@ -51,6 +51,7 @@ pub(crate) fn approve_plugin_authority<'a>( // Validate asset permissions. let (mut asset, _, _) = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -111,6 +112,7 @@ pub(crate) fn approve_collection_plugin_authority<'a>( // Validate collection permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, Some(&plugin), diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index 0c30c87d..8d01f7d1 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -84,6 +84,7 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnV1Args) -> Pro // Validate asset permissions. let _ = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -132,6 +133,7 @@ pub(crate) fn burn_collection<'a>( // Validate collection permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/compress.rs b/programs/mpl-core/src/processor/compress.rs index 11595cd1..47a40b4f 100644 --- a/programs/mpl-core/src/processor/compress.rs +++ b/programs/mpl-core/src/processor/compress.rs @@ -44,6 +44,7 @@ pub(crate) fn compress<'a>( // Validate asset permissions. let _ = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 19c886ed..98a0aa1b 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -166,6 +166,9 @@ pub(crate) fn process_create<'a>( != CheckResult::None { let validation_ctx = PluginValidationContext { + accounts, + asset_info: Some(ctx.accounts.asset), + collection_info: ctx.accounts.collection, self_authority: &plugin.authority.unwrap_or(plugin.plugin.manager()), authority_info: authority, resolved_authorities: None, @@ -202,6 +205,9 @@ pub(crate) fn process_create<'a>( if ExternalPlugin::check_create(plugin_init_info) != ExternalCheckResult::none() { let validation_ctx = PluginValidationContext { + accounts, + asset_info: Some(ctx.accounts.asset), + collection_info: ctx.accounts.collection, // External plugins are always managed by the update authority. self_authority: &Authority::UpdateAuthority, authority_info: authority, diff --git a/programs/mpl-core/src/processor/create_collection.rs b/programs/mpl-core/src/processor/create_collection.rs index 460bc7f8..645b8e21 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -132,6 +132,9 @@ pub(crate) fn process_create_collection<'a>( if PluginType::check_create(&plugin_type) != CheckResult::None { let validation_ctx = PluginValidationContext { + accounts, + asset_info: None, + collection_info: Some(ctx.accounts.collection), self_authority: &plugin.authority.unwrap_or(plugin.plugin.manager()), authority_info: ctx.accounts.payer, resolved_authorities: None, @@ -167,6 +170,9 @@ pub(crate) fn process_create_collection<'a>( for plugin_init_info in &plugins { if ExternalPlugin::check_create(plugin_init_info) != ExternalCheckResult::none() { let validation_ctx = PluginValidationContext { + accounts, + asset_info: None, + collection_info: Some(ctx.accounts.collection), // External plugins are always managed by the update authority. self_authority: &Authority::UpdateAuthority, authority_info: authority, diff --git a/programs/mpl-core/src/processor/decompress.rs b/programs/mpl-core/src/processor/decompress.rs index 4014bb24..c4d11d77 100644 --- a/programs/mpl-core/src/processor/decompress.rs +++ b/programs/mpl-core/src/processor/decompress.rs @@ -60,6 +60,7 @@ pub(crate) fn decompress<'a>( // Validate asset permissions. let _ = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, diff --git a/programs/mpl-core/src/processor/remove_external_plugin.rs b/programs/mpl-core/src/processor/remove_external_plugin.rs index 20b1fd15..ce5a8a87 100644 --- a/programs/mpl-core/src/processor/remove_external_plugin.rs +++ b/programs/mpl-core/src/processor/remove_external_plugin.rs @@ -62,6 +62,7 @@ pub(crate) fn remove_external_plugin<'a>( // Validate asset permissions. let _ = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -130,6 +131,7 @@ pub(crate) fn remove_collection_external_plugin<'a>( // Validate asset permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/remove_plugin.rs b/programs/mpl-core/src/processor/remove_plugin.rs index b8878695..9818651b 100644 --- a/programs/mpl-core/src/processor/remove_plugin.rs +++ b/programs/mpl-core/src/processor/remove_plugin.rs @@ -57,6 +57,7 @@ pub(crate) fn remove_plugin<'a>( // Validate asset permissions. let _ = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -127,6 +128,7 @@ pub(crate) fn remove_collection_plugin<'a>( // Validate collection permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, Some(&plugin_to_remove), diff --git a/programs/mpl-core/src/processor/revoke_plugin_authority.rs b/programs/mpl-core/src/processor/revoke_plugin_authority.rs index 9567878e..aa60a7f6 100644 --- a/programs/mpl-core/src/processor/revoke_plugin_authority.rs +++ b/programs/mpl-core/src/processor/revoke_plugin_authority.rs @@ -58,6 +58,7 @@ pub(crate) fn revoke_plugin_authority<'a>( // Validate asset permissions. let _ = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -132,6 +133,7 @@ pub(crate) fn revoke_collection_plugin_authority<'a>( // Validate collection permissions. let _ = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, Some(&plugin), diff --git a/programs/mpl-core/src/processor/transfer.rs b/programs/mpl-core/src/processor/transfer.rs index 67971c0e..c95d8f34 100644 --- a/programs/mpl-core/src/processor/transfer.rs +++ b/programs/mpl-core/src/processor/transfer.rs @@ -77,6 +77,7 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferV1Args // Validate asset permissions. let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index a571477b..a3facc58 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -50,6 +50,7 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateV1Args) -> } let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -139,6 +140,7 @@ pub(crate) fn update_collection<'a>( } let (mut collection, plugin_header, plugin_registry) = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/update_plugin.rs b/programs/mpl-core/src/processor/update_plugin.rs index 92109254..cc1d39d5 100644 --- a/programs/mpl-core/src/processor/update_plugin.rs +++ b/programs/mpl-core/src/processor/update_plugin.rs @@ -48,6 +48,7 @@ pub(crate) fn update_plugin<'a>( } let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( + accounts, authority, ctx.accounts.asset, ctx.accounts.collection, @@ -182,6 +183,7 @@ pub(crate) fn update_collection_plugin<'a>( // Validate collection permissions. let (collection, plugin_header, plugin_registry) = validate_collection_permissions( + accounts, authority, ctx.accounts.collection, Some(&args.plugin), diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs index c5aa0c48..bf7b3043 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -196,9 +196,10 @@ pub(crate) fn resize_or_reallocate_account<'a>( #[allow(clippy::too_many_arguments, clippy::type_complexity)] /// Validate asset permissions using lifecycle validations for asset, collection, and plugins. pub(crate) fn validate_asset_permissions<'a>( + accounts: &'a [AccountInfo<'a>], authority_info: &'a AccountInfo<'a>, - asset: &AccountInfo<'a>, - collection: Option<&AccountInfo<'a>>, + asset: &'a AccountInfo<'a>, + collection: Option<&'a AccountInfo<'a>>, new_owner: Option<&'a AccountInfo<'a>>, new_plugin: Option<&Plugin>, new_external_plugin: Option<&ExternalPlugin>, @@ -330,6 +331,7 @@ pub(crate) fn validate_asset_permissions<'a>( match validate_plugin_checks( Key::CollectionV1, + accounts, &checks, authority_info, new_owner, @@ -349,6 +351,7 @@ pub(crate) fn validate_asset_permissions<'a>( match validate_plugin_checks( Key::AssetV1, + accounts, &checks, authority_info, new_owner, @@ -369,6 +372,7 @@ pub(crate) fn validate_asset_permissions<'a>( if let Some(external_plugin_validate_fp) = external_plugin_validate_fp { match validate_external_plugin_checks( Key::CollectionV1, + accounts, &external_checks, authority_info, new_owner, @@ -387,6 +391,7 @@ pub(crate) fn validate_asset_permissions<'a>( match validate_external_plugin_checks( Key::AssetV1, + accounts, &external_checks, authority_info, new_owner, @@ -416,8 +421,9 @@ pub(crate) fn validate_asset_permissions<'a>( /// Validate collection permissions using lifecycle validations for collection and plugins. #[allow(clippy::type_complexity, clippy::too_many_arguments)] pub(crate) fn validate_collection_permissions<'a>( + accounts: &'a [AccountInfo<'a>], authority_info: &'a AccountInfo<'a>, - collection: &AccountInfo<'a>, + collection: &'a AccountInfo<'a>, new_plugin: Option<&Plugin>, new_external_plugin: Option<&ExternalPlugin>, collection_check_fp: fn() -> CheckResult, @@ -506,6 +512,7 @@ pub(crate) fn validate_collection_permissions<'a>( match validate_plugin_checks( Key::CollectionV1, + accounts, &checks, authority_info, None, @@ -526,6 +533,7 @@ pub(crate) fn validate_collection_permissions<'a>( if let Some(external_plugin_validate_fp) = external_plugin_validate_fp { match validate_external_plugin_checks( Key::CollectionV1, + accounts, &external_checks, authority_info, None, From 4d14ce36e490652d5ac8ecf2a70c50d9bff5b37c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:29:02 -0700 Subject: [PATCH 03/28] Implement Oracle lifecycle validations --- .../mpl-core/src/plugins/external_plugins.rs | 67 +++++++++++++++-- programs/mpl-core/src/plugins/lifecycle.rs | 10 +++ programs/mpl-core/src/plugins/oracle.rs | 75 ++++++++++++++++++- 3 files changed, 143 insertions(+), 9 deletions(-) diff --git a/programs/mpl-core/src/plugins/external_plugins.rs b/programs/mpl-core/src/plugins/external_plugins.rs index ec56b4b6..51a3ed4b 100644 --- a/programs/mpl-core/src/plugins/external_plugins.rs +++ b/programs/mpl-core/src/plugins/external_plugins.rs @@ -3,10 +3,12 @@ use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, }; - use strum::EnumCount; -use crate::error::MplCoreError; +use crate::{ + error::MplCoreError, + state::{AssetV1, SolanaAccount}, +}; use super::{ Authority, DataStore, DataStoreInitInfo, DataStoreUpdateInfo, ExternalCheckResult, @@ -213,32 +215,35 @@ pub enum HookableLifecycleEvent { Update, } +/// Prefix used with some of the `ExtraAccounts` that are PDAs. +pub const MPL_CORE_PREFIX: &str = "mpl-core"; + /// Type used to specify extra accounts for external plugins. #[repr(C)] #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq)] pub enum ExtraAccount { - /// Program-based PDA with seeds ["mpl-core"] + /// Program-based PDA with seeds \["mpl-core"\] PreconfiguredProgram { /// Account is a signer is_signer: bool, /// Account is writable. is_writable: bool, }, - /// Collection-based PDA with seeds ["mpl-core", ] + /// Collection-based PDA with seeds \["mpl-core", collection_pubkey\] PreconfiguredCollection { /// Account is a signer is_signer: bool, /// Account is writable. is_writable: bool, }, - /// Owner-based PDA with seeds ["mpl-core", ] + /// Owner-based PDA with seeds \["mpl-core", owner_pubkey\] PreconfiguredOwner { /// Account is a signer is_signer: bool, /// Account is writable. is_writable: bool, }, - /// Recipient-based PDA with seeds ["mpl-core", ] + /// Recipient-based PDA with seeds \["mpl-core", recipient_pubkey\] /// If the lifecycle event has no recipient the derivation will fail. PreconfiguredRecipient { /// Account is a signer @@ -246,7 +251,7 @@ pub enum ExtraAccount { /// Account is writable. is_writable: bool, }, - /// Asset-based PDA with seeds ["mpl-core", ] + /// Asset-based PDA with seeds \["mpl-core", asset_pubkey\] PreconfiguredAsset { /// Account is a signer is_signer: bool, @@ -273,6 +278,54 @@ pub enum ExtraAccount { }, } +impl ExtraAccount { + pub(crate) fn derive( + &self, + program_id: &Pubkey, + ctx: &PluginValidationContext, + ) -> Result { + match &self { + ExtraAccount::PreconfiguredProgram { .. } => { + let seeds = &[MPL_CORE_PREFIX.as_bytes()]; + let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); + Ok(pubkey) + } + ExtraAccount::PreconfiguredCollection { .. } => { + let collection = ctx.collection_info.ok_or(MplCoreError::NotAvailable)?.key; + let seeds = &[MPL_CORE_PREFIX.as_bytes(), collection.as_ref()]; + let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); + Ok(pubkey) + } + ExtraAccount::PreconfiguredOwner { .. } => { + let asset_info = ctx.asset_info.ok_or(MplCoreError::NotAvailable)?; + let owner = AssetV1::load(asset_info, 0)?.owner; + let seeds = &[MPL_CORE_PREFIX.as_bytes(), owner.as_ref()]; + let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); + Ok(pubkey) + } + ExtraAccount::PreconfiguredRecipient { .. } => { + let recipient = ctx.new_owner.ok_or(MplCoreError::NotAvailable)?.key; + let seeds = &[MPL_CORE_PREFIX.as_bytes(), recipient.as_ref()]; + let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); + Ok(pubkey) + } + ExtraAccount::PreconfiguredAsset { .. } => { + let asset = ctx.asset_info.ok_or(MplCoreError::NotAvailable)?.key; + let seeds = &[MPL_CORE_PREFIX.as_bytes(), asset.as_ref()]; + let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); + Ok(pubkey) + } + ExtraAccount::CustomPda { seeds: _seeds, .. } => { + // TODO merge prefix with user specified seeds. + let new_seeds = &[MPL_CORE_PREFIX.as_bytes()]; + let (pubkey, _bump) = Pubkey::find_program_address(new_seeds, program_id); + Ok(pubkey) + } + ExtraAccount::Address { address, .. } => Ok(*address), + } + } +} + /// Seeds to be used for extra account custom PDA derivations. #[repr(C)] #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq)] diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 0bee5388..519b68c0 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -654,6 +654,16 @@ pub enum ExternalValidationResult { Pass, } +impl From for ValidationResult { + fn from(result: ExternalValidationResult) -> Self { + match result { + ExternalValidationResult::Approved => Self::Approved, + ExternalValidationResult::Rejected => Self::Rejected, + ExternalValidationResult::Pass => Self::Pass, + } + } +} + /// The required context for a plugin validation. #[allow(dead_code)] pub(crate) struct PluginValidationContext<'a, 'b> { diff --git a/programs/mpl-core/src/plugins/oracle.rs b/programs/mpl-core/src/plugins/oracle.rs index f4f61d7f..9d16d581 100644 --- a/programs/mpl-core/src/plugins/oracle.rs +++ b/programs/mpl-core/src/plugins/oracle.rs @@ -1,6 +1,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{program_error::ProgramError, pubkey::Pubkey}; +use crate::error::MplCoreError; + use super::{ Authority, ExternalCheckResult, ExternalValidationResult, ExtraAccount, HookableLifecycleEvent, PluginValidation, PluginValidationContext, ValidationResult, @@ -28,11 +30,69 @@ impl PluginValidation for Oracle { Ok(ValidationResult::Pass) } + fn validate_create( + &self, + ctx: &PluginValidationContext, + ) -> Result { + self.validate_helper(ctx, HookableLifecycleEvent::Create) + } + fn validate_transfer( &self, - _ctx: &PluginValidationContext, + ctx: &PluginValidationContext, ) -> Result { - Ok(ValidationResult::Pass) + self.validate_helper(ctx, HookableLifecycleEvent::Transfer) + } + + fn validate_burn( + &self, + ctx: &PluginValidationContext, + ) -> Result { + self.validate_helper(ctx, HookableLifecycleEvent::Burn) + } + + fn validate_update( + &self, + ctx: &PluginValidationContext, + ) -> Result { + self.validate_helper(ctx, HookableLifecycleEvent::Update) + } +} + +impl Oracle { + fn validate_helper( + &self, + ctx: &PluginValidationContext, + event: HookableLifecycleEvent, + ) -> Result { + let oracle_account = match &self.pda { + None => self.base_address, + Some(extra_account) => extra_account.derive(&self.base_address, ctx)?, + }; + + let oracle_account = ctx + .accounts + .iter() + .find(|account| *account.key == oracle_account) + .ok_or(MplCoreError::NotAvailable)?; + + let offset = self.results_offset.to_offset_usize(); + let validation_result = + OracleValidation::deserialize(&mut &(*oracle_account.data).borrow()[offset..])?; + + match validation_result { + OracleValidation::V1 { + create, + transfer, + burn, + update, + } => match event { + HookableLifecycleEvent::Create => Ok(ValidationResult::from(create)), + HookableLifecycleEvent::Transfer => Ok(ValidationResult::from(transfer)), + HookableLifecycleEvent::Burn => Ok(ValidationResult::from(burn)), + HookableLifecycleEvent::Update => Ok(ValidationResult::from(update)), + }, + } } } @@ -90,6 +150,17 @@ pub enum ValidationResultsOffset { Custom(usize), } +impl ValidationResultsOffset { + /// Convert the `ValidationResultsOffset` to the correct offset value as a `usize`. + pub fn to_offset_usize(&self) -> usize { + match self { + Self::NoOffset => 0, + Self::Anchor => 8, + Self::Custom(offset) => *offset, + } + } +} + /// Validation results struct for an Oracle account. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq)] pub enum OracleValidation { From e73230ffbaddb1fed1be2cdc265effb0f1d789e3 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Thu, 25 Apr 2024 23:49:29 -0700 Subject: [PATCH 04/28] oracle test wip --- clients/js/package.json | 1 + clients/js/src/plugins/lifecycleChecks.ts | 19 +- .../js/test/externalPlugins/oracle.test.ts | 165 ++++++++++++++++++ configs/scripts/program/build.sh | 1 + configs/validator.cjs | 5 + 5 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 clients/js/test/externalPlugins/oracle.test.ts diff --git a/clients/js/package.json b/clients/js/package.json index 4764839d..65847cff 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -33,6 +33,7 @@ "devDependencies": { "@metaplex-foundation/mpl-toolbox": "^0.8.0", "@ava/typescript": "^3.0.1", + "@metaplex-foundation/mpl-core-oracle-example": "^0.0.1", "@metaplex-foundation/umi": "^0.8.10", "@metaplex-foundation/umi-bundle-tests": "^0.8.10", "@solana/web3.js": "^1.73.0", diff --git a/clients/js/src/plugins/lifecycleChecks.ts b/clients/js/src/plugins/lifecycleChecks.ts index 7be856a1..bcfd4064 100644 --- a/clients/js/src/plugins/lifecycleChecks.ts +++ b/clients/js/src/plugins/lifecycleChecks.ts @@ -71,12 +71,19 @@ export function hookableLifecycleEventToLifecycleCheckKey( export function lifecycleChecksToBase( l: LifecycleChecks ): [HookableLifecycleEvent, ExternalCheckResult][] { - return Object(l) - .keys() - .map((key: keyof LifecycleChecks) => [ - lifecycleCheckKeyToEnum(key), - l[key], - ]); + return Object + .keys(l) + .map((key) => { + const k = key as keyof LifecycleChecks; + const value = l[k]; + if (value) { + return [ + lifecycleCheckKeyToEnum(k), + checkResultsToExternalCheckResult(value), + ] + } + return null + }).filter((x) => x !== null) as [HookableLifecycleEvent, ExternalCheckResult][]; } export function lifecycleChecksFromBase( diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts new file mode 100644 index 00000000..abcb1eed --- /dev/null +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -0,0 +1,165 @@ +import test from 'ava'; +import fs from "fs"; + +import { mplCoreOracleExample, fixedAccountInit, fixedAccountSet } from '@metaplex-foundation/mpl-core-oracle-example' +import { createSignerFromKeypair, Context, generateSigner } from '@metaplex-foundation/umi'; +import { ExternalValidationResult } from '@metaplex-foundation/mpl-core-oracle-example/dist/src/hooked'; +import { assertAsset, createUmi as baseCreateUmi, DEFAULT_ASSET } from '../_setupRaw'; +import { createAsset } from '../_setupSdk'; +import { CheckResult, transfer, update } from '../../src'; + + +const createUmi = async () => (await baseCreateUmi()).use(mplCoreOracleExample()); +function loadSecretFromFile(filename: string) { + const secret = JSON.parse(fs.readFileSync(filename).toString()) as number[]; + const secretKey = Uint8Array.from(secret); + return secretKey +} + +const secret = loadSecretFromFile('../../../mpl-core-oracle-example/aaa48hFxxsUJb2MUeUVe8ABH42F6nho69oXUkSgKeSM.json') +function getAuthoritySigner(umi: Context) { + return createSignerFromKeypair(umi, umi.eddsa.createKeypairFromSecretKey(secret)) +} + +test('it can use fixed address oracle to control update', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi) + + const signer = getAuthoritySigner(umi) + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + } + } + }).sendAndConfirm(umi) + + // create asset referencing the oracle account + const asset = await createAsset(umi, { + plugins: [{ + type: 'Oracle', + resultsOffset: { + type: 'Anchor' + }, + lifecycleChecks: { + update: [CheckResult.CAN_DENY] + }, + baseAddress: account.publicKey, + }] + }) + + const result = update(umi, { + asset, + name: 'new name' + }).sendAndConfirm(umi) + + await t.throwsAsync(result, {name: 'InvalidAuthority'}) + + await fixedAccountSet(umi, { + account: account.publicKey, + signer, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + } + } + }).sendAndConfirm(umi) + + await update(umi, { + asset, + name: 'new name 2' + }).sendAndConfirm(umi) + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + name: 'new name 2' + }) + +}) + +test('it can use fixed address oracle to control transfer', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi) + + const signer = getAuthoritySigner(umi) + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + } + } + }).sendAndConfirm(umi) + + // create asset referencing the oracle account + const asset = await createAsset(umi, { + plugins: [{ + type: 'Oracle', + resultsOffset: { + type: 'Anchor' + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_DENY] + }, + baseAddress: account.publicKey, + }] + }) + + const newOwner = generateSigner(umi) + + const result = transfer(umi, { + asset, + newOwner: newOwner.publicKey + }).sendAndConfirm(umi) + + await t.throwsAsync(result, {name: 'InvalidAuthority'}) + + await fixedAccountSet(umi, { + account: account.publicKey, + signer, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + } + } + }).sendAndConfirm(umi) + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey + }).sendAndConfirm(umi) + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }) + +}) \ No newline at end of file diff --git a/configs/scripts/program/build.sh b/configs/scripts/program/build.sh index c8708bc7..576c081a 100755 --- a/configs/scripts/program/build.sh +++ b/configs/scripts/program/build.sh @@ -4,6 +4,7 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) OUTPUT="./programs/.bin" # saves external programs binaries to the output directory source ${SCRIPT_DIR}/dump.sh ${OUTPUT} +cp ~/src/mpl-core-oracle-example/target/deploy/mpl_core_oracle_example.so ${OUTPUT}/mpl_core_oracle_example.so # go to parent folder cd $(dirname $(dirname $(dirname ${SCRIPT_DIR}))) diff --git a/configs/validator.cjs b/configs/validator.cjs index 10be4405..58043348 100755 --- a/configs/validator.cjs +++ b/configs/validator.cjs @@ -15,6 +15,11 @@ module.exports = { programId: "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d", deployPath: getProgram("mpl_core_program.so"), }, + { + label: "Mpl Core Oracle Example", + programId: "4RZ7RhXeL4oz4kVX5fpRfkNQ3nz1n4eruqBn2AGPQepo", + deployPath: getProgram("mpl_core_oracle_example.so"), + }, // Below are external programs that should be included in the local validator. // You may configure which ones to fetch from the cluster when building // programs within the `configs/program-scripts/dump.sh` script. From f10eea43466b118fe9b9ce808c3fbdb97ed58361 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:29:27 -0700 Subject: [PATCH 05/28] Add new error codes --- programs/mpl-core/src/error.rs | 8 ++++++++ programs/mpl-core/src/plugins/external_plugins.rs | 13 ++++++++----- programs/mpl-core/src/plugins/oracle.rs | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index c5fae33f..4cff407a 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -136,6 +136,14 @@ pub enum MplCoreError { /// 31 - External Plugin not found #[error("External Plugin not found")] ExternalPluginNotFound, + + /// 32 - Missing asset needed for extra account PDA derivation + #[error("Missing asset needed for extra account PDA derivation")] + MissingAsset, + + /// 33 - Missing account needed for external plugin + #[error("Missing account needed for external plugin")] + MissingExternalAccount, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/external_plugins.rs b/programs/mpl-core/src/plugins/external_plugins.rs index 51a3ed4b..e1d2e41e 100644 --- a/programs/mpl-core/src/plugins/external_plugins.rs +++ b/programs/mpl-core/src/plugins/external_plugins.rs @@ -284,33 +284,36 @@ impl ExtraAccount { program_id: &Pubkey, ctx: &PluginValidationContext, ) -> Result { - match &self { + match self { ExtraAccount::PreconfiguredProgram { .. } => { let seeds = &[MPL_CORE_PREFIX.as_bytes()]; let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) } ExtraAccount::PreconfiguredCollection { .. } => { - let collection = ctx.collection_info.ok_or(MplCoreError::NotAvailable)?.key; + let collection = ctx + .collection_info + .ok_or(MplCoreError::MissingCollection)? + .key; let seeds = &[MPL_CORE_PREFIX.as_bytes(), collection.as_ref()]; let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) } ExtraAccount::PreconfiguredOwner { .. } => { - let asset_info = ctx.asset_info.ok_or(MplCoreError::NotAvailable)?; + let asset_info = ctx.asset_info.ok_or(MplCoreError::MissingAsset)?; let owner = AssetV1::load(asset_info, 0)?.owner; let seeds = &[MPL_CORE_PREFIX.as_bytes(), owner.as_ref()]; let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) } ExtraAccount::PreconfiguredRecipient { .. } => { - let recipient = ctx.new_owner.ok_or(MplCoreError::NotAvailable)?.key; + let recipient = ctx.new_owner.ok_or(MplCoreError::MissingNewOwner)?.key; let seeds = &[MPL_CORE_PREFIX.as_bytes(), recipient.as_ref()]; let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) } ExtraAccount::PreconfiguredAsset { .. } => { - let asset = ctx.asset_info.ok_or(MplCoreError::NotAvailable)?.key; + let asset = ctx.asset_info.ok_or(MplCoreError::MissingAsset)?.key; let seeds = &[MPL_CORE_PREFIX.as_bytes(), asset.as_ref()]; let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) diff --git a/programs/mpl-core/src/plugins/oracle.rs b/programs/mpl-core/src/plugins/oracle.rs index 9d16d581..ababa957 100644 --- a/programs/mpl-core/src/plugins/oracle.rs +++ b/programs/mpl-core/src/plugins/oracle.rs @@ -74,7 +74,7 @@ impl Oracle { .accounts .iter() .find(|account| *account.key == oracle_account) - .ok_or(MplCoreError::NotAvailable)?; + .ok_or(MplCoreError::MissingExternalAccount)?; let offset = self.results_offset.to_offset_usize(); let validation_result = From 17920ac070725921e9a17be6bbb568765560fd5a Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:30:24 -0700 Subject: [PATCH 06/28] Regenerate IDL and clients --- clients/js/src/generated/errors/mplCore.ts | 30 +++++++++++++++++++ clients/rust/src/generated/errors/mpl_core.rs | 6 ++++ idls/mpl_core.json | 10 +++++++ 3 files changed, 46 insertions(+) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 0ccb5459..760b166f 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -446,6 +446,36 @@ export class ExternalPluginNotFoundError extends ProgramError { codeToErrorMap.set(0x1f, ExternalPluginNotFoundError); nameToErrorMap.set('ExternalPluginNotFound', ExternalPluginNotFoundError); +/** MissingAsset: Missing asset needed for extra account PDA derivation */ +export class MissingAssetError extends ProgramError { + override readonly name: string = 'MissingAsset'; + + readonly code: number = 0x20; // 32 + + constructor(program: Program, cause?: Error) { + super( + 'Missing asset needed for extra account PDA derivation', + program, + cause + ); + } +} +codeToErrorMap.set(0x20, MissingAssetError); +nameToErrorMap.set('MissingAsset', MissingAssetError); + +/** MissingExternalAccount: Missing account needed for external plugin */ +export class MissingExternalAccountError extends ProgramError { + override readonly name: string = 'MissingExternalAccount'; + + readonly code: number = 0x21; // 33 + + constructor(program: Program, cause?: Error) { + super('Missing account needed for external plugin', program, cause); + } +} +codeToErrorMap.set(0x21, MissingExternalAccountError); +nameToErrorMap.set('MissingExternalAccount', MissingExternalAccountError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index ca432402..8a550f01 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -106,6 +106,12 @@ pub enum MplCoreError { /// 31 (0x1F) - External Plugin not found #[error("External Plugin not found")] ExternalPluginNotFound, + /// 32 (0x20) - Missing asset needed for extra account PDA derivation + #[error("Missing asset needed for extra account PDA derivation")] + MissingAsset, + /// 33 (0x21) - Missing account needed for external plugin + #[error("Missing account needed for external plugin")] + MissingExternalAccount, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 28f5a8cb..821f11fb 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -4108,6 +4108,16 @@ "code": 31, "name": "ExternalPluginNotFound", "msg": "External Plugin not found" + }, + { + "code": 32, + "name": "MissingAsset", + "msg": "Missing asset needed for extra account PDA derivation" + }, + { + "code": 33, + "name": "MissingExternalAccount", + "msg": "Missing account needed for external plugin" } ], "metadata": { From 4102baff2081fcb124c65caa386840fe96c4c9fd Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 26 Apr 2024 02:19:36 -0700 Subject: [PATCH 07/28] Derive custom PDA using Seeds Vec --- .../mpl-core/src/plugins/external_plugins.rs | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/programs/mpl-core/src/plugins/external_plugins.rs b/programs/mpl-core/src/plugins/external_plugins.rs index e1d2e41e..bd2aadf1 100644 --- a/programs/mpl-core/src/plugins/external_plugins.rs +++ b/programs/mpl-core/src/plugins/external_plugins.rs @@ -318,10 +318,13 @@ impl ExtraAccount { let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) } - ExtraAccount::CustomPda { seeds: _seeds, .. } => { - // TODO merge prefix with user specified seeds. - let new_seeds = &[MPL_CORE_PREFIX.as_bytes()]; - let (pubkey, _bump) = Pubkey::find_program_address(new_seeds, program_id); + ExtraAccount::CustomPda { seeds, .. } => { + let seeds = transform_seeds(seeds, ctx)?; + + // Convert the Vec of Vec into Vec of u8 slices. + let vec_of_slices: Vec<&[u8]> = seeds.iter().map(Vec::as_slice).collect(); + + let (pubkey, _bump) = Pubkey::find_program_address(&vec_of_slices, program_id); Ok(pubkey) } ExtraAccount::Address { address, .. } => Ok(*address), @@ -329,6 +332,59 @@ impl ExtraAccount { } } +// Transform seeds from their tokens into actual seeds based on passed-in context values. +fn transform_seeds( + seeds: &Vec, + ctx: &PluginValidationContext, +) -> Result>, ProgramError> { + let mut transformed_seeds = Vec::>::new(); + + for seed in seeds { + match seed { + Seed::Program => { + transformed_seeds.push(crate::ID.as_ref().to_vec()); + } + Seed::Collection => { + let collection = ctx + .collection_info + .ok_or(MplCoreError::MissingCollection)? + .key + .as_ref() + .to_vec(); + transformed_seeds.push(collection); + } + Seed::Owner => { + let asset_info = ctx.asset_info.ok_or(MplCoreError::MissingAsset)?; + let owner = AssetV1::load(asset_info, 0)?.owner.as_ref().to_vec(); + transformed_seeds.push(owner); + } + Seed::Recipient => { + let recipient = ctx + .new_owner + .ok_or(MplCoreError::MissingNewOwner)? + .key + .as_ref() + .to_vec(); + transformed_seeds.push(recipient); + } + Seed::Asset => { + let asset = ctx + .asset_info + .ok_or(MplCoreError::MissingAsset)? + .key + .as_ref() + .to_vec(); + transformed_seeds.push(asset); + } + Seed::Bytes(val) => { + transformed_seeds.push(val.clone()); + } + } + } + + Ok(transformed_seeds) +} + /// Seeds to be used for extra account custom PDA derivations. #[repr(C)] #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq)] From 738809caafe2c8c7c1e490ce386a9270774d6140 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:23:42 -0700 Subject: [PATCH 08/28] Make sure third party plugins can approve or reject before using output --- programs/mpl-core/src/plugins/lifecycle.rs | 23 ++++++++++++++----- programs/mpl-core/src/processor/add_plugin.rs | 5 ++-- programs/mpl-core/src/processor/create.rs | 9 +++++--- .../src/processor/create_collection.rs | 7 ++++-- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 519b68c0..59484ebf 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -850,7 +850,7 @@ pub(crate) fn validate_plugin_checks<'a>( _ => unreachable!(), }; - let ctx = PluginValidationContext { + let validation_ctx = PluginValidationContext { accounts, asset_info: asset, collection_info: collection, @@ -861,7 +861,10 @@ pub(crate) fn validate_plugin_checks<'a>( target_plugin: new_plugin, }; - let result = plugin_validate_fp(&Plugin::load(account, registry_record.offset)?, &ctx)?; + let result = plugin_validate_fp( + &Plugin::load(account, registry_record.offset)?, + &validation_ctx, + )?; match result { ValidationResult::Rejected => rejected = true, ValidationResult::Approved => approved = true, @@ -915,7 +918,7 @@ pub(crate) fn validate_external_plugin_checks<'a>( _ => unreachable!(), }; - let ctx = PluginValidationContext { + let validation_ctx = PluginValidationContext { accounts, asset_info: asset, collection_info: collection, @@ -928,11 +931,19 @@ pub(crate) fn validate_external_plugin_checks<'a>( let result = external_plugin_validate_fp( &ExternalPlugin::load(account, external_registry_record.offset)?, - &ctx, + &validation_ctx, )?; match result { - ValidationResult::Rejected => return Ok(ValidationResult::Rejected), - ValidationResult::Approved => approved = true, + ValidationResult::Rejected => { + if check_result.can_reject() { + return Ok(ValidationResult::Rejected); + } + } + ValidationResult::Approved => { + if check_result.can_approve() { + approved = true; + } + } ValidationResult::Pass => continue, // Force approved will not be possible from external plugins. ValidationResult::ForceApproved => unreachable!(), diff --git a/programs/mpl-core/src/processor/add_plugin.rs b/programs/mpl-core/src/processor/add_plugin.rs index e365193e..95c9117d 100644 --- a/programs/mpl-core/src/processor/add_plugin.rs +++ b/programs/mpl-core/src/processor/add_plugin.rs @@ -120,7 +120,7 @@ pub(crate) fn add_collection_plugin<'a>( } } - let validation_context = PluginValidationContext { + let validation_ctx = PluginValidationContext { accounts, asset_info: None, collection_info: Some(ctx.accounts.collection), @@ -130,8 +130,7 @@ pub(crate) fn add_collection_plugin<'a>( new_owner: None, target_plugin: Some(&args.plugin), }; - if Plugin::validate_add_plugin(&args.plugin, &validation_context)? == ValidationResult::Rejected - { + if Plugin::validate_add_plugin(&args.plugin, &validation_ctx)? == ValidationResult::Rejected { return Err(MplCoreError::InvalidAuthority.into()); } diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 98a0aa1b..4f348436 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, initialize_plugin, - CheckResult, ExternalCheckResult, ExternalPlugin, ExternalPluginInitInfo, Plugin, + CheckResult, ExternalCheckResultBits, ExternalPlugin, ExternalPluginInitInfo, Plugin, PluginAuthorityPair, PluginType, PluginValidationContext, ValidationResult, }, state::{ @@ -202,8 +202,11 @@ pub(crate) fn process_create<'a>( ctx.accounts.system_program, )?; for plugin_init_info in &plugins { - if ExternalPlugin::check_create(plugin_init_info) != ExternalCheckResult::none() - { + let external_check_result_bits = ExternalCheckResultBits::from( + ExternalPlugin::check_create(plugin_init_info), + ); + + if external_check_result_bits.can_reject() { 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 645b8e21..77f9b0b5 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -10,7 +10,7 @@ use crate::{ instruction::accounts::CreateCollectionV2Accounts, plugins::{ create_meta_idempotent, create_plugin_meta, initialize_external_plugin, initialize_plugin, - CheckResult, ExternalCheckResult, ExternalPlugin, ExternalPluginInitInfo, Plugin, + CheckResult, ExternalCheckResultBits, ExternalPlugin, ExternalPluginInitInfo, Plugin, PluginAuthorityPair, PluginType, PluginValidationContext, ValidationResult, }, state::{Authority, CollectionV1, Key}, @@ -168,7 +168,10 @@ pub(crate) fn process_create_collection<'a>( ctx.accounts.system_program, )?; for plugin_init_info in &plugins { - if ExternalPlugin::check_create(plugin_init_info) != ExternalCheckResult::none() { + let external_check_result_bits = + ExternalCheckResultBits::from(ExternalPlugin::check_create(plugin_init_info)); + + if external_check_result_bits.can_reject() { let validation_ctx = PluginValidationContext { accounts, asset_info: None, From d2b7c6f468a8d771746a93d7576d558301a725fe Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:47:24 -0700 Subject: [PATCH 09/28] Use user program for Program Seed --- programs/mpl-core/src/plugins/external_plugins.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/programs/mpl-core/src/plugins/external_plugins.rs b/programs/mpl-core/src/plugins/external_plugins.rs index bd2aadf1..58a51630 100644 --- a/programs/mpl-core/src/plugins/external_plugins.rs +++ b/programs/mpl-core/src/plugins/external_plugins.rs @@ -319,7 +319,7 @@ impl ExtraAccount { Ok(pubkey) } ExtraAccount::CustomPda { seeds, .. } => { - let seeds = transform_seeds(seeds, ctx)?; + let seeds = transform_seeds(seeds, program_id, ctx)?; // Convert the Vec of Vec into Vec of u8 slices. let vec_of_slices: Vec<&[u8]> = seeds.iter().map(Vec::as_slice).collect(); @@ -335,6 +335,7 @@ impl ExtraAccount { // Transform seeds from their tokens into actual seeds based on passed-in context values. fn transform_seeds( seeds: &Vec, + program_id: &Pubkey, ctx: &PluginValidationContext, ) -> Result>, ProgramError> { let mut transformed_seeds = Vec::>::new(); @@ -342,7 +343,7 @@ fn transform_seeds( for seed in seeds { match seed { Seed::Program => { - transformed_seeds.push(crate::ID.as_ref().to_vec()); + transformed_seeds.push(program_id.as_ref().to_vec()); } Seed::Collection => { let collection = ctx From a797186b5448e454f5bfcae23bba651731542d61 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Fri, 26 Apr 2024 19:20:31 -0700 Subject: [PATCH 10/28] add more oracle tests, make function to derive oracle accounts --- .../types/baseExternalPluginInitInfo.ts | 4 +- .../generated/types/baseExternalPluginKey.ts | 4 +- .../types/baseExternalPluginUpdateInfo.ts | 4 +- .../generated/types/basePluginAuthority.ts | 2 +- .../generated/types/baseUpdateAuthority.ts | 2 +- .../types/baseValidationResultsOffset.ts | 4 +- clients/js/src/hooked/pluginRegistryV1Data.ts | 5 +- .../js/src/plugins/externalPluginManifest.ts | 6 +- clients/js/src/plugins/externalPlugins.ts | 37 +- clients/js/src/plugins/extraAccount.ts | 61 +- clients/js/src/plugins/lib.ts | 1 - clients/js/src/plugins/lifecycleChecks.ts | 13 +- clients/js/src/plugins/masterEdition.ts | 21 +- clients/js/src/plugins/oracle.ts | 28 +- clients/js/src/plugins/types.ts | 10 +- clients/js/src/utils.ts | 4 +- clients/js/test/_setupRaw.ts | 14 + clients/js/test/burn.test.ts | 15 +- .../js/test/externalPlugins/oracle.test.ts | 637 ++++++++++++++++-- .../test/plugins/asset/permanentBurn.test.ts | 7 +- clients/js/test/sdkv1.test.ts | 10 +- 21 files changed, 722 insertions(+), 167 deletions(-) diff --git a/clients/js/src/generated/types/baseExternalPluginInitInfo.ts b/clients/js/src/generated/types/baseExternalPluginInitInfo.ts index c739845e..6d93b748 100644 --- a/clients/js/src/generated/types/baseExternalPluginInitInfo.ts +++ b/clients/js/src/generated/types/baseExternalPluginInitInfo.ts @@ -88,14 +88,14 @@ export function baseExternalPluginInitInfo( >['fields'] ): GetDataEnumKind; export function baseExternalPluginInitInfo< - K extends BaseExternalPluginInitInfoArgs['__kind'], + K extends BaseExternalPluginInitInfoArgs['__kind'] >(kind: K, data?: any): Extract { return Array.isArray(data) ? { __kind: kind, fields: data } : { __kind: kind, ...(data ?? {}) }; } export function isBaseExternalPluginInitInfo< - K extends BaseExternalPluginInitInfo['__kind'], + K extends BaseExternalPluginInitInfo['__kind'] >( kind: K, value: BaseExternalPluginInitInfo diff --git a/clients/js/src/generated/types/baseExternalPluginKey.ts b/clients/js/src/generated/types/baseExternalPluginKey.ts index 294d0052..a9f34c10 100644 --- a/clients/js/src/generated/types/baseExternalPluginKey.ts +++ b/clients/js/src/generated/types/baseExternalPluginKey.ts @@ -78,14 +78,14 @@ export function baseExternalPluginKey( data: GetDataEnumKindContent['fields'] ): GetDataEnumKind; export function baseExternalPluginKey< - K extends BaseExternalPluginKeyArgs['__kind'], + K extends BaseExternalPluginKeyArgs['__kind'] >(kind: K, data?: any): Extract { return Array.isArray(data) ? { __kind: kind, fields: data } : { __kind: kind, ...(data ?? {}) }; } export function isBaseExternalPluginKey< - K extends BaseExternalPluginKey['__kind'], + K extends BaseExternalPluginKey['__kind'] >( kind: K, value: BaseExternalPluginKey diff --git a/clients/js/src/generated/types/baseExternalPluginUpdateInfo.ts b/clients/js/src/generated/types/baseExternalPluginUpdateInfo.ts index 6cd1e7d7..c97eefbb 100644 --- a/clients/js/src/generated/types/baseExternalPluginUpdateInfo.ts +++ b/clients/js/src/generated/types/baseExternalPluginUpdateInfo.ts @@ -91,7 +91,7 @@ export function baseExternalPluginUpdateInfo( >['fields'] ): GetDataEnumKind; export function baseExternalPluginUpdateInfo< - K extends BaseExternalPluginUpdateInfoArgs['__kind'], + K extends BaseExternalPluginUpdateInfoArgs['__kind'] >( kind: K, data?: any @@ -101,7 +101,7 @@ export function baseExternalPluginUpdateInfo< : { __kind: kind, ...(data ?? {}) }; } export function isBaseExternalPluginUpdateInfo< - K extends BaseExternalPluginUpdateInfo['__kind'], + K extends BaseExternalPluginUpdateInfo['__kind'] >( kind: K, value: BaseExternalPluginUpdateInfo diff --git a/clients/js/src/generated/types/basePluginAuthority.ts b/clients/js/src/generated/types/basePluginAuthority.ts index 8a6a7002..c9916538 100644 --- a/clients/js/src/generated/types/basePluginAuthority.ts +++ b/clients/js/src/generated/types/basePluginAuthority.ts @@ -60,7 +60,7 @@ export function basePluginAuthority( data: GetDataEnumKindContent ): GetDataEnumKind; export function basePluginAuthority< - K extends BasePluginAuthorityArgs['__kind'], + K extends BasePluginAuthorityArgs['__kind'] >(kind: K, data?: any): Extract { return Array.isArray(data) ? { __kind: kind, fields: data } diff --git a/clients/js/src/generated/types/baseUpdateAuthority.ts b/clients/js/src/generated/types/baseUpdateAuthority.ts index de8818f2..f32f22a3 100644 --- a/clients/js/src/generated/types/baseUpdateAuthority.ts +++ b/clients/js/src/generated/types/baseUpdateAuthority.ts @@ -62,7 +62,7 @@ export function baseUpdateAuthority( data: GetDataEnumKindContent['fields'] ): GetDataEnumKind; export function baseUpdateAuthority< - K extends BaseUpdateAuthorityArgs['__kind'], + K extends BaseUpdateAuthorityArgs['__kind'] >(kind: K, data?: any): Extract { return Array.isArray(data) ? { __kind: kind, fields: data } diff --git a/clients/js/src/generated/types/baseValidationResultsOffset.ts b/clients/js/src/generated/types/baseValidationResultsOffset.ts index 0d960574..8dc23a02 100644 --- a/clients/js/src/generated/types/baseValidationResultsOffset.ts +++ b/clients/js/src/generated/types/baseValidationResultsOffset.ts @@ -61,7 +61,7 @@ export function baseValidationResultsOffset( >['fields'] ): GetDataEnumKind; export function baseValidationResultsOffset< - K extends BaseValidationResultsOffsetArgs['__kind'], + K extends BaseValidationResultsOffsetArgs['__kind'] >( kind: K, data?: any @@ -71,7 +71,7 @@ export function baseValidationResultsOffset< : { __kind: kind, ...(data ?? {}) }; } export function isBaseValidationResultsOffset< - K extends BaseValidationResultsOffset['__kind'], + K extends BaseValidationResultsOffset['__kind'] >( kind: K, value: BaseValidationResultsOffset diff --git a/clients/js/src/hooked/pluginRegistryV1Data.ts b/clients/js/src/hooked/pluginRegistryV1Data.ts index 0cead5b4..56e264b5 100644 --- a/clients/js/src/hooked/pluginRegistryV1Data.ts +++ b/clients/js/src/hooked/pluginRegistryV1Data.ts @@ -134,7 +134,10 @@ export function getExternalRegistryRecordSerializer(): Serializer< lifecycleChecksOffset ); - const [dataOffset, dataOffsetOffset] = option(u64()).deserialize(buffer, pluginOffsetOffset); + const [dataOffset, dataOffsetOffset] = option(u64()).deserialize( + buffer, + pluginOffsetOffset + ); const [dataLen] = option(u64()).deserialize(buffer, dataOffsetOffset); return [ { diff --git a/clients/js/src/plugins/externalPluginManifest.ts b/clients/js/src/plugins/externalPluginManifest.ts index beab8ba9..bc61bc62 100644 --- a/clients/js/src/plugins/externalPluginManifest.ts +++ b/clients/js/src/plugins/externalPluginManifest.ts @@ -10,7 +10,11 @@ export type ExternalPluginManifest< UpdateBase extends Object > = { type: ExternalPluginTypeString; - fromBase: (input: Base, record: ExternalRegistryRecord, account: Uint8Array) => T; + fromBase: ( + input: Base, + record: ExternalRegistryRecord, + account: Uint8Array + ) => T; initToBase: (input: Init) => InitBase; updateToBase: (input: Update) => UpdateBase; }; diff --git a/clients/js/src/plugins/externalPlugins.ts b/clients/js/src/plugins/externalPlugins.ts index e1a4a45f..80a1e355 100644 --- a/clients/js/src/plugins/externalPlugins.ts +++ b/clients/js/src/plugins/externalPlugins.ts @@ -1,4 +1,9 @@ -import { AccountMeta, Context, PublicKey, Option } from '@metaplex-foundation/umi'; +import { + AccountMeta, + Context, + PublicKey, + Option, +} from '@metaplex-foundation/umi'; import { lifecycleHookFromBase, LifecycleHookInitInfoArgs, @@ -50,25 +55,25 @@ export type ExternalPluginsList = { export type ExternalPluginInitInfoArgs = | ({ - type: 'Oracle'; - } & OracleInitInfoArgs) + type: 'Oracle'; + } & OracleInitInfoArgs) | ({ - type: 'LifecycleHook'; - } & LifecycleHookInitInfoArgs) + type: 'LifecycleHook'; + } & LifecycleHookInitInfoArgs) | ({ - type: 'DataStore'; - } & DataStoreInitInfoArgs); + type: 'DataStore'; + } & DataStoreInitInfoArgs); export type ExternalPluginUpdateInfoArgs = | ({ - type: 'Oracle'; - } & OracleUpdateInfoArgs) + type: 'Oracle'; + } & OracleUpdateInfoArgs) | ({ - type: 'LifecycleHook'; - } & LifecycleHookUpdateInfoArgs) + type: 'LifecycleHook'; + } & LifecycleHookUpdateInfoArgs) | ({ - type: 'DataStore'; - } & DataStoreUpdateInfoArgs); + type: 'DataStore'; + } & DataStoreUpdateInfoArgs); export const externalPluginManifests = { Oracle: oracleManifest, @@ -128,7 +133,11 @@ export function externalRegistryRecordsToExternalPluginList( result.lifecycleHooks.push({ type: 'LifecycleHook', ...mappedPlugin, - ...lifecycleHookFromBase(deserializedPlugin.fields[0], record, accountData), + ...lifecycleHookFromBase( + deserializedPlugin.fields[0], + record, + accountData + ), }); } }); diff --git a/clients/js/src/plugins/extraAccount.ts b/clients/js/src/plugins/extraAccount.ts index 276f90b9..8c44aa1c 100644 --- a/clients/js/src/plugins/extraAccount.ts +++ b/clients/js/src/plugins/extraAccount.ts @@ -20,21 +20,27 @@ export const findPreconfiguredPda = ( ]); export type ExtraAccount = - | Exclude< - RenameToType, - { type: 'CustomPda' } | { type: 'Address' } - > + | (Omit< + Exclude< + RenameToType, + { type: 'CustomPda' } | { type: 'Address' } + >, + 'isSigner' | 'isWritable' + > & { + isSigner?: boolean; + isWritable?: boolean; + }) | { type: 'CustomPda'; seeds: Array; - isSigner: boolean; - isWritable: boolean; + isSigner?: boolean; + isWritable?: boolean; } | { type: 'Address'; address: PublicKey; - isSigner: boolean; - isWritable: boolean; + isSigner?: boolean; + isWritable?: boolean; }; export function extraAccountToAccountMeta( @@ -48,55 +54,55 @@ export function extraAccountToAccountMeta( owner?: PublicKey; } ): AccountMeta { + const acccountMeta: Pick = { + isSigner: e.isSigner || false, + isWritable: e.isWritable || false, + }; + switch (e.type) { case 'PreconfiguredProgram': if (!inputs.program) throw new Error('Program address is required'); return { + ...acccountMeta, pubkey: context.eddsa.findPda(inputs.program, [ string({ size: 'variable' }).serialize(PRECONFIGURED_SEED), ])[0], - isSigner: e.isSigner, - isWritable: e.isWritable, }; case 'PreconfiguredCollection': if (!inputs.program) throw new Error('Program address is required'); if (!inputs.collection) throw new Error('Collection address is required'); return { + ...acccountMeta, pubkey: findPreconfiguredPda( context, inputs.program, inputs.collection )[0], - isSigner: e.isSigner, - isWritable: e.isWritable, }; case 'PreconfiguredOwner': if (!inputs.program) throw new Error('Program address is required'); if (!inputs.owner) throw new Error('Owner address is required'); return { + ...acccountMeta, pubkey: findPreconfiguredPda(context, inputs.program, inputs.owner)[0], - isSigner: e.isSigner, - isWritable: e.isWritable, }; case 'PreconfiguredRecipient': if (!inputs.program) throw new Error('Program address is required'); if (!inputs.recipient) throw new Error('Recipient address is required'); return { + ...acccountMeta, pubkey: findPreconfiguredPda( context, inputs.program, inputs.recipient )[0], - isSigner: e.isSigner, - isWritable: e.isWritable, }; case 'PreconfiguredAsset': if (!inputs.program) throw new Error('Program address is required'); if (!inputs.asset) throw new Error('Asset address is required'); return { + ...acccountMeta, pubkey: findPreconfiguredPda(context, inputs.program, inputs.asset)[0], - isSigner: e.isSigner, - isWritable: e.isWritable, }; case 'CustomPda': if (!inputs.program) throw new Error('Program address is required'); @@ -129,14 +135,12 @@ export function extraAccountToAccountMeta( } }) )[0], - isSigner: e.isSigner, - isWritable: e.isWritable, + ...acccountMeta, }; case 'Address': return { + ...acccountMeta, pubkey: e.address, - isSigner: e.isSigner, - isWritable: e.isWritable, }; default: throw new Error('Unknown extra account type'); @@ -144,27 +148,28 @@ export function extraAccountToAccountMeta( } export function extraAccountToBase(s: ExtraAccount): BaseExtraAccount { + const acccountMeta: Pick = { + isSigner: s.isSigner || false, + isWritable: s.isWritable || false, + }; if (s.type === 'CustomPda') { return { __kind: 'CustomPda', - isSigner: s.isSigner, - isWritable: s.isWritable, + ...acccountMeta, seeds: s.seeds.map(seedToBase), }; } if (s.type === 'Address') { return { __kind: 'Address', - isSigner: s.isSigner, - isWritable: s.isWritable, + ...acccountMeta, address: s.address, }; } return { __kind: s.type, - isSigner: s.isSigner, - isWritable: s.isWritable, + ...acccountMeta, }; } diff --git a/clients/js/src/plugins/lib.ts b/clients/js/src/plugins/lib.ts index 99795635..2254664f 100644 --- a/clients/js/src/plugins/lib.ts +++ b/clients/js/src/plugins/lib.ts @@ -26,7 +26,6 @@ import { import { royaltiesFromBase, royaltiesToBase } from './royalties'; import { masterEditionFromBase, masterEditionToBase } from './masterEdition'; - export function formPluginHeaderV1( pluginRegistryOffset: bigint ): Omit { diff --git a/clients/js/src/plugins/lifecycleChecks.ts b/clients/js/src/plugins/lifecycleChecks.ts index bcfd4064..a9d984b6 100644 --- a/clients/js/src/plugins/lifecycleChecks.ts +++ b/clients/js/src/plugins/lifecycleChecks.ts @@ -71,8 +71,7 @@ export function hookableLifecycleEventToLifecycleCheckKey( export function lifecycleChecksToBase( l: LifecycleChecks ): [HookableLifecycleEvent, ExternalCheckResult][] { - return Object - .keys(l) + return Object.keys(l) .map((key) => { const k = key as keyof LifecycleChecks; const value = l[k]; @@ -80,10 +79,14 @@ export function lifecycleChecksToBase( return [ lifecycleCheckKeyToEnum(k), checkResultsToExternalCheckResult(value), - ] + ]; } - return null - }).filter((x) => x !== null) as [HookableLifecycleEvent, ExternalCheckResult][]; + return null; + }) + .filter((x) => x !== null) as [ + HookableLifecycleEvent, + ExternalCheckResult + ][]; } export function lifecycleChecksFromBase( diff --git a/clients/js/src/plugins/masterEdition.ts b/clients/js/src/plugins/masterEdition.ts index 337b1939..96f3815c 100644 --- a/clients/js/src/plugins/masterEdition.ts +++ b/clients/js/src/plugins/masterEdition.ts @@ -1,27 +1,26 @@ -import { BaseMasterEdition } from "../generated"; -import { someOrNone, unwrapOption } from "../utils"; - +import { BaseMasterEdition } from '../generated'; +import { someOrNone, unwrapOption } from '../utils'; export type MasterEdition = { maxSupply?: number; - name?: string - uri?: string -} + name?: string; + uri?: string; +}; -export type MasterEditionArgs = MasterEdition +export type MasterEditionArgs = MasterEdition; export function masterEditionToBase(s: MasterEdition): BaseMasterEdition { return { maxSupply: someOrNone(s.maxSupply), name: someOrNone(s.name), - uri: someOrNone(s.uri) - } + uri: someOrNone(s.uri), + }; } export function masterEditionFromBase(s: BaseMasterEdition): MasterEdition { return { maxSupply: unwrapOption(s.maxSupply), name: unwrapOption(s.name), - uri: unwrapOption( s.uri) - } + uri: unwrapOption(s.uri), + }; } diff --git a/clients/js/src/plugins/oracle.ts b/clients/js/src/plugins/oracle.ts index 6ca0b2df..c904d4e5 100644 --- a/clients/js/src/plugins/oracle.ts +++ b/clients/js/src/plugins/oracle.ts @@ -1,6 +1,8 @@ +import { Context, PublicKey } from '@metaplex-foundation/umi'; import { ExtraAccount, extraAccountFromBase, + extraAccountToAccountMeta, extraAccountToBase, } from './extraAccount'; import { @@ -83,7 +85,11 @@ export function oracleUpdateInfoArgsToBase( }; } -export function oracleFromBase(s: BaseOracle, r: ExternalRegistryRecord, account: Uint8Array): Oracle { +export function oracleFromBase( + s: BaseOracle, + r: ExternalRegistryRecord, + account: Uint8Array +): Oracle { return { ...s, pda: @@ -92,6 +98,26 @@ export function oracleFromBase(s: BaseOracle, r: ExternalRegistryRecord, account }; } +export function findOracleAccount( + context: Pick, + oracle: Pick, + inputs: { + asset?: PublicKey; + collection?: PublicKey; + recipient?: PublicKey; + owner?: PublicKey; + } +): PublicKey { + if (!oracle.pda) { + return oracle.baseAddress; + } + + return extraAccountToAccountMeta(context, oracle.pda, { + ...inputs, + program: oracle.baseAddress, + }).pubkey; +} + export const oracleManifest: ExternalPluginManifest< Oracle, BaseOracle, diff --git a/clients/js/src/plugins/types.ts b/clients/js/src/plugins/types.ts index 4def5935..3ef970b6 100644 --- a/clients/js/src/plugins/types.ts +++ b/clients/js/src/plugins/types.ts @@ -74,9 +74,9 @@ export type CreatePluginArgs = data: EditionArgs; } | { - type: 'MasterEdition'; - data: BaseMasterEditionArgs - }; + type: 'MasterEdition'; + data: BaseMasterEditionArgs; + }; export type AuthorityArgsV2 = { authority?: PluginAuthority; @@ -116,8 +116,8 @@ export type AddablePluginArgsV2 = type: 'Attributes'; } & AttributesArgs) | ({ - type: 'MasterEdition' - } & MasterEditionArgs) + type: 'MasterEdition'; + } & MasterEditionArgs); export type PluginArgsV2 = AddablePluginArgsV2 | CreateOnlyPluginArgsV2; export type PluginAuthorityPairArgsV2 = PluginArgsV2 & AuthorityArgsV2; diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index b4577a19..a9f36a12 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -1,4 +1,4 @@ -import { none, Option, some } from "@metaplex-foundation/umi"; +import { none, Option, some } from '@metaplex-foundation/umi'; export type RenameField = Omit< T, @@ -29,4 +29,4 @@ export function someOrNone(value: T | undefined): Option { export function unwrapOption(value: Option): T | undefined { return value.__option === 'Some' ? value.value : undefined; -} \ No newline at end of file +} diff --git a/clients/js/test/_setupRaw.ts b/clients/js/test/_setupRaw.ts index 4b6bb075..b0a2f632 100644 --- a/clients/js/test/_setupRaw.ts +++ b/clients/js/test/_setupRaw.ts @@ -5,6 +5,7 @@ import { PublicKey, Signer, Umi, + assertAccountExists, generateSigner, publicKey, } from '@metaplex-foundation/umi'; @@ -204,3 +205,16 @@ export const assertCollection = async ( t.like(collectionWithPlugins, testObj); }; + +export const assertBurned = async ( + t: Assertions, + umi: Umi, + asset: PublicKey +) => { + const account = await umi.rpc.getAccount(asset); + t.true(account.exists); + assertAccountExists(account); + t.is(account.data.length, 1); + t.is(account.data[0], Key.Uninitialized); + return account; +}; diff --git a/clients/js/test/burn.test.ts b/clients/js/test/burn.test.ts index df366d42..17bb08c3 100644 --- a/clients/js/test/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -1,15 +1,12 @@ -import { - assertAccountExists, - generateSigner, - sol, -} from '@metaplex-foundation/umi'; +import { generateSigner, sol } from '@metaplex-foundation/umi'; import test from 'ava'; -import { burnCollectionV1, burnV1, Key, pluginAuthorityPair } from '../src'; +import { burnCollectionV1, burnV1, pluginAuthorityPair } from '../src'; import { DEFAULT_ASSET, DEFAULT_COLLECTION, assertAsset, + assertBurned, assertCollection, createAsset, createAssetWithCollection, @@ -33,12 +30,8 @@ test('it can burn an asset as the owner', async (t) => { }).sendAndConfirm(umi); // And the asset address still exists but was resized to 1. - const afterAsset = await umi.rpc.getAccount(asset.publicKey); - t.true(afterAsset.exists); - assertAccountExists(afterAsset); + const afterAsset = await assertBurned(t, umi, asset.publicKey); t.deepEqual(afterAsset.lamports, sol(0.00089784 + 0.0015)); - t.is(afterAsset.data.length, 1); - t.is(afterAsset.data[0], Key.Uninitialized); }); test('it cannot burn an asset if not the owner', async (t) => { diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index abcb1eed..ce4ab4f6 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -1,32 +1,61 @@ import test from 'ava'; -import fs from "fs"; +import fs from 'fs'; -import { mplCoreOracleExample, fixedAccountInit, fixedAccountSet } from '@metaplex-foundation/mpl-core-oracle-example' -import { createSignerFromKeypair, Context, generateSigner } from '@metaplex-foundation/umi'; +import { + mplCoreOracleExample, + fixedAccountInit, + fixedAccountSet, + preconfiguredAssetPdaInit, + MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + preconfiguredAssetPdaSet, +} from '@metaplex-foundation/mpl-core-oracle-example'; +import { + createSignerFromKeypair, + Context, + generateSigner, +} from '@metaplex-foundation/umi'; import { ExternalValidationResult } from '@metaplex-foundation/mpl-core-oracle-example/dist/src/hooked'; -import { assertAsset, createUmi as baseCreateUmi, DEFAULT_ASSET } from '../_setupRaw'; -import { createAsset } from '../_setupSdk'; -import { CheckResult, transfer, update } from '../../src'; +import { + assertAsset, + assertBurned, + createUmi as baseCreateUmi, + DEFAULT_ASSET, +} from '../_setupRaw'; +import { createAsset, createAssetWithCollection } from '../_setupSdk'; +import { + burn, + CheckResult, + create, + findOracleAccount, + OracleInitInfoArgs, + transfer, + update, +} from '../../src'; - -const createUmi = async () => (await baseCreateUmi()).use(mplCoreOracleExample()); +const createUmi = async () => + (await baseCreateUmi()).use(mplCoreOracleExample()); function loadSecretFromFile(filename: string) { const secret = JSON.parse(fs.readFileSync(filename).toString()) as number[]; const secretKey = Uint8Array.from(secret); - return secretKey + return secretKey; } -const secret = loadSecretFromFile('../../../mpl-core-oracle-example/aaa48hFxxsUJb2MUeUVe8ABH42F6nho69oXUkSgKeSM.json') +const secret = loadSecretFromFile( + '../../../mpl-core-oracle-example/aaa48hFxxsUJb2MUeUVe8ABH42F6nho69oXUkSgKeSM.json' +); function getAuthoritySigner(umi: Context) { - return createSignerFromKeypair(umi, umi.eddsa.createKeypairFromSecretKey(secret)) + return createSignerFromKeypair( + umi, + umi.eddsa.createKeypairFromSecretKey(secret) + ); } -test('it can use fixed address oracle to control update', async (t) => { +test('it can use fixed address oracle to deny update', async (t) => { const umi = await createUmi(); - const account = generateSigner(umi) + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); - const signer = getAuthoritySigner(umi) - // write to example program oracle account await fixedAccountInit(umi, { account, @@ -39,30 +68,32 @@ test('it can use fixed address oracle to control update', async (t) => { update: ExternalValidationResult.Rejected, transfer: ExternalValidationResult.Pass, burn: ExternalValidationResult.Pass, - } - } - }).sendAndConfirm(umi) + }, + }, + }).sendAndConfirm(umi); // create asset referencing the oracle account const asset = await createAsset(umi, { - plugins: [{ - type: 'Oracle', - resultsOffset: { - type: 'Anchor' - }, - lifecycleChecks: { - update: [CheckResult.CAN_DENY] + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, }, - baseAddress: account.publicKey, - }] - }) + ], + }); const result = update(umi, { asset, - name: 'new name' - }).sendAndConfirm(umi) + name: 'new name', + }).sendAndConfirm(umi); - await t.throwsAsync(result, {name: 'InvalidAuthority'}) + await t.throwsAsync(result, { name: 'InvalidAuthority' }); await fixedAccountSet(umi, { account: account.publicKey, @@ -74,30 +105,105 @@ test('it can use fixed address oracle to control update', async (t) => { update: ExternalValidationResult.Pass, transfer: ExternalValidationResult.Pass, burn: ExternalValidationResult.Pass, - } - } - }).sendAndConfirm(umi) + }, + }, + }).sendAndConfirm(umi); await update(umi, { asset, - name: 'new name 2' - }).sendAndConfirm(umi) + name: 'new name 2', + }).sendAndConfirm(umi); await assertAsset(t, umi, { ...DEFAULT_ASSET, asset: asset.publicKey, owner: umi.identity.publicKey, - name: 'new name 2' - }) + name: 'new name 2', + }); +}); + +test('it can use fixed address oracle to deny update via collection', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); -}) + // create asset referencing the oracle account + const { asset } = await createAssetWithCollection( + umi, + {}, + { + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, + }, + ], + } + ); -test('it can use fixed address oracle to control transfer', async (t) => { + const result = update(umi, { + asset, + name: 'new name', + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await fixedAccountSet(umi, { + account: account.publicKey, + signer, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await update(umi, { + asset, + name: 'new name 2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + name: 'new name 2', + }); +}); + +test('it can use fixed address oracle to deny transfer', async (t) => { const umi = await createUmi(); - const account = generateSigner(umi) + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); - const signer = getAuthoritySigner(umi) - // write to example program oracle account await fixedAccountInit(umi, { account, @@ -110,32 +216,34 @@ test('it can use fixed address oracle to control transfer', async (t) => { update: ExternalValidationResult.Pass, transfer: ExternalValidationResult.Rejected, burn: ExternalValidationResult.Pass, - } - } - }).sendAndConfirm(umi) + }, + }, + }).sendAndConfirm(umi); // create asset referencing the oracle account const asset = await createAsset(umi, { - plugins: [{ - type: 'Oracle', - resultsOffset: { - type: 'Anchor' + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, }, - lifecycleChecks: { - transfer: [CheckResult.CAN_DENY] - }, - baseAddress: account.publicKey, - }] - }) + ], + }); - const newOwner = generateSigner(umi) + const newOwner = generateSigner(umi); const result = transfer(umi, { asset, - newOwner: newOwner.publicKey - }).sendAndConfirm(umi) + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); - await t.throwsAsync(result, {name: 'InvalidAuthority'}) + await t.throwsAsync(result, { name: 'InvalidAuthority' }); await fixedAccountSet(umi, { account: account.publicKey, @@ -147,19 +255,414 @@ test('it can use fixed address oracle to control transfer', async (t) => { update: ExternalValidationResult.Pass, transfer: ExternalValidationResult.Pass, burn: ExternalValidationResult.Pass, - } - } - }).sendAndConfirm(umi) + }, + }, + }).sendAndConfirm(umi); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +}); + +test('it cannot use fixed address oracle to force approve transfer', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); + const owner = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Approved, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // create asset referencing the oracle account + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_APPROVE], + }, + baseAddress: account.publicKey, + }, + ], + }); + + const newOwner = generateSigner(umi); + + const result = transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); await transfer(umi, { asset, - newOwner: newOwner.publicKey - }).sendAndConfirm(umi) + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); await assertAsset(t, umi, { ...DEFAULT_ASSET, asset: asset.publicKey, owner: newOwner.publicKey, - }) + }); +}); -}) \ No newline at end of file +test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event but has same type', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); + const owner = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // create asset referencing the oracle account + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_APPROVE], + }, + baseAddress: account.publicKey, + }, + ], + }); + + const newOwner = generateSigner(umi); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +}); + +test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); + const owner = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // create asset referencing the oracle account + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, + }, + ], + }); + + const newOwner = generateSigner(umi); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +}); + +test('it can use fixed address oracle to deny create', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Rejected, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // create asset referencing the oracle account + const assetSigner = generateSigner(umi); + const result = create(umi, { + ...DEFAULT_ASSET, + asset: assetSigner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, + }, + ], + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + await fixedAccountSet(umi, { + account: account.publicKey, + signer, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await create(umi, { + ...DEFAULT_ASSET, + asset: assetSigner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, + }, + ], + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: assetSigner.publicKey, + owner: umi.identity.publicKey, + }); +}); + +test('it can use fixed address oracle to deny burn', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + const signer = getAuthoritySigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Rejected, + }, + }, + }).sendAndConfirm(umi); + + // create asset referencing the oracle account + const asset = await createAsset(umi, { + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + burn: [CheckResult.CAN_DENY], + }, + baseAddress: account.publicKey, + }, + ], + }); + + const result = burn(umi, { + asset, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await fixedAccountSet(umi, { + account: account.publicKey, + signer, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await burn(umi, { + asset, + }).sendAndConfirm(umi); + + await assertBurned(t, umi, asset.publicKey); +}); + +test('it can use asset pda oracle to deny update', async (t) => { + const umi = await createUmi(); + const signer = getAuthoritySigner(umi); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_DENY], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'PreconfiguredAsset', + }, + }; + + const asset = await createAsset(umi, { + plugins: [oraclePlugin], + }); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + asset: asset.publicKey, + }); + + // write to the PDA which corresponds to the asset + await preconfiguredAssetPdaInit(umi, { + account, + signer, + payer: umi.identity, + args: { + asset: asset.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const result = update(umi, { + asset, + name: 'new name', + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await preconfiguredAssetPdaSet(umi, { + account, + signer, + args: { + asset: asset.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await update(umi, { + asset, + name: 'new name 2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + name: 'new name 2', + }); +}); diff --git a/clients/js/test/plugins/asset/permanentBurn.test.ts b/clients/js/test/plugins/asset/permanentBurn.test.ts index ec7b0ae4..5270d37b 100644 --- a/clients/js/test/plugins/asset/permanentBurn.test.ts +++ b/clients/js/test/plugins/asset/permanentBurn.test.ts @@ -17,6 +17,7 @@ import { } from '../../../src'; import { assertAsset, + assertBurned, createAsset, createCollection, createUmi, @@ -39,12 +40,8 @@ test('it can burn an assets as an owner', async (t) => { asset: asset.publicKey, }).sendAndConfirm(umi); - const afterAsset = await umi.rpc.getAccount(asset.publicKey); - t.true(afterAsset.exists); - assertAccountExists(afterAsset); + const afterAsset = await assertBurned(t, umi, asset.publicKey); t.deepEqual(afterAsset.lamports, sol(0.00089784 + 0.0015)); - t.is(afterAsset.data.length, 1); - t.is(afterAsset.data[0], Key.Uninitialized); }); test('it can burn an assets as a delegate', async (t) => { diff --git a/clients/js/test/sdkv1.test.ts b/clients/js/test/sdkv1.test.ts index da2122ce..8b956e5b 100644 --- a/clients/js/test/sdkv1.test.ts +++ b/clients/js/test/sdkv1.test.ts @@ -80,7 +80,7 @@ test('it can create asset and collection with all update auth managed party plug maxSupply: 100, name: 'master', uri: 'uri master', - } + }, ], }); @@ -138,7 +138,7 @@ test('it can create asset and collection with all update auth managed party plug maxSupply: 100, name: 'master', uri: 'uri master', - } + }, }); const asset = await createAsset(umi, { @@ -706,7 +706,7 @@ test('it can update all updatable plugins on collection', async (t) => { maxSupply: 100, name: 'master', uri: 'uri master', - } + }, ], }); @@ -743,7 +743,7 @@ test('it can update all updatable plugins on collection', async (t) => { maxSupply: 200, name: 'master2', uri: 'uri master2', - } + }, ]; await Promise.all( @@ -797,7 +797,7 @@ test('it can update all updatable plugins on collection', async (t) => { maxSupply: 200, name: 'master2', uri: 'uri master2', - } + }, }); }); From a3d1117f0c197bc40c999bceb82eb59379ad1a06 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Sat, 27 Apr 2024 10:54:39 -0700 Subject: [PATCH 11/28] correclt pass in collection to test --- clients/js/test/externalPlugins/oracle.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index ce4ab4f6..af6f21df 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -145,7 +145,7 @@ test('it can use fixed address oracle to deny update via collection', async (t) }).sendAndConfirm(umi); // create asset referencing the oracle account - const { asset } = await createAssetWithCollection( + const { asset, collection } = await createAssetWithCollection( umi, {}, { @@ -166,6 +166,7 @@ test('it can use fixed address oracle to deny update via collection', async (t) const result = update(umi, { asset, + collection, name: 'new name', }).sendAndConfirm(umi); @@ -187,6 +188,7 @@ test('it can use fixed address oracle to deny update via collection', async (t) await update(umi, { asset, + collection, name: 'new name 2', }).sendAndConfirm(umi); From a710891908b9988e613357069d2755ee993ca99e Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Mon, 29 Apr 2024 23:02:36 -0700 Subject: [PATCH 12/28] rename DENY to REJECT --- clients/js/src/plugins/lifecycleChecks.ts | 6 +++--- clients/js/test/externalPlugins/oracle.test.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/clients/js/src/plugins/lifecycleChecks.ts b/clients/js/src/plugins/lifecycleChecks.ts index a9d984b6..d1b5acdb 100644 --- a/clients/js/src/plugins/lifecycleChecks.ts +++ b/clients/js/src/plugins/lifecycleChecks.ts @@ -8,7 +8,7 @@ export type LifecycleEvent = 'create' | 'update' | 'transfer' | 'burn'; export enum CheckResult { CAN_LISTEN, CAN_APPROVE, - CAN_DENY, + CAN_REJECT, } export const externalCheckResultToCheckResults = ( @@ -22,7 +22,7 @@ export const externalCheckResultToCheckResults = ( results.push(CheckResult.CAN_APPROVE); } if (check.flags & 4) { - results.push(CheckResult.CAN_DENY); + results.push(CheckResult.CAN_REJECT); } return results; }; @@ -39,7 +39,7 @@ export const checkResultsToExternalCheckResult = ( case CheckResult.CAN_APPROVE: flags |= 2; break; - case CheckResult.CAN_DENY: + case CheckResult.CAN_REJECT: flags |= 4; break; default: diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index af6f21df..59d18e63 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -81,7 +81,7 @@ test('it can use fixed address oracle to deny update', async (t) => { type: 'Anchor', }, lifecycleChecks: { - update: [CheckResult.CAN_DENY], + update: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -156,7 +156,7 @@ test('it can use fixed address oracle to deny update via collection', async (t) type: 'Anchor', }, lifecycleChecks: { - update: [CheckResult.CAN_DENY], + update: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -231,7 +231,7 @@ test('it can use fixed address oracle to deny transfer', async (t) => { type: 'Anchor', }, lifecycleChecks: { - transfer: [CheckResult.CAN_DENY], + transfer: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -423,7 +423,7 @@ test('it cannot use fixed address oracle to deny transfer if not registered for type: 'Anchor', }, lifecycleChecks: { - create: [CheckResult.CAN_DENY], + create: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -479,7 +479,7 @@ test('it can use fixed address oracle to deny create', async (t) => { type: 'Anchor', }, lifecycleChecks: { - create: [CheckResult.CAN_DENY], + create: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -511,7 +511,7 @@ test('it can use fixed address oracle to deny create', async (t) => { type: 'Anchor', }, lifecycleChecks: { - create: [CheckResult.CAN_DENY], + create: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -556,7 +556,7 @@ test('it can use fixed address oracle to deny burn', async (t) => { type: 'Anchor', }, lifecycleChecks: { - burn: [CheckResult.CAN_DENY], + burn: [CheckResult.CAN_REJECT], }, baseAddress: account.publicKey, }, @@ -600,7 +600,7 @@ test('it can use asset pda oracle to deny update', async (t) => { type: 'Anchor', }, lifecycleChecks: { - update: [CheckResult.CAN_DENY], + update: [CheckResult.CAN_REJECT], }, baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, pda: { From f0fa1b42c76510b4af3592eac830edebd79e7f7f Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:19:57 -0700 Subject: [PATCH 13/28] Remove authority and add more Oracle tests --- .../js/test/externalPlugins/oracle.test.ts | 530 ++++++++++++++++-- 1 file changed, 474 insertions(+), 56 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 59d18e63..3a32f974 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -1,5 +1,4 @@ import test from 'ava'; -import fs from 'fs'; import { mplCoreOracleExample, @@ -8,12 +7,17 @@ import { preconfiguredAssetPdaInit, MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, preconfiguredAssetPdaSet, + preconfiguredProgramPdaInit, + preconfiguredProgramPdaSet, + preconfiguredOwnerPdaInit, + preconfiguredOwnerPdaSet, + preconfiguredRecipientPdaInit, + preconfiguredRecipientPdaSet, + customPdaAllSeedsInit, + customPdaAllSeedsSet, + close, } from '@metaplex-foundation/mpl-core-oracle-example'; -import { - createSignerFromKeypair, - Context, - generateSigner, -} from '@metaplex-foundation/umi'; +import { generateSigner } from '@metaplex-foundation/umi'; import { ExternalValidationResult } from '@metaplex-foundation/mpl-core-oracle-example/dist/src/hooked'; import { assertAsset, @@ -34,32 +38,15 @@ import { const createUmi = async () => (await baseCreateUmi()).use(mplCoreOracleExample()); -function loadSecretFromFile(filename: string) { - const secret = JSON.parse(fs.readFileSync(filename).toString()) as number[]; - const secretKey = Uint8Array.from(secret); - return secretKey; -} - -const secret = loadSecretFromFile( - '../../../mpl-core-oracle-example/aaa48hFxxsUJb2MUeUVe8ABH42F6nho69oXUkSgKeSM.json' -); -function getAuthoritySigner(umi: Context) { - return createSignerFromKeypair( - umi, - umi.eddsa.createKeypairFromSecretKey(secret) - ); -} test('it can use fixed address oracle to deny update', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - const signer = getAuthoritySigner(umi); - // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -97,7 +84,7 @@ test('it can use fixed address oracle to deny update', async (t) => { await fixedAccountSet(umi, { account: account.publicKey, - signer, + signer: umi.identity, args: { oracleData: { __kind: 'V1', @@ -126,12 +113,10 @@ test('it can use fixed address oracle to deny update via collection', async (t) const umi = await createUmi(); const account = generateSigner(umi); - const signer = getAuthoritySigner(umi); - // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -174,7 +159,7 @@ test('it can use fixed address oracle to deny update via collection', async (t) await fixedAccountSet(umi, { account: account.publicKey, - signer, + signer: umi.identity, args: { oracleData: { __kind: 'V1', @@ -204,12 +189,10 @@ test('it can use fixed address oracle to deny transfer', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - const signer = getAuthoritySigner(umi); - // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -249,7 +232,7 @@ test('it can use fixed address oracle to deny transfer', async (t) => { await fixedAccountSet(umi, { account: account.publicKey, - signer, + signer: umi.identity, args: { oracleData: { __kind: 'V1', @@ -276,14 +259,12 @@ test('it can use fixed address oracle to deny transfer', async (t) => { test('it cannot use fixed address oracle to force approve transfer', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - - const signer = getAuthoritySigner(umi); const owner = generateSigner(umi); // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -338,14 +319,12 @@ test('it cannot use fixed address oracle to force approve transfer', async (t) = test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event but has same type', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - - const signer = getAuthoritySigner(umi); const owner = generateSigner(umi); // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -393,14 +372,12 @@ test('it cannot use fixed address oracle to deny transfer if not registered for test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - - const signer = getAuthoritySigner(umi); const owner = generateSigner(umi); // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -449,12 +426,10 @@ test('it can use fixed address oracle to deny create', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - const signer = getAuthoritySigner(umi); - // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -489,7 +464,7 @@ test('it can use fixed address oracle to deny create', async (t) => { await t.throwsAsync(result, { name: 'InvalidAuthority' }); await fixedAccountSet(umi, { account: account.publicKey, - signer, + signer: umi.identity, args: { oracleData: { __kind: 'V1', @@ -529,12 +504,10 @@ test('it can use fixed address oracle to deny burn', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); - const signer = getAuthoritySigner(umi); - // write to example program oracle account await fixedAccountInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { oracleData: { @@ -571,8 +544,260 @@ test('it can use fixed address oracle to deny burn', async (t) => { await fixedAccountSet(umi, { account: account.publicKey, - signer, + signer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await burn(umi, { + asset, + }).sendAndConfirm(umi); + + await assertBurned(t, umi, asset.publicKey); +}); + +test('it can use preconfigured program pda oracle to deny update', async (t) => { + const umi = await createUmi(); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'PreconfiguredProgram', + }, + }; + + const asset = await createAsset(umi, { + plugins: [oraclePlugin], + }); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, {}); + + // Need to close program account from previous test runs on same amman/validator session. + await close(umi, { + account, + signer: umi.identity, + payer: umi.identity, + }).sendAndConfirm(umi); + + // write to the PDA which corresponds to the asset + await preconfiguredProgramPdaInit(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 result = update(umi, { + asset, + name: 'new name', + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await preconfiguredProgramPdaSet(umi, { + account, + signer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await update(umi, { + asset, + name: 'new name 2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + name: 'new name 2', + }); +}); + +test('it can use preconfigured collection pda oracle to deny update', async (t) => { + const umi = await createUmi(); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'PreconfiguredCollection', + }, + }; + + const { asset, collection } = await createAssetWithCollection( + umi, + { + plugins: [oraclePlugin], + }, + {} + ); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + collection: collection.publicKey, + }); + + // write to the PDA which corresponds to the asset + await preconfiguredAssetPdaInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + additionalPubkey: collection.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const result = update(umi, { + asset, + collection, + name: 'new name', + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await preconfiguredAssetPdaSet(umi, { + account, + signer: umi.identity, + args: { + additionalPubkey: collection.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await update(umi, { + asset, + collection, + name: 'new name 2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + owner: umi.identity.publicKey, + name: 'new name 2', + }); +}); + +test('it can use preconfigured owner pda oracle to deny burn', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + burn: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'PreconfiguredOwner', + }, + }; + + const asset = await createAsset(umi, { + owner, + plugins: [oraclePlugin], + }); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + owner: owner.publicKey, + }); + + // write to the PDA which corresponds to the asset + await preconfiguredOwnerPdaInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, args: { + additionalPubkey: owner.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Rejected, + }, + }, + }).sendAndConfirm(umi); + + // ******************** + // ******************** + // TODO: + // Error { + // message: 'Owner address is required', + // } + // ******************** + // ******************** + const result = burn(umi, { + asset, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + }); + + await preconfiguredOwnerPdaSet(umi, { + account, + signer: umi.identity, + args: { + additionalPubkey: owner.publicKey, oracleData: { __kind: 'V1', create: ExternalValidationResult.Pass, @@ -590,9 +815,86 @@ test('it can use fixed address oracle to deny burn', async (t) => { await assertBurned(t, umi, asset.publicKey); }); -test('it can use asset pda oracle to deny update', async (t) => { +test('it can use preconfigured recipient pda oracle to deny transfer', async (t) => { + const umi = await createUmi(); + const newOwner = generateSigner(umi); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'PreconfiguredRecipient', + }, + }; + + const asset = await createAsset(umi, { + plugins: [oraclePlugin], + }); + + // Find the oracle PDA for the new owner. + const account = findOracleAccount(umi, oraclePlugin, { + recipient: newOwner.publicKey, + }); + + // write to the PDA which corresponds to the new owner. + await preconfiguredRecipientPdaInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + additionalPubkey: newOwner.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const result = transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await preconfiguredRecipientPdaSet(umi, { + account, + signer: umi.identity, + args: { + additionalPubkey: newOwner.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +}); + +test('it can use preconfigured asset pda oracle to deny update', async (t) => { const umi = await createUmi(); - const signer = getAuthoritySigner(umi); const oraclePlugin: OracleInitInfoArgs = { type: 'Oracle', @@ -620,10 +922,10 @@ test('it can use asset pda oracle to deny update', async (t) => { // write to the PDA which corresponds to the asset await preconfiguredAssetPdaInit(umi, { account, - signer, + signer: umi.identity, payer: umi.identity, args: { - asset: asset.publicKey, + additionalPubkey: asset.publicKey, oracleData: { __kind: 'V1', create: ExternalValidationResult.Pass, @@ -643,9 +945,9 @@ test('it can use asset pda oracle to deny update', async (t) => { await preconfiguredAssetPdaSet(umi, { account, - signer, + signer: umi.identity, args: { - asset: asset.publicKey, + additionalPubkey: asset.publicKey, oracleData: { __kind: 'V1', create: ExternalValidationResult.Pass, @@ -668,3 +970,119 @@ test('it can use asset pda oracle to deny update', async (t) => { name: 'new name 2', }); }); + +test('it can use custom pda oracle to deny transfer', async (t) => { + const umi = await createUmi(); + const seedPubkey = generateSigner(umi).publicKey; + const owner = generateSigner(umi); + const newOwner = generateSigner(umi); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'CustomPda', + seeds: [ + { type: 'Collection' }, + { type: 'Owner' }, + { type: 'Recipient' }, + { type: 'Asset' }, + { type: 'Address', pubkey: seedPubkey }, + { + type: 'Bytes', + bytes: Buffer.from('example-seed-bytes', 'utf8'), + }, + ], + }, + }; + + const { asset, collection } = await createAssetWithCollection( + umi, + { + owner, + plugins: [oraclePlugin], + }, + {} + ); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + collection: collection.publicKey, + owner: owner.publicKey, + recipient: newOwner.publicKey, + asset: asset.publicKey, + // ******************** + // ******************** + // TODO: Need to specify more seeds + // ******************** + // ******************** + //address: seedPubkey, + //bytes: Buffer.from('example-seed-bytes', 'utf8'), + }); + + // write to the PDA which corresponds to the asset + await customPdaAllSeedsInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + collection: collection.publicKey, + owner: owner.publicKey, + recipient: newOwner.publicKey, + asset: asset.publicKey, + address: seedPubkey, + bytes: Buffer.from('example-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const result = transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await customPdaAllSeedsSet(umi, { + account, + signer: umi.identity, + args: { + collection: collection.publicKey, + owner: owner.publicKey, + recipient: newOwner.publicKey, + asset: asset.publicKey, + address: seedPubkey, + bytes: Buffer.from('example-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +}); From 0623927b1be97c0ae6745081d9e67ea316264dc5 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:14:11 -0700 Subject: [PATCH 14/28] Add handling on account close for preconfigured program PDA --- clients/js/test/externalPlugins/oracle.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 3a32f974..b00c9ece 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -588,11 +588,19 @@ test('it can use preconfigured program pda oracle to deny update', async (t) => const account = findOracleAccount(umi, oraclePlugin, {}); // Need to close program account from previous test runs on same amman/validator session. - await close(umi, { - account, - signer: umi.identity, - payer: umi.identity, - }).sendAndConfirm(umi); + try { + await close(umi, { + account, + signer: umi.identity, + payer: umi.identity, + }).sendAndConfirm(umi); + } catch (error) { + if (error.name === 'ProgramErrorNotRecognizedError') { + // Do nothing. + } else { + throw error; + } + } // write to the PDA which corresponds to the asset await preconfiguredProgramPdaInit(umi, { From 876e1157c4868c89125df769e1e5bf70592ea9b5 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Tue, 30 Apr 2024 18:37:46 -0700 Subject: [PATCH 15/28] fix burn auth to be writable, add test, fix extra account derive --- .../instructions/burnCollectionV1.ts | 2 +- .../js/src/generated/instructions/burnV1.ts | 2 +- clients/js/src/plugins/externalPlugins.ts | 10 ++---- clients/js/src/plugins/extraAccount.ts | 2 ++ clients/js/test/burn.test.ts | 32 ++++++++++++++----- .../js/test/externalPlugins/oracle.test.ts | 19 ++++------- clients/js/test/info.test.ts | 2 +- .../instructions/burn_collection_v1.rs | 8 ++--- .../src/generated/instructions/burn_v1.rs | 8 ++--- idls/mpl_core.json | 4 +-- programs/mpl-core/src/instruction.rs | 4 +-- 11 files changed, 50 insertions(+), 43 deletions(-) diff --git a/clients/js/src/generated/instructions/burnCollectionV1.ts b/clients/js/src/generated/instructions/burnCollectionV1.ts index 1aeff52f..94be0b72 100644 --- a/clients/js/src/generated/instructions/burnCollectionV1.ts +++ b/clients/js/src/generated/instructions/burnCollectionV1.ts @@ -108,7 +108,7 @@ export function burnCollectionV1( }, authority: { index: 2, - isWritable: false as boolean, + isWritable: true as boolean, value: input.authority ?? null, }, logWrapper: { diff --git a/clients/js/src/generated/instructions/burnV1.ts b/clients/js/src/generated/instructions/burnV1.ts index f4c7bec0..32cc9490 100644 --- a/clients/js/src/generated/instructions/burnV1.ts +++ b/clients/js/src/generated/instructions/burnV1.ts @@ -114,7 +114,7 @@ export function burnV1( }, authority: { index: 3, - isWritable: false as boolean, + isWritable: true as boolean, value: input.authority ?? null, }, systemProgram: { diff --git a/clients/js/src/plugins/externalPlugins.ts b/clients/js/src/plugins/externalPlugins.ts index 80a1e355..4981dfb9 100644 --- a/clients/js/src/plugins/externalPlugins.ts +++ b/clients/js/src/plugins/externalPlugins.ts @@ -190,17 +190,14 @@ export const findExtraAccounts = ( } ): AccountMeta[] => { const accounts: AccountMeta[] = []; - const { asset, collection, owner, recipient } = inputs; externalPlugins.oracles?.forEach((oracle) => { if (oracle.lifecycleChecks?.[lifecycle]) { if (oracle.pda) { accounts.push( extraAccountToAccountMeta(context, oracle.pda, { + ...inputs, program: oracle.baseAddress, - asset, - collection, - recipient, }) ); } else { @@ -224,11 +221,8 @@ export const findExtraAccounts = ( hook.extraAccounts?.forEach((extra) => { accounts.push( extraAccountToAccountMeta(context, extra, { + ...inputs, program: hook.hookedProgram, - asset, - collection, - recipient, - owner, }) ); }); diff --git a/clients/js/src/plugins/extraAccount.ts b/clients/js/src/plugins/extraAccount.ts index 7900195d..b5a8de9d 100644 --- a/clients/js/src/plugins/extraAccount.ts +++ b/clients/js/src/plugins/extraAccount.ts @@ -129,6 +129,8 @@ export function extraAccountToAccountMeta( return publicKeySerializer().serialize(seed.pubkey); case 'Bytes': return seed.bytes; + default: + throw new Error('Unknown seed type'); } }) )[0], diff --git a/clients/js/test/burn.test.ts b/clients/js/test/burn.test.ts index 17bb08c3..c272cf63 100644 --- a/clients/js/test/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -1,6 +1,7 @@ import { generateSigner, sol } from '@metaplex-foundation/umi'; import test from 'ava'; +import { generateSignerWithSol } from '@metaplex-foundation/umi-bundle-tests'; import { burnCollectionV1, burnV1, pluginAuthorityPair } from '../src'; import { DEFAULT_ASSET, @@ -15,7 +16,6 @@ import { } from './_setupRaw'; test('it can burn an asset as the owner', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const asset = await createAsset(umi); await assertAsset(t, umi, { @@ -35,7 +35,6 @@ test('it can burn an asset as the owner', async (t) => { }); test('it cannot burn an asset if not the owner', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const attacker = generateSigner(umi); @@ -62,7 +61,6 @@ test('it cannot burn an asset if not the owner', async (t) => { }); test('it cannot burn an asset if it is frozen', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const asset = await createAsset(umi, { @@ -107,7 +105,6 @@ test('it cannot burn an asset if it is frozen', async (t) => { }); test('it cannot burn asset in collection if no collection specified', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const { asset, collection } = await createAssetWithCollection(umi, {}); @@ -126,7 +123,6 @@ test('it cannot burn asset in collection if no collection specified', async (t) }); test('it cannot burn an asset if collection permanently frozen', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const { asset, collection } = await createAssetWithCollection( @@ -175,7 +171,6 @@ test('it cannot burn an asset if collection permanently frozen', async (t) => { }); test('it cannot use an invalid system program for assets', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const asset = await createAsset(umi); const fakeSystemProgram = generateSigner(umi); @@ -195,7 +190,6 @@ test('it cannot use an invalid system program for assets', async (t) => { }); test('it cannot use an invalid noop program for assets', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const asset = await createAsset(umi); const fakeLogWrapper = generateSigner(umi); @@ -215,7 +209,6 @@ test('it cannot use an invalid noop program for assets', async (t) => { }); test('it cannot use an invalid noop program for collections', async (t) => { - // Given a Umi instance and a new signer. const umi = await createUmi(); const collection = await createCollection(umi); const fakeLogWrapper = generateSigner(umi); @@ -233,3 +226,26 @@ test('it cannot use an invalid noop program for collections', async (t) => { await t.throwsAsync(result, { name: 'InvalidLogWrapperProgram' }); }); + +test('it can burn using owner authority', async (t) => { + const umi = await createUmi(); + const owner = await generateSignerWithSol(umi); + const asset = await createAsset(umi, { + owner: owner.publicKey, + }); + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await burnV1(umi, { + asset: asset.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + // And the asset address still exists but was resized to 1. + const afterAsset = await assertBurned(t, umi, asset.publicKey); + t.deepEqual(afterAsset.lamports, sol(0.00089784 + 0.0015)); +}); diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index b00c9ece..eff99388 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -19,6 +19,7 @@ import { } from '@metaplex-foundation/mpl-core-oracle-example'; import { generateSigner } from '@metaplex-foundation/umi'; import { ExternalValidationResult } from '@metaplex-foundation/mpl-core-oracle-example/dist/src/hooked'; +import { generateSignerWithSol } from '@metaplex-foundation/umi-bundle-tests'; import { assertAsset, assertBurned, @@ -739,7 +740,7 @@ test('it can use preconfigured collection pda oracle to deny update', async (t) test('it can use preconfigured owner pda oracle to deny burn', async (t) => { const umi = await createUmi(); - const owner = generateSigner(umi); + const owner = await generateSignerWithSol(umi); const oraclePlugin: OracleInitInfoArgs = { type: 'Oracle', resultsOffset: { @@ -781,16 +782,9 @@ test('it can use preconfigured owner pda oracle to deny burn', async (t) => { }, }).sendAndConfirm(umi); - // ******************** - // ******************** - // TODO: - // Error { - // message: 'Owner address is required', - // } - // ******************** - // ******************** const result = burn(umi, { asset, + authority: owner, }).sendAndConfirm(umi); await t.throwsAsync(result, { name: 'InvalidAuthority' }); @@ -798,7 +792,7 @@ test('it can use preconfigured owner pda oracle to deny burn', async (t) => { await assertAsset(t, umi, { ...DEFAULT_ASSET, asset: asset.publicKey, - owner: umi.identity.publicKey, + owner, }); await preconfiguredOwnerPdaSet(umi, { @@ -818,6 +812,7 @@ test('it can use preconfigured owner pda oracle to deny burn', async (t) => { await burn(umi, { asset, + authority: owner, }).sendAndConfirm(umi); await assertBurned(t, umi, asset.publicKey); @@ -1030,8 +1025,8 @@ test('it can use custom pda oracle to deny transfer', async (t) => { // TODO: Need to specify more seeds // ******************** // ******************** - //address: seedPubkey, - //bytes: Buffer.from('example-seed-bytes', 'utf8'), + // address: seedPubkey, + // bytes: Buffer.from('example-seed-bytes', 'utf8'), }); // write to the PDA which corresponds to the asset diff --git a/clients/js/test/info.test.ts b/clients/js/test/info.test.ts index 692eaca9..ff051e4f 100644 --- a/clients/js/test/info.test.ts +++ b/clients/js/test/info.test.ts @@ -46,7 +46,7 @@ test.skip('fetch account info for ledger state', async (t) => { // Print the size of the account. const account = await umi.rpc.getAccount(assetAddress.publicKey); if (account.exists) { - console.log(`Account Size ${account.data.length} bytes`); + // console.log(`Account Size ${account.data.length} bytes`); } // Then an account was created with the correct data. diff --git a/clients/rust/src/generated/instructions/burn_collection_v1.rs b/clients/rust/src/generated/instructions/burn_collection_v1.rs index 6bcc5032..65d86f20 100644 --- a/clients/rust/src/generated/instructions/burn_collection_v1.rs +++ b/clients/rust/src/generated/instructions/burn_collection_v1.rs @@ -43,7 +43,7 @@ impl BurnCollectionV1 { self.payer, true, )); if let Some(authority) = self.authority { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( + accounts.push(solana_program::instruction::AccountMeta::new( authority, true, )); } else { @@ -99,7 +99,7 @@ pub struct BurnCollectionV1InstructionArgs { /// /// 0. `[writable]` collection /// 1. `[writable, signer]` payer -/// 2. `[signer, optional]` authority +/// 2. `[writable, signer, optional]` authority /// 3. `[optional]` log_wrapper #[derive(Default)] pub struct BurnCollectionV1Builder { @@ -270,7 +270,7 @@ impl<'a, 'b> BurnCollectionV1Cpi<'a, 'b> { true, )); if let Some(authority) = self.authority { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( + accounts.push(solana_program::instruction::AccountMeta::new( *authority.key, true, )); @@ -335,7 +335,7 @@ impl<'a, 'b> BurnCollectionV1Cpi<'a, 'b> { /// /// 0. `[writable]` collection /// 1. `[writable, signer]` payer -/// 2. `[signer, optional]` authority +/// 2. `[writable, signer, optional]` authority /// 3. `[optional]` log_wrapper pub struct BurnCollectionV1CpiBuilder<'a, 'b> { instruction: Box>, diff --git a/clients/rust/src/generated/instructions/burn_v1.rs b/clients/rust/src/generated/instructions/burn_v1.rs index 803c102e..02d1dcde 100644 --- a/clients/rust/src/generated/instructions/burn_v1.rs +++ b/clients/rust/src/generated/instructions/burn_v1.rs @@ -56,7 +56,7 @@ impl BurnV1 { self.payer, true, )); if let Some(authority) = self.authority { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( + accounts.push(solana_program::instruction::AccountMeta::new( authority, true, )); } else { @@ -124,7 +124,7 @@ pub struct BurnV1InstructionArgs { /// 0. `[writable]` asset /// 1. `[writable, optional]` collection /// 2. `[writable, signer]` payer -/// 3. `[signer, optional]` authority +/// 3. `[writable, signer, optional]` authority /// 4. `[optional]` system_program /// 5. `[optional]` log_wrapper #[derive(Default)] @@ -338,7 +338,7 @@ impl<'a, 'b> BurnV1Cpi<'a, 'b> { true, )); if let Some(authority) = self.authority { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( + accounts.push(solana_program::instruction::AccountMeta::new( *authority.key, true, )); @@ -421,7 +421,7 @@ impl<'a, 'b> BurnV1Cpi<'a, 'b> { /// 0. `[writable]` asset /// 1. `[writable, optional]` collection /// 2. `[writable, signer]` payer -/// 3. `[signer, optional]` authority +/// 3. `[writable, signer, optional]` authority /// 4. `[optional]` system_program /// 5. `[optional]` log_wrapper pub struct BurnV1CpiBuilder<'a, 'b> { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index f906e7c9..9b5b14c9 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -803,7 +803,7 @@ }, { "name": "authority", - "isMut": false, + "isMut": true, "isSigner": true, "isOptional": true, "docs": [ @@ -863,7 +863,7 @@ }, { "name": "authority", - "isMut": false, + "isMut": true, "isSigner": true, "isOptional": true, "docs": [ diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index 4ab37ac1..ca1352cf 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -127,7 +127,7 @@ pub(crate) enum MplAssetInstruction { #[account(0, writable, name="asset", desc = "The address of the asset")] #[account(1, optional, writable, name="collection", desc = "The collection to which the asset belongs")] #[account(2, writable, signer, name="payer", desc = "The account paying for the storage fees")] - #[account(3, optional, signer, name="authority", desc = "The owner or delegate of the asset")] + #[account(3, optional, writable, signer, name="authority", desc = "The owner or delegate of the asset")] #[account(4, optional, name="system_program", desc = "The system program")] #[account(5, optional, name="log_wrapper", desc = "The SPL Noop Program")] BurnV1(BurnV1Args), @@ -135,7 +135,7 @@ pub(crate) enum MplAssetInstruction { /// Burn an mpl-core. #[account(0, writable, name="collection", desc = "The address of the asset")] #[account(1, writable, signer, name="payer", desc = "The account paying for the storage fees")] - #[account(2, optional, signer, name="authority", desc = "The owner or delegate of the asset")] + #[account(2, optional, writable, signer, name="authority", desc = "The owner or delegate of the asset")] #[account(3, optional, name="log_wrapper", desc = "The SPL Noop Program")] BurnCollectionV1(BurnCollectionV1Args), From 112e98f699c44e91ce3b1de8764c86dc1a3ad42b Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Tue, 30 Apr 2024 18:43:52 -0700 Subject: [PATCH 16/28] pass in collection and owner auth to transfer --- clients/js/test/externalPlugins/oracle.test.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index eff99388..c7b39ff9 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -1020,13 +1020,6 @@ test('it can use custom pda oracle to deny transfer', async (t) => { owner: owner.publicKey, recipient: newOwner.publicKey, asset: asset.publicKey, - // ******************** - // ******************** - // TODO: Need to specify more seeds - // ******************** - // ******************** - // address: seedPubkey, - // bytes: Buffer.from('example-seed-bytes', 'utf8'), }); // write to the PDA which corresponds to the asset @@ -1053,7 +1046,9 @@ test('it can use custom pda oracle to deny transfer', async (t) => { const result = transfer(umi, { asset, + collection, newOwner: newOwner.publicKey, + authority: owner, }).sendAndConfirm(umi); await t.throwsAsync(result, { name: 'InvalidAuthority' }); @@ -1080,7 +1075,9 @@ test('it can use custom pda oracle to deny transfer', async (t) => { await transfer(umi, { asset, + collection, newOwner: newOwner.publicKey, + authority: owner, }).sendAndConfirm(umi); await assertAsset(t, umi, { From 901a8c908e912ebd24db4f5c6c5820b5380b0905 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Wed, 1 May 2024 14:07:06 -0700 Subject: [PATCH 17/28] use devnet oracle and real npm js package --- clients/js/pnpm-lock.yaml | 13 +++ clients/js/src/helpers/lifecycle.ts | 6 ++ clients/js/src/plugins/index.ts | 1 + configs/scripts/program/build.sh | 5 +- .../scripts/program/dump_oracle_example.sh | 84 +++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100755 configs/scripts/program/dump_oracle_example.sh diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index 211d6cd7..1b3f2115 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -9,6 +9,9 @@ devDependencies: '@ava/typescript': specifier: ^3.0.1 version: 3.0.1 + '@metaplex-foundation/mpl-core-oracle-example': + specifier: ^0.0.1 + version: 0.0.1(@metaplex-foundation/umi@0.8.10)(@noble/hashes@1.3.1) '@metaplex-foundation/mpl-toolbox': specifier: ^0.8.0 version: 0.8.0(@metaplex-foundation/umi@0.8.10) @@ -1857,6 +1860,16 @@ packages: - supports-color dev: true + /@metaplex-foundation/mpl-core-oracle-example@0.0.1(@metaplex-foundation/umi@0.8.10)(@noble/hashes@1.3.1): + resolution: {integrity: sha512-z2432DCY6eqPSUbMAqHNpZoN2keDu9kLo5tr0d/Kx7GbZ3LHIg7tiyber9gtIUJAJx3c0k3DX/awoA9Fd5kbdA==} + peerDependencies: + '@metaplex-foundation/umi': '>=0.8.2 < 1' + '@noble/hashes': ^1.3.1 + dependencies: + '@metaplex-foundation/umi': 0.8.10 + '@noble/hashes': 1.3.1 + dev: true + /@metaplex-foundation/mpl-toolbox@0.8.0(@metaplex-foundation/umi@0.8.10): resolution: {integrity: sha512-SK1VUPU4hCaL3sozgtoVjjbZxqx2gWiRt0YTFbwEt5LAHWOlCb7J7rcrrA5XwymX4iV2bIWygYs0yz7hYyx2rg==} peerDependencies: diff --git a/clients/js/src/helpers/lifecycle.ts b/clients/js/src/helpers/lifecycle.ts index 3c08224d..48a64177 100644 --- a/clients/js/src/helpers/lifecycle.ts +++ b/clients/js/src/helpers/lifecycle.ts @@ -28,6 +28,8 @@ export function canTransfer( return true; } + // TODO check oracle + if (!isFrozen(asset, collection)) { if (dAsset.owner === authority) { return true; @@ -66,6 +68,8 @@ export function canBurn( return true; } + // TODO check oracle + if (!isFrozen(asset, collection)) { if (dAsset.owner === authority) { return true; @@ -93,5 +97,7 @@ export function canUpdate( asset: AssetV1, collection?: CollectionV1 ): boolean { + // TODO check oracle + return hasAssetUpdateAuthority(authority, asset, collection); } diff --git a/clients/js/src/plugins/index.ts b/clients/js/src/plugins/index.ts index 83417e70..ba8a63ad 100644 --- a/clients/js/src/plugins/index.ts +++ b/clients/js/src/plugins/index.ts @@ -12,3 +12,4 @@ export * from './externalPlugins'; export * from './updateAuthority'; export * from './seed'; export * from './extraAccount'; +export * from './validationResultsOffset'; diff --git a/configs/scripts/program/build.sh b/configs/scripts/program/build.sh index 576c081a..0b324b55 100755 --- a/configs/scripts/program/build.sh +++ b/configs/scripts/program/build.sh @@ -4,7 +4,10 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) OUTPUT="./programs/.bin" # saves external programs binaries to the output directory source ${SCRIPT_DIR}/dump.sh ${OUTPUT} -cp ~/src/mpl-core-oracle-example/target/deploy/mpl_core_oracle_example.so ${OUTPUT}/mpl_core_oracle_example.so + +# Oracle program used for tests and is only deployed to devnet +source ${SCRIPT_DIR}/dump_oracle_example.sh ${OUTPUT} + # go to parent folder cd $(dirname $(dirname $(dirname ${SCRIPT_DIR}))) diff --git a/configs/scripts/program/dump_oracle_example.sh b/configs/scripts/program/dump_oracle_example.sh new file mode 100755 index 00000000..b815a0b7 --- /dev/null +++ b/configs/scripts/program/dump_oracle_example.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +EXTERNAL_ID=("4RZ7RhXeL4oz4kVX5fpRfkNQ3nz1n4eruqBn2AGPQepo") +EXTERNAL_SO=("mpl_core_oracle_example.so") + +# output colours +RED() { echo $'\e[1;31m'$1$'\e[0m'; } +GRN() { echo $'\e[1;32m'$1$'\e[0m'; } +YLW() { echo $'\e[1;33m'$1$'\e[0m'; } + +CURRENT_DIR=$(pwd) +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +# go to parent folder +cd $(dirname $(dirname $(dirname $SCRIPT_DIR))) + +OUTPUT=$1 + +# oracle example program is only deployed to devnet +RPC="https://api.devnet.solana.com" + +if [ -z "$OUTPUT" ]; then + echo "missing output directory" + exit 1 +fi + +# creates the output directory if it doesn't exist +if [ ! -d ${OUTPUT} ]; then + mkdir ${OUTPUT} +fi + +# only prints this if we have external programs +if [ ${#EXTERNAL_ID[@]} -gt 0 ]; then + echo "Dumping external accounts to '${OUTPUT}':" +fi + +# copy external programs or accounts binaries from the chain +copy_from_chain() { + ACCOUNT_TYPE=`echo $1 | cut -d. -f2` + PREFIX=$2 + + case "$ACCOUNT_TYPE" in + "bin") + solana account -u $RPC ${EXTERNAL_ID[$i]} -o ${OUTPUT}/$2$1 > /dev/null + ;; + "so") + solana program dump -u $RPC ${EXTERNAL_ID[$i]} ${OUTPUT}/$2$1 > /dev/null + ;; + *) + echo $(RED "[ ERROR ] unknown account type for '$1'") + exit 1 + ;; + esac + + if [ -z "$PREFIX" ]; then + echo "Wrote account data to ${OUTPUT}/$2$1" + fi +} + +# dump external programs binaries if needed +for i in ${!EXTERNAL_ID[@]}; do + if [ ! -f "${OUTPUT}/${EXTERNAL_SO[$i]}" ]; then + copy_from_chain "${EXTERNAL_SO[$i]}" + else + copy_from_chain "${EXTERNAL_SO[$i]}" "onchain-" + + ON_CHAIN=`sha256sum -b ${OUTPUT}/onchain-${EXTERNAL_SO[$i]} | cut -d ' ' -f 1` + LOCAL=`sha256sum -b ${OUTPUT}/${EXTERNAL_SO[$i]} | cut -d ' ' -f 1` + + if [ "$ON_CHAIN" != "$LOCAL" ]; then + echo $(YLW "[ WARNING ] on-chain and local binaries are different for '${EXTERNAL_SO[$i]}'") + else + echo "$(GRN "[ SKIPPED ]") on-chain and local binaries are the same for '${EXTERNAL_SO[$i]}'" + fi + + rm ${OUTPUT}/onchain-${EXTERNAL_SO[$i]} + fi +done + +# only prints this if we have external programs +if [ ${#EXTERNAL_ID[@]} -gt 0 ]; then + echo "" +fi + +cd ${CURRENT_DIR} \ No newline at end of file From 4ae11fd9c81930057641936160eacd55943da165 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Wed, 1 May 2024 14:15:54 -0700 Subject: [PATCH 18/28] Add test for preconfigured asset pda with custom offset --- .../js/test/externalPlugins/oracle.test.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index c7b39ff9..6e452c7e 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -15,6 +15,8 @@ import { preconfiguredRecipientPdaSet, customPdaAllSeedsInit, customPdaAllSeedsSet, + preconfiguredAssetPdaCustomOffsetInit, + preconfiguredAssetPdaCustomOffsetSet, close, } from '@metaplex-foundation/mpl-core-oracle-example'; import { generateSigner } from '@metaplex-foundation/umi'; @@ -1086,3 +1088,93 @@ test('it can use custom pda oracle to deny transfer', async (t) => { owner: newOwner.publicKey, }); }); + +test('it can use preconfigured asset pda custom offset oracle to deny update', async (t) => { + const umi = await createUmi(); + + // This test uses an oracle with the data struct: + // pub struct CustomDataValidation { + // pub authority: Pubkey, + // pub sequence_num: u64, + // pub validation: OracleValidation, + // } + // + // Thus the `resultsOffset` below is set to 48. This is because the anchor discriminator, the + // `authority` `Pubkey`, and the `sequence_num` all precede the `OracleValidation` struct + // within the account: + // + // 8 (anchor discriminator) + 32 (authority) + 8 (sequence number) = 48. + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Custom', + offset: 48n, + }, + lifecycleChecks: { + update: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'PreconfiguredAsset', + }, + }; + + const asset = await createAsset(umi, { + plugins: [oraclePlugin], + }); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + asset: asset.publicKey, + }); + + const dataAuthority = generateSigner(umi); + // write to the PDA which corresponds to the asset + await preconfiguredAssetPdaCustomOffsetInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + authority: dataAuthority.publicKey, + asset: asset.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }).sendAndConfirm(umi); + + const result = update(umi, { + asset, + name: 'new name', + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await preconfiguredAssetPdaCustomOffsetSet(umi, { + account, + authority: dataAuthority, + sequenceNum: 1, + asset: asset.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }).sendAndConfirm(umi); + + await update(umi, { + asset, + name: 'new name 2', + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + name: 'new name 2', + }); +}); From 7aa75a501b3839605c08030444ef21bb22063102 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Wed, 1 May 2024 14:22:22 -0700 Subject: [PATCH 19/28] Fix typo --- clients/js/test/externalPlugins/oracle.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 6e452c7e..6c92b5a7 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -1095,8 +1095,8 @@ test('it can use preconfigured asset pda custom offset oracle to deny update', a // This test uses an oracle with the data struct: // pub struct CustomDataValidation { // pub authority: Pubkey, - // pub sequence_num: u64, - // pub validation: OracleValidation, + // pub sequence_num: u64, + // pub validation: OracleValidation, // } // // Thus the `resultsOffset` below is set to 48. This is because the anchor discriminator, the From 33e24461794bf0b4e496ea426943247e5bf8d342 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Wed, 1 May 2024 15:09:47 -0700 Subject: [PATCH 20/28] Add test for custom pda typical Add extra checks in custom offset test --- .../js/test/externalPlugins/oracle.test.ts | 156 +++++++++++++++++- 1 file changed, 152 insertions(+), 4 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 6c92b5a7..2df2dfd9 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -15,6 +15,8 @@ import { preconfiguredRecipientPdaSet, customPdaAllSeedsInit, customPdaAllSeedsSet, + customPdaTypicalInit, + customPdaTypicalSet, preconfiguredAssetPdaCustomOffsetInit, preconfiguredAssetPdaCustomOffsetSet, close, @@ -976,7 +978,7 @@ test('it can use preconfigured asset pda oracle to deny update', async (t) => { }); }); -test('it can use custom pda oracle to deny transfer', async (t) => { +test('it can use custom pda (all seeds) oracle to deny transfer', async (t) => { const umi = await createUmi(); const seedPubkey = generateSigner(umi).publicKey; const owner = generateSigner(umi); @@ -1089,6 +1091,109 @@ test('it can use custom pda oracle to deny transfer', async (t) => { }); }); +test('it can use custom pda (typical) oracle to deny transfer', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const newOwner = generateSigner(umi); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'CustomPda', + seeds: [ + { + type: 'Bytes', + bytes: Buffer.from('prefix-seed-bytes', 'utf8'), + }, + { type: 'Collection' }, + { + type: 'Bytes', + bytes: Buffer.from('additional-bytes-seed-bytes', 'utf8'), + }, + ], + }, + }; + + const { asset, collection } = await createAssetWithCollection( + umi, + { + owner, + plugins: [oraclePlugin], + }, + {} + ); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + collection: collection.publicKey, + }); + + // write to the PDA + await customPdaTypicalInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + prefixBytes: Buffer.from('prefix-seed-bytes', 'utf8'), + collection: collection.publicKey, + additionalBytes: Buffer.from('additional-bytes-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const result = transfer(umi, { + asset, + collection, + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await customPdaTypicalSet(umi, { + account, + signer: umi.identity, + args: { + prefixBytes: Buffer.from('prefix-seed-bytes', 'utf8'), + collection: collection.publicKey, + additionalBytes: Buffer.from('additional-bytes-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await transfer(umi, { + asset, + collection, + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +}); + test('it can use preconfigured asset pda custom offset oracle to deny update', async (t) => { const umi = await createUmi(); @@ -1145,17 +1250,60 @@ test('it can use preconfigured asset pda custom offset oracle to deny update', a }, }).sendAndConfirm(umi); - const result = update(umi, { + const update_result = update(umi, { asset, name: 'new name', }).sendAndConfirm(umi); - await t.throwsAsync(result, { name: 'InvalidAuthority' }); + await t.throwsAsync(update_result, { name: 'InvalidAuthority' }); + + // Making sure the incorrect authority cannot update the oracle. This is more just a test of the + // example program functionality. + const set_result = preconfiguredAssetPdaCustomOffsetSet(umi, { + account, + authority: umi.identity, + sequenceNum: 2, + asset: asset.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(set_result, { name: 'ProgramErrorNotRecognizedError' }); + + // Making sure a lower sequence number passes but does not update the oracle. This is also just + // a test of the example program functionality. + await preconfiguredAssetPdaCustomOffsetSet(umi, { + account, + authority: dataAuthority, + sequenceNum: 0, + asset: asset.publicKey, + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }).sendAndConfirm(umi); + + // Validate still cannot update the mpl-core asset because the oracle did not change. + const update_result_2 = update(umi, { + asset, + name: 'new name', + }).sendAndConfirm(umi); + + await t.throwsAsync(update_result_2, { name: 'InvalidAuthority' }); + // Oracle update that works. await preconfiguredAssetPdaCustomOffsetSet(umi, { account, authority: dataAuthority, - sequenceNum: 1, + sequenceNum: 2, asset: asset.publicKey, oracleData: { __kind: 'V1', From 58f9f5946929018e3ff62acbfb72a911e79d2c22 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Wed, 1 May 2024 15:34:38 -0700 Subject: [PATCH 21/28] merge fix --- clients/js/test/burn.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/js/test/burn.test.ts b/clients/js/test/burn.test.ts index 54460818..ef100f8a 100644 --- a/clients/js/test/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -248,6 +248,7 @@ test('it can burn using owner authority', async (t) => { // And the asset address still exists but was resized to 1. const afterAsset = await assertBurned(t, umi, asset.publicKey); t.deepEqual(afterAsset.lamports, sol(0.00089784 + 0.0015)); +}) test('it cannot burn an asset with the wrong collection specified', async (t) => { // Given a Umi instance and a new signer. @@ -262,4 +263,4 @@ test('it cannot burn an asset with the wrong collection specified', async (t) => }).sendAndConfirm(umi); await t.throwsAsync(result, { name: 'InvalidCollection' }); -}); +}) From 1d225884ec1ccca0bff2c4a66b0d4c0dc4f98dfd Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Wed, 1 May 2024 15:44:20 -0700 Subject: [PATCH 22/28] formatting --- clients/js/test/burn.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/js/test/burn.test.ts b/clients/js/test/burn.test.ts index ef100f8a..d1c314de 100644 --- a/clients/js/test/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -248,7 +248,7 @@ test('it can burn using owner authority', async (t) => { // And the asset address still exists but was resized to 1. const afterAsset = await assertBurned(t, umi, asset.publicKey); t.deepEqual(afterAsset.lamports, sol(0.00089784 + 0.0015)); -}) +}); test('it cannot burn an asset with the wrong collection specified', async (t) => { // Given a Umi instance and a new signer. @@ -263,4 +263,4 @@ test('it cannot burn an asset with the wrong collection specified', async (t) => }).sendAndConfirm(umi); await t.throwsAsync(result, { name: 'InvalidCollection' }); -}) +}); From b453af16e377ce7cf9469c26bb69295d4d995480 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Wed, 1 May 2024 19:02:10 -0700 Subject: [PATCH 23/28] add validate lifecycle helper methods, fix deserialization bug, add more tests --- clients/js/src/helpers/lifecycle.ts | 340 +++++++++- clients/js/src/hooked/pluginRegistryV1Data.ts | 7 +- clients/js/src/plugins/extraAccount.ts | 109 +++- clients/js/src/plugins/oracle.ts | 16 + .../js/test/externalPlugins/oracle.test.ts | 12 +- clients/js/test/helps/lifecycle.test.ts | 602 ++++++++++++++++-- 6 files changed, 970 insertions(+), 116 deletions(-) diff --git a/clients/js/src/helpers/lifecycle.ts b/clients/js/src/helpers/lifecycle.ts index 48a64177..5ed74ec9 100644 --- a/clients/js/src/helpers/lifecycle.ts +++ b/clients/js/src/helpers/lifecycle.ts @@ -1,12 +1,23 @@ -import { PublicKey } from '@metaplex-foundation/umi'; -import { AssetV1, CollectionV1, PluginType } from '../generated'; +import { Context, PublicKey } from '@metaplex-foundation/umi'; +import { + AssetV1, + CollectionV1, + ExternalValidationResult, + PluginType, +} from '../generated'; import { deriveAssetPlugins, isFrozen } from './state'; import { checkPluginAuthorities } from './plugin'; import { hasAssetUpdateAuthority } from './authority'; +import { + CheckResult, + deserializeOracleValidation, + findOracleAccount, + getExtraAccountRequiredInputs, +} from '../plugins'; /** * Check if the given authority is eligible to transfer the asset. - * This does NOT check if the asset's roylaty rule sets. + * This does NOT check if the asset's royalty rule sets or external plugins. Use `validateTransfer` for more comprehensive checks. * @param {PublicKey | string} authority Pubkey * @param {AssetV1} asset Asset * @param {CollectionV1 | undefined} collection Collection @@ -18,6 +29,8 @@ export function canTransfer( collection?: CollectionV1 ): boolean { const dAsset = deriveAssetPlugins(asset, collection); + + // Permanent plugins have force approve powers const permaTransferDelegate = checkPluginAuthorities({ authority, pluginTypes: [PluginType.PermanentTransferDelegate], @@ -28,25 +41,134 @@ export function canTransfer( return true; } - // TODO check oracle + if (isFrozen(asset, collection)) { + return false; + } + + if (dAsset.owner === authority) { + return true; + } + const transferDelegates = checkPluginAuthorities({ + authority, + pluginTypes: [PluginType.TransferDelegate], + asset: dAsset, + collection, + }); + return transferDelegates.some((d) => d); +} + +export type ValidateTransferInput = { + authority: PublicKey | string; + asset: AssetV1; + collection?: CollectionV1; + recipient?: PublicKey; +}; - if (!isFrozen(asset, collection)) { - if (dAsset.owner === authority) { - return true; +/** + * Check if the given authority is eligible to transfer the asset and receive an error message if not. + * + * @param {Context} context Umi context + * @param {ValidateTransferInput} inputs Inputs to validate transfer + * @returns {null | string} null if value or error message + */ +export async function validateTransfer( + context: Pick, + { authority, asset, collection, recipient }: ValidateTransferInput +): Promise { + const dAsset = deriveAssetPlugins(asset, collection); + + // Permanent plugins have force approve powers + const permaTransferDelegate = checkPluginAuthorities({ + authority, + pluginTypes: [PluginType.PermanentTransferDelegate], + asset: dAsset, + collection, + }); + if (permaTransferDelegate.some((d) => d)) { + return null; + } + + if (isFrozen(asset, collection)) { + return 'Unable to transfer: asset is frozen.'; + } + + if (dAsset.oracles?.length) { + const eligibleOracles = dAsset.oracles + .filter((o) => + o.lifecycleChecks?.transfer?.includes(CheckResult.CAN_REJECT) + ) + .filter((o) => { + // there's no PDA to derive, we can check the oracle account + if (!o.pda) { + return true; + } + // If there's a recipient in the inputs, we can try to check the oracle account + if (recipient) { + return true; + } + + if (!getExtraAccountRequiredInputs(o.pda).includes('recipient')) { + return true; + } + // we skip the check if there's a recipient required but no recipient provided + // this is due how UIs generally show the availability of the transfer button before requiring the recipient address + return false; + }); + if (eligibleOracles.length) { + const accountsWithOffset = eligibleOracles.map((o) => { + const account = findOracleAccount(context, o, { + asset: asset.publicKey, + collection: collection?.publicKey, + owner: asset.owner, + recipient, + }); + + return { + pubkey: account, + offset: o.resultsOffset, + }; + }); + + const oracleValidations = ( + await context.rpc.getAccounts(accountsWithOffset.map((a) => a.pubkey)) + ).map((a, index) => { + if (a.exists) { + return deserializeOracleValidation( + a.data, + accountsWithOffset[index].offset + ); + } + return null; + }); + + const oraclePass = oracleValidations.every( + (v) => v?.transfer === ExternalValidationResult.Pass + ); + if (!oraclePass) { + return 'Unable to transfer: oracle validation failed.'; + } } - const transferDelegates = checkPluginAuthorities({ - authority, - pluginTypes: [PluginType.TransferDelegate], - asset: dAsset, - collection, - }); - return transferDelegates.some((d) => d); - } - return false; + } + + if (dAsset.owner === authority) { + return null; + } + const transferDelegates = checkPluginAuthorities({ + authority, + pluginTypes: [PluginType.TransferDelegate], + asset: dAsset, + collection, + }); + if (transferDelegates.some((d) => d)) { + return null; + } + + return 'Unable to transfer: no authority to transfer.'; } /** * Check if the given pubkey is eligible to burn the asset. + * This does NOT external plugins, use `validateBurn` for more comprehensive checks. * @param {PublicKey | string} authority Pubkey * @param {AssetV1} asset Asset * @param {CollectionV1 | undefined} collection Collection @@ -68,25 +190,120 @@ export function canBurn( return true; } - // TODO check oracle + if (isFrozen(asset, collection)) { + return false; + } + + if (dAsset.owner === authority) { + return true; + } + const burnDelegates = checkPluginAuthorities({ + authority, + pluginTypes: [PluginType.BurnDelegate], + asset, + collection, + }); + return burnDelegates.some((d) => d); +} + +export type ValidateBurnInput = { + authority: PublicKey | string; + asset: AssetV1; + collection?: CollectionV1; +}; + +/** + * Check if the given authority is eligible to burn the asset and receive an error message if not. + * + * @param {Context} context Umi context + * @param {ValidateBurnInput} inputs Inputs to validate burn + * @returns {null | string} null if value or error message + */ +export async function validateBurn( + context: Pick, + { + authority, + asset, + collection, + }: { + authority: PublicKey | string; + asset: AssetV1; + collection?: CollectionV1; + } +): Promise { + const dAsset = deriveAssetPlugins(asset, collection); + const permaBurnDelegate = checkPluginAuthorities({ + authority, + pluginTypes: [PluginType.PermanentBurnDelegate], + asset: dAsset, + collection, + }); + if (permaBurnDelegate.some((d) => d)) { + return null; + } + + if (isFrozen(asset, collection)) { + return 'Unable to burn: asset is frozen.'; + } + + if (dAsset.oracles?.length) { + const eligibleOracles = dAsset.oracles.filter((o) => + o.lifecycleChecks?.burn?.includes(CheckResult.CAN_REJECT) + ); + if (eligibleOracles.length) { + const accountsWithOffset = eligibleOracles.map((o) => { + const account = findOracleAccount(context, o, { + asset: asset.publicKey, + collection: collection?.publicKey, + owner: asset.owner, + }); + + return { + pubkey: account, + offset: o.resultsOffset, + }; + }); + + const oracleValidations = ( + await context.rpc.getAccounts(accountsWithOffset.map((a) => a.pubkey)) + ).map((a, index) => { + if (a.exists) { + return deserializeOracleValidation( + a.data, + accountsWithOffset[index].offset + ); + } + return null; + }); - if (!isFrozen(asset, collection)) { - if (dAsset.owner === authority) { - return true; + const oraclePass = oracleValidations.every( + (v) => v?.burn === ExternalValidationResult.Pass + ); + if (!oraclePass) { + return 'Unable to burn: oracle validation failed.'; + } } - const burnDelegates = checkPluginAuthorities({ - authority, - pluginTypes: [PluginType.BurnDelegate], - asset, - collection, - }); - return burnDelegates.some((d) => d); - } - return false; + } + + if (dAsset.owner === authority) { + return null; + } + const burnDelegates = checkPluginAuthorities({ + authority, + pluginTypes: [PluginType.BurnDelegate], + asset, + collection, + }); + if (burnDelegates.some((d) => d)) { + return null; + } + + return 'Unable to burn: no authority to burn.'; } /** * Check if the given pubkey is eligible to update the asset. + * This does NOT check external plugins. Use `validateUpdate` for more comprehensive checks. * @param {PublicKey | string} authority Pubkey * @param {AssetV1} asset Asset * @param {CollectionV1 | undefined} collection Collection @@ -97,7 +314,68 @@ export function canUpdate( asset: AssetV1, collection?: CollectionV1 ): boolean { - // TODO check oracle - return hasAssetUpdateAuthority(authority, asset, collection); } + +export type ValidateUpdateInput = { + authority: PublicKey | string; + asset: AssetV1; + collection?: CollectionV1; +}; + +/** + * Check if the given authority is eligible to update the asset and receive an error message if not. + * + * @param {Context} context Umi context + * @param {ValidateUpdateInput} inputs Inputs to validate update + * @returns {null | string} null if value or error message + */ +export async function validateUpdate( + context: Pick, + { authority, asset, collection }: ValidateUpdateInput +): Promise { + if (asset.oracles?.length) { + const eligibleOracles = asset.oracles.filter((o) => + o.lifecycleChecks?.update?.includes(CheckResult.CAN_REJECT) + ); + if (eligibleOracles.length) { + const accountsWithOffset = eligibleOracles.map((o) => { + const account = findOracleAccount(context, o, { + asset: asset.publicKey, + collection: collection?.publicKey, + owner: asset.owner, + }); + + return { + pubkey: account, + offset: o.resultsOffset, + }; + }); + + const oracleValidations = ( + await context.rpc.getAccounts(accountsWithOffset.map((a) => a.pubkey)) + ).map((a, index) => { + if (a.exists) { + return deserializeOracleValidation( + a.data, + accountsWithOffset[index].offset + ); + } + return null; + }); + + const oraclePass = oracleValidations.every( + (v) => v?.update === ExternalValidationResult.Pass + ); + if (!oraclePass) { + return 'Unable to update: oracle validation failed.'; + } + } + } + + if (!hasAssetUpdateAuthority(authority, asset, collection)) { + return 'Unable to update: no authority to update.'; + } + + return null; +} diff --git a/clients/js/src/hooked/pluginRegistryV1Data.ts b/clients/js/src/hooked/pluginRegistryV1Data.ts index 56e264b5..62a76836 100644 --- a/clients/js/src/hooked/pluginRegistryV1Data.ts +++ b/clients/js/src/hooked/pluginRegistryV1Data.ts @@ -138,7 +138,10 @@ export function getExternalRegistryRecordSerializer(): Serializer< buffer, pluginOffsetOffset ); - const [dataLen] = option(u64()).deserialize(buffer, dataOffsetOffset); + const [dataLen, dataLenOffset] = option(u64()).deserialize( + buffer, + dataOffsetOffset + ); return [ { pluginType, @@ -149,7 +152,7 @@ export function getExternalRegistryRecordSerializer(): Serializer< dataOffset, dataLen, }, - pluginOffsetOffset, + dataLenOffset, ]; }, }; diff --git a/clients/js/src/plugins/extraAccount.ts b/clients/js/src/plugins/extraAccount.ts index b5a8de9d..071c0a4c 100644 --- a/clients/js/src/plugins/extraAccount.ts +++ b/clients/js/src/plugins/extraAccount.ts @@ -59,72 +59,78 @@ export function extraAccountToAccountMeta( isWritable: e.isWritable || false, }; + const requiredInputs = getExtraAccountRequiredInputs(e); + const missing: string[] = []; + + requiredInputs.forEach((input) => { + if (!inputs[input]) { + missing.push(input); + } + }); + + if (missing.length) { + throw new Error( + `Missing required inputs to derive account address: ${missing.join(', ')}` + ); + } switch (e.type) { case 'PreconfiguredProgram': - if (!inputs.program) throw new Error('Program address is required'); return { ...acccountMeta, - pubkey: context.eddsa.findPda(inputs.program, [ + pubkey: context.eddsa.findPda(inputs.program!, [ string({ size: 'variable' }).serialize(PRECONFIGURED_SEED), ])[0], }; case 'PreconfiguredCollection': - if (!inputs.program) throw new Error('Program address is required'); - if (!inputs.collection) throw new Error('Collection address is required'); return { ...acccountMeta, pubkey: findPreconfiguredPda( context, - inputs.program, - inputs.collection + inputs.program!, + inputs.collection! )[0], }; case 'PreconfiguredOwner': - if (!inputs.program) throw new Error('Program address is required'); - if (!inputs.owner) throw new Error('Owner address is required'); return { ...acccountMeta, - pubkey: findPreconfiguredPda(context, inputs.program, inputs.owner)[0], + pubkey: findPreconfiguredPda( + context, + inputs.program!, + inputs.owner! + )[0], }; case 'PreconfiguredRecipient': - if (!inputs.program) throw new Error('Program address is required'); - if (!inputs.recipient) throw new Error('Recipient address is required'); return { ...acccountMeta, pubkey: findPreconfiguredPda( context, - inputs.program, - inputs.recipient + inputs.program!, + inputs.recipient! )[0], }; case 'PreconfiguredAsset': - if (!inputs.program) throw new Error('Program address is required'); - if (!inputs.asset) throw new Error('Asset address is required'); return { ...acccountMeta, - pubkey: findPreconfiguredPda(context, inputs.program, inputs.asset)[0], + pubkey: findPreconfiguredPda( + context, + inputs.program!, + inputs.asset! + )[0], }; case 'CustomPda': - if (!inputs.program) throw new Error('Program address is required'); return { pubkey: context.eddsa.findPda( - inputs.program, + inputs.program!, e.seeds.map((seed) => { switch (seed.type) { case 'Collection': - if (!inputs.collection) - throw new Error('Collection address is required'); - return publicKeySerializer().serialize(inputs.collection); + return publicKeySerializer().serialize(inputs.collection!); case 'Owner': - if (!inputs.owner) throw new Error('Owner address is required'); - return publicKeySerializer().serialize(inputs.owner); + return publicKeySerializer().serialize(inputs.owner!); case 'Recipient': - if (!inputs.recipient) - throw new Error('Recipient address is required'); - return publicKeySerializer().serialize(inputs.recipient); + return publicKeySerializer().serialize(inputs.recipient!); case 'Asset': - if (!inputs.asset) throw new Error('Asset address is required'); - return publicKeySerializer().serialize(inputs.asset); + return publicKeySerializer().serialize(inputs.asset!); case 'Address': return publicKeySerializer().serialize(seed.pubkey); case 'Bytes': @@ -196,3 +202,50 @@ export function extraAccountFromBase(s: BaseExtraAccount): ExtraAccount { isWritable: s.isWritable, }; } + +export type ExtraAccountInput = + | 'owner' + | 'recipient' + | 'asset' + | 'collection' + | 'program'; + +const EXTRA_ACCOUNT_INPUT_MAP: { + [type in ExtraAccount['type']]?: ExtraAccountInput; +} = { + PreconfiguredOwner: 'owner', + PreconfiguredRecipient: 'recipient', + PreconfiguredAsset: 'asset', + PreconfiguredCollection: 'collection', + PreconfiguredProgram: 'program', +}; + +export function getExtraAccountRequiredInputs( + s: ExtraAccount +): ExtraAccountInput[] { + const preconfigured = EXTRA_ACCOUNT_INPUT_MAP[s.type]; + if (preconfigured) { + return [preconfigured]; + } + + if (s.type === 'CustomPda') { + return s.seeds + .map((seed) => { + switch (seed.type) { + case 'Collection': + return 'collection'; + case 'Owner': + return 'owner'; + case 'Recipient': + return 'recipient'; + case 'Asset': + return 'asset'; + default: + return null; + } + }) + .filter((input) => input) as ExtraAccountInput[]; + } + + return []; +} diff --git a/clients/js/src/plugins/oracle.ts b/clients/js/src/plugins/oracle.ts index c904d4e5..9fe8b013 100644 --- a/clients/js/src/plugins/oracle.ts +++ b/clients/js/src/plugins/oracle.ts @@ -10,6 +10,8 @@ import { BaseOracleInitInfoArgs, BaseOracleUpdateInfoArgs, ExternalRegistryRecord, + getOracleValidationSerializer, + OracleValidation, } from '../generated'; import { LifecycleChecks, lifecycleChecksToBase } from './lifecycleChecks'; import { PluginAuthority, pluginAuthorityToBase } from './pluginAuthority'; @@ -118,6 +120,20 @@ export function findOracleAccount( }).pubkey; } +export function deserializeOracleValidation( + data: Uint8Array, + offset: ValidationResultsOffset +): OracleValidation { + let offs = 0; + if (offset.type === 'Custom') { + offs = Number(offset.offset); + } else if (offset.type === 'Anchor') { + offs = 8; + } + + return getOracleValidationSerializer().deserialize(data, offs)[0]; +} + export const oracleManifest: ExternalPluginManifest< Oracle, BaseOracle, diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 2df2dfd9..17a2db07 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -1250,16 +1250,16 @@ test('it can use preconfigured asset pda custom offset oracle to deny update', a }, }).sendAndConfirm(umi); - const update_result = update(umi, { + const updateResult = update(umi, { asset, name: 'new name', }).sendAndConfirm(umi); - await t.throwsAsync(update_result, { name: 'InvalidAuthority' }); + await t.throwsAsync(updateResult, { name: 'InvalidAuthority' }); // Making sure the incorrect authority cannot update the oracle. This is more just a test of the // example program functionality. - const set_result = preconfiguredAssetPdaCustomOffsetSet(umi, { + const setResult = preconfiguredAssetPdaCustomOffsetSet(umi, { account, authority: umi.identity, sequenceNum: 2, @@ -1273,7 +1273,7 @@ test('it can use preconfigured asset pda custom offset oracle to deny update', a }, }).sendAndConfirm(umi); - await t.throwsAsync(set_result, { name: 'ProgramErrorNotRecognizedError' }); + await t.throwsAsync(setResult, { name: 'ProgramErrorNotRecognizedError' }); // Making sure a lower sequence number passes but does not update the oracle. This is also just // a test of the example program functionality. @@ -1292,12 +1292,12 @@ test('it can use preconfigured asset pda custom offset oracle to deny update', a }).sendAndConfirm(umi); // Validate still cannot update the mpl-core asset because the oracle did not change. - const update_result_2 = update(umi, { + const updateResult2 = update(umi, { asset, name: 'new name', }).sendAndConfirm(umi); - await t.throwsAsync(update_result_2, { name: 'InvalidAuthority' }); + await t.throwsAsync(updateResult2, { name: 'InvalidAuthority' }); // Oracle update that works. await preconfiguredAssetPdaCustomOffsetSet(umi, { diff --git a/clients/js/test/helps/lifecycle.test.ts b/clients/js/test/helps/lifecycle.test.ts index 3b42ed1b..c46a24c9 100644 --- a/clients/js/test/helps/lifecycle.test.ts +++ b/clients/js/test/helps/lifecycle.test.ts @@ -1,16 +1,23 @@ import test from 'ava'; import { generateSigner } from '@metaplex-foundation/umi'; import { - addressPluginAuthority, + customPdaAllSeedsInit, + fixedAccountInit, + MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, +} from '@metaplex-foundation/mpl-core-oracle-example'; +import { canBurn, canTransfer, - pluginAuthorityPair, + CheckResult, + ExternalValidationResult, + findOracleAccount, + OracleInitInfoArgs, + validateBurn, + validateTransfer, + validateUpdate, } from '../../src'; -import { - createAsset, - createAssetWithCollection, - createUmi, -} from '../_setupRaw'; +import { createUmi } from '../_setupRaw'; +import { createAsset, createAssetWithCollection } from '../_setupSdk'; test('it can detect transferrable on basic asset', async (t) => { const umi = await createUmi(); @@ -21,6 +28,10 @@ test('it can detect transferrable on basic asset', async (t) => { }); t.assert(canTransfer(owner.publicKey, asset)); + t.is( + await validateTransfer(umi, { authority: owner.publicKey, asset }), + null + ); }); test('it can detect non transferrable from frozen asset', async (t) => { @@ -30,14 +41,18 @@ test('it can detect non transferrable from frozen asset', async (t) => { const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'FreezeDelegate', - data: { frozen: true }, - }), + frozen: true, + }, ], }); t.assert(!canTransfer(owner.publicKey, asset)); + t.is( + await validateTransfer(umi, { authority: owner.publicKey, asset }), + 'Unable to transfer: asset is frozen.' + ); }); test('it can detect transferrable on asset with transfer delegate', async (t) => { @@ -48,14 +63,21 @@ test('it can detect transferrable on asset with transfer delegate', async (t) => const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'TransferDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, ], }); t.assert(canTransfer(delegate.publicKey, asset)); + t.is( + await validateTransfer(umi, { authority: delegate.publicKey, asset }), + null + ); }); test('it can detect transferrable from permanent transfer', async (t) => { @@ -66,14 +88,21 @@ test('it can detect transferrable from permanent transfer', async (t) => { const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'PermanentTransferDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, ], }); t.assert(canTransfer(delegate.publicKey, asset)); + t.is( + await validateTransfer(umi, { authority: delegate.publicKey, asset }), + null + ); }); test('it can detect transferrable when frozen with permanent transfer', async (t) => { @@ -84,19 +113,30 @@ test('it can detect transferrable when frozen with permanent transfer', async (t const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'PermanentTransferDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), - pluginAuthorityPair({ + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, + { type: 'FreezeDelegate', - data: { frozen: true }, - }), + frozen: true, + }, ], }); t.assert(!canTransfer(owner.publicKey, asset)); t.assert(canTransfer(delegate.publicKey, asset)); + t.is( + await validateTransfer(umi, { authority: owner.publicKey, asset }), + 'Unable to transfer: asset is frozen.' + ); + t.is( + await validateTransfer(umi, { authority: delegate.publicKey, asset }), + null + ); }); test('it can detect transferrable when frozen with permanent collection transfer delegate', async (t) => { @@ -109,24 +149,43 @@ test('it can detect transferrable when frozen with permanent collection transfer { owner, plugins: [ - pluginAuthorityPair({ + { type: 'FreezeDelegate', - data: { frozen: true }, - }), + frozen: true, + }, ], }, { plugins: [ - pluginAuthorityPair({ + { type: 'PermanentTransferDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, ], } ); t.assert(!canTransfer(owner.publicKey, asset, collection)); t.assert(canTransfer(delegate.publicKey, asset, collection)); + t.is( + await validateTransfer(umi, { + authority: owner.publicKey, + asset, + collection, + }), + 'Unable to transfer: asset is frozen.' + ); + t.is( + await validateTransfer(umi, { + authority: delegate.publicKey, + asset, + collection, + }), + null + ); }); test('it can detect burnable on basic asset', async (t) => { @@ -138,6 +197,7 @@ test('it can detect burnable on basic asset', async (t) => { }); t.assert(canBurn(owner.publicKey, asset)); + t.is(await validateBurn(umi, { authority: owner.publicKey, asset }), null); }); test('it can detect non burnable from frozen asset', async (t) => { @@ -147,14 +207,18 @@ test('it can detect non burnable from frozen asset', async (t) => { const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'FreezeDelegate', - data: { frozen: true }, - }), + frozen: true, + }, ], }); t.assert(!canBurn(owner.publicKey, asset)); + t.is( + await validateBurn(umi, { authority: owner.publicKey, asset }), + 'Unable to burn: asset is frozen.' + ); }); test('it can detect burnable on asset with burn delegate', async (t) => { @@ -165,14 +229,18 @@ test('it can detect burnable on asset with burn delegate', async (t) => { const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'BurnDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, ], }); t.assert(canBurn(delegate.publicKey, asset)); + t.is(await validateBurn(umi, { authority: delegate.publicKey, asset }), null); }); test('it can detect burnable from permanent burn', async (t) => { @@ -183,14 +251,18 @@ test('it can detect burnable from permanent burn', async (t) => { const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'PermanentBurnDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, ], }); t.assert(canBurn(delegate.publicKey, asset)); + t.is(await validateBurn(umi, { authority: delegate.publicKey, asset }), null); }); test('it can detect burnable when frozen with permanent burn', async (t) => { @@ -201,19 +273,27 @@ test('it can detect burnable when frozen with permanent burn', async (t) => { const asset = await createAsset(umi, { owner, plugins: [ - pluginAuthorityPair({ + { type: 'PermanentBurnDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), - pluginAuthorityPair({ + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, + { type: 'FreezeDelegate', - data: { frozen: true }, - }), + frozen: true, + }, ], }); t.assert(!canBurn(owner.publicKey, asset)); t.assert(canBurn(delegate.publicKey, asset)); + t.is( + await validateBurn(umi, { authority: owner.publicKey, asset }), + 'Unable to burn: asset is frozen.' + ); + t.is(await validateBurn(umi, { authority: delegate.publicKey, asset }), null); }); test('it can detect burnable when frozen with permanent collection burn delegate', async (t) => { @@ -226,22 +306,446 @@ test('it can detect burnable when frozen with permanent collection burn delegate { owner, plugins: [ - pluginAuthorityPair({ + { type: 'FreezeDelegate', - data: { frozen: true }, - }), + frozen: true, + }, ], }, { plugins: [ - pluginAuthorityPair({ + { type: 'PermanentBurnDelegate', - authority: addressPluginAuthority(delegate.publicKey), - }), + authority: { + type: 'Address', + address: delegate.publicKey, + }, + }, ], } ); t.assert(!canBurn(owner.publicKey, asset, collection)); t.assert(canBurn(delegate.publicKey, asset, collection)); + t.is( + await validateBurn(umi, { authority: owner.publicKey, asset, collection }), + 'Unable to burn: asset is frozen.' + ); + t.is( + await validateBurn(umi, { + authority: delegate.publicKey, + asset, + collection, + }), + null + ); +}); + +test('it can validate non-transferrable asset with oracle', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oracle = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: oracle, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + baseAddress: oracle.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + }, + ], + }); + + t.is( + await validateTransfer(umi, { + authority: owner.publicKey, + asset, + }), + 'Unable to transfer: oracle validation failed.' + ); +}); + +test('it can validate transferrable asset with oracle', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oracle = generateSigner(umi); + const oracle2 = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: oracle, + 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, { + owner, + plugins: [ + { + type: 'Oracle', + baseAddress: oracle.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + }, + { + type: 'Oracle', + baseAddress: oracle2.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_REJECT], + }, + }, + ], + }); + + t.is( + await validateTransfer(umi, { + authority: owner.publicKey, + asset, + }), + null + ); +}); + +test('it can validate non-transferrable asset with oracle with recipient seed', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const seedPubkey = generateSigner(umi).publicKey; + const newOwner = generateSigner(umi); + + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'CustomPda', + seeds: [ + { type: 'Collection' }, + { type: 'Owner' }, + { type: 'Recipient' }, + { type: 'Asset' }, + { type: 'Address', pubkey: seedPubkey }, + { + type: 'Bytes', + bytes: Buffer.from('example-seed-bytes', 'utf8'), + }, + ], + }, + }; + + const { asset, collection } = await createAssetWithCollection( + umi, + { + owner, + plugins: [oraclePlugin], + }, + {} + ); + + const account = findOracleAccount(umi, oraclePlugin, { + collection: collection.publicKey, + owner: owner.publicKey, + recipient: newOwner.publicKey, + asset: asset.publicKey, + }); + + // write to example program oracle account + await customPdaAllSeedsInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + collection: collection.publicKey, + owner: owner.publicKey, + recipient: newOwner.publicKey, + asset: asset.publicKey, + address: seedPubkey, + bytes: Buffer.from('example-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + t.is( + await validateTransfer(umi, { + authority: owner.publicKey, + asset, + recipient: newOwner.publicKey, + collection, + }), + 'Unable to transfer: oracle validation failed.' + ); +}); + +test('it can validate and skip transferrable asset with oracle with recipient seed if missing recipient', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + + const { asset, collection } = await createAssetWithCollection( + umi, + { + owner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID, + pda: { + type: 'CustomPda', + seeds: [ + { + type: 'Recipient', + }, + ], + }, + }, + ], + }, + {} + ); + + t.is( + await validateTransfer(umi, { + authority: owner.publicKey, + collection, + asset, + }), + null + ); +}); + +test('it can validate non-burnable asset with oracle', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oracle = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: oracle, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Rejected, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + baseAddress: oracle.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + burn: [CheckResult.CAN_REJECT], + }, + }, + ], + }); + + t.is( + await validateBurn(umi, { + authority: owner.publicKey, + asset, + }), + 'Unable to burn: oracle validation failed.' + ); +}); + +test('it can validate burnable asset with oracle', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oracle = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: oracle, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + baseAddress: oracle.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + }, + ], + }); + + t.is( + await validateBurn(umi, { + authority: owner.publicKey, + asset, + }), + null + ); +}); + +test('it can validate non-updatable asset with oracle', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oracle = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: oracle, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Rejected, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + baseAddress: oracle.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + update: [CheckResult.CAN_REJECT], + }, + }, + ], + }); + + t.is( + await validateUpdate(umi, { + authority: owner.publicKey, + asset, + }), + 'Unable to update: oracle validation failed.' + ); +}); + +test('it can validate updatable asset with oracle', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const oracle = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: oracle, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + baseAddress: oracle.publicKey, + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + }, + ], + }); + + t.is( + await validateUpdate(umi, { + authority: umi.identity.publicKey, + asset, + }), + null + ); }); From 8256819f1cf7c69c9245951614d2d52424b09ca4 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 11:54:37 -0700 Subject: [PATCH 24/28] Test that Oracle cannot approve or listen (can only deny) --- .../js/test/externalPlugins/oracle.test.ts | 51 ++++--------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 17a2db07..6079fde4 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -261,7 +261,7 @@ test('it can use fixed address oracle to deny transfer', async (t) => { }); }); -test('it cannot use fixed address oracle to force approve transfer', async (t) => { +test('it cannot configure oracle to approve', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); @@ -282,8 +282,8 @@ test('it cannot use fixed address oracle to force approve transfer', async (t) = }, }).sendAndConfirm(umi); - // create asset referencing the oracle account - const asset = await createAsset(umi, { + // Validate cannot have Oracle with `CheckResult.CAN_APPROVE` + const result = createAsset(umi, { owner, plugins: [ { @@ -299,29 +299,10 @@ test('it cannot use fixed address oracle to force approve transfer', async (t) = ], }); - const newOwner = generateSigner(umi); - - const result = transfer(umi, { - asset, - newOwner: newOwner.publicKey, - }).sendAndConfirm(umi); - - await t.throwsAsync(result, { name: 'InvalidAuthority' }); - - await transfer(umi, { - asset, - newOwner: newOwner.publicKey, - authority: owner, - }).sendAndConfirm(umi); - - await assertAsset(t, umi, { - ...DEFAULT_ASSET, - asset: asset.publicKey, - owner: newOwner.publicKey, - }); + await t.throwsAsync(result, { name: 'InvalidExternalPluginSetting' }); }); -test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event but has same type', async (t) => { +test('it cannot configure oracle to listen', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); @@ -336,14 +317,14 @@ test('it cannot use fixed address oracle to deny transfer if not registered for __kind: 'V1', create: ExternalValidationResult.Pass, update: ExternalValidationResult.Pass, - transfer: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Approved, burn: ExternalValidationResult.Pass, }, }, }).sendAndConfirm(umi); - // create asset referencing the oracle account - const asset = await createAsset(umi, { + // Validate cannot have Oracle with `CheckResult.CAN_LISTEN` + const result = createAsset(umi, { owner, plugins: [ { @@ -352,26 +333,14 @@ test('it cannot use fixed address oracle to deny transfer if not registered for type: 'Anchor', }, lifecycleChecks: { - transfer: [CheckResult.CAN_APPROVE], + transfer: [CheckResult.CAN_LISTEN], }, baseAddress: account.publicKey, }, ], }); - const newOwner = generateSigner(umi); - - await transfer(umi, { - asset, - newOwner: newOwner.publicKey, - authority: owner, - }).sendAndConfirm(umi); - - await assertAsset(t, umi, { - ...DEFAULT_ASSET, - asset: asset.publicKey, - owner: newOwner.publicKey, - }); + await t.throwsAsync(result, { name: 'InvalidExternalPluginSetting' }); }); test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event', async (t) => { From 5bcd5d86f1b1b8e62ca5894ae583cd16a195681c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 12:07:47 -0700 Subject: [PATCH 25/28] Update error code --- clients/js/test/externalPlugins/oracle.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 6079fde4..471d954f 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -299,7 +299,7 @@ test('it cannot configure oracle to approve', async (t) => { ], }); - await t.throwsAsync(result, { name: 'InvalidExternalPluginSetting' }); + await t.throwsAsync(result, { name: 'OracleCanDenyOnly' }); }); test('it cannot configure oracle to listen', async (t) => { @@ -340,7 +340,7 @@ test('it cannot configure oracle to listen', async (t) => { ], }); - await t.throwsAsync(result, { name: 'InvalidExternalPluginSetting' }); + await t.throwsAsync(result, { name: 'OracleCanDenyOnly' }); }); test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event', async (t) => { From d22a612c984a3e42787ef843127ac24f770dfeaa Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Thu, 2 May 2024 16:13:24 -0700 Subject: [PATCH 26/28] add deprecation comment --- clients/js/src/helpers/lifecycle.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/clients/js/src/helpers/lifecycle.ts b/clients/js/src/helpers/lifecycle.ts index 5ed74ec9..5edce43a 100644 --- a/clients/js/src/helpers/lifecycle.ts +++ b/clients/js/src/helpers/lifecycle.ts @@ -17,7 +17,8 @@ import { /** * Check if the given authority is eligible to transfer the asset. - * This does NOT check if the asset's royalty rule sets or external plugins. Use `validateTransfer` for more comprehensive checks. + * This does NOT check the asset's royalty rule sets or external plugins. Use `validateTransfer` for more comprehensive checks. + * @deprecated since v1.0.0. Use `validateTransfer` instead. * @param {PublicKey | string} authority Pubkey * @param {AssetV1} asset Asset * @param {CollectionV1 | undefined} collection Collection @@ -168,7 +169,8 @@ export async function validateTransfer( /** * Check if the given pubkey is eligible to burn the asset. - * This does NOT external plugins, use `validateBurn` for more comprehensive checks. + * This does NOT check external plugins, use `validateBurn` for more comprehensive checks. + * @deprecated since v1.0.0. Use `validateBurn` instead. * @param {PublicKey | string} authority Pubkey * @param {AssetV1} asset Asset * @param {CollectionV1 | undefined} collection Collection @@ -304,6 +306,7 @@ export async function validateBurn( /** * Check if the given pubkey is eligible to update the asset. * This does NOT check external plugins. Use `validateUpdate` for more comprehensive checks. + * @deprecated since v1.0.0. Use `validateTransfer` instead. * @param {PublicKey | string} authority Pubkey * @param {AssetV1} asset Asset * @param {CollectionV1 | undefined} collection Collection From 80c500ae04ffebd5db81d92ee586c0c9df7203a3 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 21:22:24 -0700 Subject: [PATCH 27/28] Fixup use correct bit position for CanReject --- clients/rust/tests/add_external_plugins.rs | 2 +- clients/rust/tests/create_with_external_plugins.rs | 2 +- clients/rust/tests/remove_external_plugins.rs | 2 +- clients/rust/tests/update_external_plugins.rs | 2 +- programs/mpl-core/src/plugins/lifecycle.rs | 4 ++++ programs/mpl-core/src/plugins/utils.rs | 4 ++-- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/clients/rust/tests/add_external_plugins.rs b/clients/rust/tests/add_external_plugins.rs index c3c8e5a9..8e20e36e 100644 --- a/clients/rust/tests/add_external_plugins.rs +++ b/clients/rust/tests/add_external_plugins.rs @@ -150,7 +150,7 @@ async fn test_add_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 2 }, + ExternalCheckResult { flags: 4 }, )]), pda: None, results_offset: None, diff --git a/clients/rust/tests/create_with_external_plugins.rs b/clients/rust/tests/create_with_external_plugins.rs index 44b98048..248491d5 100644 --- a/clients/rust/tests/create_with_external_plugins.rs +++ b/clients/rust/tests/create_with_external_plugins.rs @@ -92,7 +92,7 @@ async fn test_create_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 2 }, + ExternalCheckResult { flags: 4 }, )]), pda: None, results_offset: None, diff --git a/clients/rust/tests/remove_external_plugins.rs b/clients/rust/tests/remove_external_plugins.rs index 85461cd0..1064f96b 100644 --- a/clients/rust/tests/remove_external_plugins.rs +++ b/clients/rust/tests/remove_external_plugins.rs @@ -127,7 +127,7 @@ async fn test_remove_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 2 }, + ExternalCheckResult { flags: 4 }, )]), pda: None, results_offset: None, diff --git a/clients/rust/tests/update_external_plugins.rs b/clients/rust/tests/update_external_plugins.rs index dd546743..5817c5df 100644 --- a/clients/rust/tests/update_external_plugins.rs +++ b/clients/rust/tests/update_external_plugins.rs @@ -140,7 +140,7 @@ async fn test_update_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 2 }, + ExternalCheckResult { flags: 4 }, )]), pda: None, results_offset: None, diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 9f7b4fc9..02033f7f 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -40,6 +40,10 @@ impl ExternalCheckResult { pub(crate) fn none() -> Self { Self { flags: 0 } } + + pub(crate) fn can_reject_only() -> Self { + Self { flags: 0x4 } + } } /// Bitfield representation of lifecycle permissions for external, third party plugins. diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 8f2801ca..7bb7b2e3 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -6,6 +6,7 @@ use solana_program::{ use crate::{ error::MplCoreError, + plugins::ExternalCheckResult, state::{AssetV1, Authority, CoreAsset, DataBlob, Key, SolanaAccount}, utils::resize_or_reallocate_account, }; @@ -333,8 +334,7 @@ pub fn initialize_external_plugin<'a, T: DataBlob + SolanaAccount>( // You cannot configure an Oracle plugin to approve lifecycle events. if let Some(lifecycle_checks) = &init_info.lifecycle_checks { for (_, result) in lifecycle_checks { - // Deny is bit 2. - if result.flags != 0x2 { + if *result != ExternalCheckResult::can_reject_only() { return Err(MplCoreError::OracleCanDenyOnly.into()); } } From 108334968c3d01cca154e1759cea1c0906ebf158 Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Thu, 2 May 2024 22:08:00 -0700 Subject: [PATCH 28/28] change validation return type --- clients/js/src/helpers/lifecycle.ts | 34 +++++++++++++++---------- clients/js/test/helps/lifecycle.test.ts | 21 +++++++-------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/clients/js/src/helpers/lifecycle.ts b/clients/js/src/helpers/lifecycle.ts index 5edce43a..f5dc93dd 100644 --- a/clients/js/src/helpers/lifecycle.ts +++ b/clients/js/src/helpers/lifecycle.ts @@ -15,6 +15,12 @@ import { getExtraAccountRequiredInputs, } from '../plugins'; +export enum LifecycleValidationError { + OracleValidationFailed = 'Oracle validation failed.', + NoAuthority = 'No authority to perform this action.', + AssetFrozen = 'Asset is frozen.', +} + /** * Check if the given authority is eligible to transfer the asset. * This does NOT check the asset's royalty rule sets or external plugins. Use `validateTransfer` for more comprehensive checks. @@ -70,12 +76,12 @@ export type ValidateTransferInput = { * * @param {Context} context Umi context * @param {ValidateTransferInput} inputs Inputs to validate transfer - * @returns {null | string} null if value or error message + * @returns {null | LifecycleValidationError} null if success or error message */ export async function validateTransfer( context: Pick, { authority, asset, collection, recipient }: ValidateTransferInput -): Promise { +): Promise { const dAsset = deriveAssetPlugins(asset, collection); // Permanent plugins have force approve powers @@ -90,7 +96,7 @@ export async function validateTransfer( } if (isFrozen(asset, collection)) { - return 'Unable to transfer: asset is frozen.'; + return LifecycleValidationError.AssetFrozen } if (dAsset.oracles?.length) { @@ -146,7 +152,7 @@ export async function validateTransfer( (v) => v?.transfer === ExternalValidationResult.Pass ); if (!oraclePass) { - return 'Unable to transfer: oracle validation failed.'; + return LifecycleValidationError.OracleValidationFailed; } } } @@ -164,7 +170,7 @@ export async function validateTransfer( return null; } - return 'Unable to transfer: no authority to transfer.'; + return LifecycleValidationError.NoAuthority; } /** @@ -219,7 +225,7 @@ export type ValidateBurnInput = { * * @param {Context} context Umi context * @param {ValidateBurnInput} inputs Inputs to validate burn - * @returns {null | string} null if value or error message + * @returns {null | LifecycleValidationError} null if success or error message */ export async function validateBurn( context: Pick, @@ -232,7 +238,7 @@ export async function validateBurn( asset: AssetV1; collection?: CollectionV1; } -): Promise { +): Promise { const dAsset = deriveAssetPlugins(asset, collection); const permaBurnDelegate = checkPluginAuthorities({ authority, @@ -245,7 +251,7 @@ export async function validateBurn( } if (isFrozen(asset, collection)) { - return 'Unable to burn: asset is frozen.'; + return LifecycleValidationError.AssetFrozen; } if (dAsset.oracles?.length) { @@ -282,7 +288,7 @@ export async function validateBurn( (v) => v?.burn === ExternalValidationResult.Pass ); if (!oraclePass) { - return 'Unable to burn: oracle validation failed.'; + return LifecycleValidationError.OracleValidationFailed; } } } @@ -300,7 +306,7 @@ export async function validateBurn( return null; } - return 'Unable to burn: no authority to burn.'; + return LifecycleValidationError.NoAuthority; } /** @@ -331,12 +337,12 @@ export type ValidateUpdateInput = { * * @param {Context} context Umi context * @param {ValidateUpdateInput} inputs Inputs to validate update - * @returns {null | string} null if value or error message + * @returns {null | LifecycleValidationError} null if success or error message */ export async function validateUpdate( context: Pick, { authority, asset, collection }: ValidateUpdateInput -): Promise { +): Promise { if (asset.oracles?.length) { const eligibleOracles = asset.oracles.filter((o) => o.lifecycleChecks?.update?.includes(CheckResult.CAN_REJECT) @@ -371,13 +377,13 @@ export async function validateUpdate( (v) => v?.update === ExternalValidationResult.Pass ); if (!oraclePass) { - return 'Unable to update: oracle validation failed.'; + return LifecycleValidationError.OracleValidationFailed; } } } if (!hasAssetUpdateAuthority(authority, asset, collection)) { - return 'Unable to update: no authority to update.'; + return LifecycleValidationError.NoAuthority; } return null; diff --git a/clients/js/test/helps/lifecycle.test.ts b/clients/js/test/helps/lifecycle.test.ts index c46a24c9..cc956bd2 100644 --- a/clients/js/test/helps/lifecycle.test.ts +++ b/clients/js/test/helps/lifecycle.test.ts @@ -11,6 +11,7 @@ import { CheckResult, ExternalValidationResult, findOracleAccount, + LifecycleValidationError, OracleInitInfoArgs, validateBurn, validateTransfer, @@ -51,7 +52,7 @@ test('it can detect non transferrable from frozen asset', async (t) => { t.assert(!canTransfer(owner.publicKey, asset)); t.is( await validateTransfer(umi, { authority: owner.publicKey, asset }), - 'Unable to transfer: asset is frozen.' + LifecycleValidationError.AssetFrozen ); }); @@ -131,7 +132,7 @@ test('it can detect transferrable when frozen with permanent transfer', async (t t.assert(canTransfer(delegate.publicKey, asset)); t.is( await validateTransfer(umi, { authority: owner.publicKey, asset }), - 'Unable to transfer: asset is frozen.' + LifecycleValidationError.AssetFrozen ); t.is( await validateTransfer(umi, { authority: delegate.publicKey, asset }), @@ -176,7 +177,7 @@ test('it can detect transferrable when frozen with permanent collection transfer asset, collection, }), - 'Unable to transfer: asset is frozen.' + LifecycleValidationError.AssetFrozen ); t.is( await validateTransfer(umi, { @@ -217,7 +218,7 @@ test('it can detect non burnable from frozen asset', async (t) => { t.assert(!canBurn(owner.publicKey, asset)); t.is( await validateBurn(umi, { authority: owner.publicKey, asset }), - 'Unable to burn: asset is frozen.' + LifecycleValidationError.AssetFrozen ); }); @@ -291,7 +292,7 @@ test('it can detect burnable when frozen with permanent burn', async (t) => { t.assert(canBurn(delegate.publicKey, asset)); t.is( await validateBurn(umi, { authority: owner.publicKey, asset }), - 'Unable to burn: asset is frozen.' + LifecycleValidationError.AssetFrozen ); t.is(await validateBurn(umi, { authority: delegate.publicKey, asset }), null); }); @@ -329,7 +330,7 @@ test('it can detect burnable when frozen with permanent collection burn delegate t.assert(canBurn(delegate.publicKey, asset, collection)); t.is( await validateBurn(umi, { authority: owner.publicKey, asset, collection }), - 'Unable to burn: asset is frozen.' + LifecycleValidationError.AssetFrozen ); t.is( await validateBurn(umi, { @@ -383,7 +384,7 @@ test('it can validate non-transferrable asset with oracle', async (t) => { authority: owner.publicKey, asset, }), - 'Unable to transfer: oracle validation failed.' + LifecycleValidationError.OracleValidationFailed ); }); @@ -520,7 +521,7 @@ test('it can validate non-transferrable asset with oracle with recipient seed', recipient: newOwner.publicKey, collection, }), - 'Unable to transfer: oracle validation failed.' + LifecycleValidationError.OracleValidationFailed ); }); @@ -608,7 +609,7 @@ test('it can validate non-burnable asset with oracle', async (t) => { authority: owner.publicKey, asset, }), - 'Unable to burn: oracle validation failed.' + LifecycleValidationError.OracleValidationFailed ); }); @@ -700,7 +701,7 @@ test('it can validate non-updatable asset with oracle', async (t) => { authority: owner.publicKey, asset, }), - 'Unable to update: oracle validation failed.' + LifecycleValidationError.OracleValidationFailed ); });