diff --git a/Cargo.lock b/Cargo.lock
index 8852eb0d..0d2d362c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5197,6 +5197,25 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "mpl-core"
+version = "0.11.0"
+source = "git+https://github.com/ivsop/mpl-core?rev=27bbab1997fdd39fd010d78406cc7ee2e7e2601d#27bbab1997fdd39fd010d78406cc7ee2e7e2601d"
+dependencies = [
+ "base64 0.22.1",
+ "borsh 0.10.4",
+ "kaigan",
+ "modular-bitfield",
+ "num-derive 0.3.3",
+ "num-traits",
+ "rmp-serde",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "solana-program",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "multimap"
version = "0.8.3"
@@ -5366,6 +5385,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "num-derive"
version = "0.4.2"
@@ -6675,6 +6705,28 @@ dependencies = [
"syn 2.0.106",
]
+[[package]]
+name = "rmp"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
+dependencies = [
+ "byteorder",
+ "num-traits",
+ "paste",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
+dependencies = [
+ "byteorder",
+ "rmp",
+ "serde",
+]
+
[[package]]
name = "rocksdb"
version = "0.23.0"
@@ -9579,7 +9631,7 @@ dependencies = [
"log 0.4.28",
"memoffset",
"num-bigint 0.4.6",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"rand 0.8.5",
"serde",
@@ -9912,7 +9964,7 @@ dependencies = [
"dialoguer 0.10.4",
"hidapi",
"log 0.4.28",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"parking_lot 0.12.4",
"qstring",
@@ -10186,7 +10238,7 @@ dependencies = [
"memmap2 0.9.8",
"mockall",
"modular-bitfield",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"num_cpus",
"num_enum",
@@ -11445,7 +11497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3"
dependencies = [
"bincode",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"serde",
"serde_derive",
@@ -11470,7 +11522,7 @@ checksum = "66631ddbe889dab5ec663294648cd1df395ec9df7a4476e7b3e095604cfdb539"
dependencies = [
"bincode",
"cfg_eval",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"serde",
"serde_derive",
@@ -11497,7 +11549,7 @@ dependencies = [
"agave-feature-set",
"bincode",
"log 0.4.28",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"serde",
"serde_derive",
@@ -11529,7 +11581,7 @@ checksum = "b2eab3cefc7a3dc06210c419fdc9da9e19a57f4198a349bfab1c56ae5f5d6278"
dependencies = [
"agave-feature-set",
"bytemuck",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"solana-instruction 3.0.0",
"solana-program-runtime",
@@ -11554,7 +11606,7 @@ dependencies = [
"itertools 0.12.1",
"js-sys",
"merlin",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"rand 0.8.5",
"serde",
@@ -11583,7 +11635,7 @@ checksum = "3175e35635af1d7227cba9e99358538d0b69af6c127bc8beb572e51cd44e3c6d"
dependencies = [
"agave-feature-set",
"bytemuck",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"solana-instruction 3.0.0",
"solana-program-runtime",
@@ -11606,7 +11658,7 @@ dependencies = [
"curve25519-dalek 4.1.3",
"itertools 0.12.1",
"merlin",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"rand 0.8.5",
"serde",
@@ -11741,7 +11793,7 @@ dependencies = [
"borsh 1.5.7",
"bytemuck",
"bytemuck_derive",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"num_enum",
"solana-program-error 3.0.0",
@@ -11759,7 +11811,7 @@ checksum = "0888304af6b3d839e435712e6c84025e09513017425ff62045b6b8c41feb77d9"
dependencies = [
"arrayref",
"bytemuck",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"num_enum",
"solana-account-info 3.0.0",
@@ -11817,7 +11869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "452d0f758af20caaa10d9a6f7608232e000d4c74462f248540b3d2ddfa419776"
dependencies = [
"bytemuck",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"num_enum",
"solana-instruction 3.0.0",
@@ -11836,7 +11888,7 @@ checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3"
dependencies = [
"arrayref",
"bytemuck",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"num_enum",
"solana-instruction 3.0.0",
@@ -11855,7 +11907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676"
dependencies = [
"borsh 1.5.7",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"solana-borsh 3.0.0",
"solana-instruction 3.0.0",
@@ -11874,7 +11926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac"
dependencies = [
"bytemuck",
- "num-derive",
+ "num-derive 0.4.2",
"num-traits",
"num_enum",
"solana-account-info 3.0.0",
@@ -12093,6 +12145,7 @@ dependencies = [
"litesvm",
"litesvm-token",
"log 0.4.28",
+ "mpl-core",
"reqwest 0.12.23",
"serde",
"serde_derive",
@@ -12248,6 +12301,7 @@ dependencies = [
"blake3",
"chrono",
"crossbeam-channel",
+ "mpl-core",
"once_cell",
"schemars 1.0.4",
"schemars_derive",
diff --git a/Cargo.toml b/Cargo.toml
index 077f0927..7c26e78d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -86,6 +86,7 @@ litesvm = { version = "0.8.1", features = ["nodejs-internal"] }
litesvm-token = "0.8.1"
log = "0.4.27"
mime_guess = { version = "2.0.4", default-features = false }
+mpl-core = { git = "https://github.com/ivsop/mpl-core", rev = "27bbab1997fdd39fd010d78406cc7ee2e7e2601d", default-features = false, features = ["serde"] }
mustache = "0.9.0"
notify = { version = "8.0.0", default-features = false }
npm_rs = "1.0.0"
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index d91458d8..d078b346 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -40,6 +40,7 @@ libloading = { workspace = true }
litesvm = { workspace = true }
litesvm-token = { workspace = true }
log = { workspace = true }
+mpl-core = { workspace = true}
reqwest = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true } # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251
diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs
index 370670ab..3293945f 100644
--- a/crates/core/src/error.rs
+++ b/crates/core/src/error.rs
@@ -336,6 +336,20 @@ impl SurfpoolError {
Self(error)
}
+ pub fn invalid_account_type
(pubkey: P, message: Option) -> Self
+ where
+ P: Display,
+ M: Display,
+ {
+ let base_msg = format!("invalid account type {pubkey}");
+ let full_msg = if let Some(msg) = message {
+ format!("{base_msg}: {msg}")
+ } else {
+ base_msg
+ };
+ Self(Error::invalid_params(full_msg))
+ }
+
pub fn invalid_account_owner(pubkey: P, message: Option) -> Self
where
P: Display,
@@ -446,4 +460,30 @@ impl SurfpoolError {
error.message = format!("Expected profile not found for key {key}");
Self(error)
}
+
+ pub fn update_core_asset_error(pubkey: Pubkey, e: T) -> Self
+ where
+ T: ToString,
+ {
+ let mut error = Error::internal_error();
+ error.data = Some(json!(format!(
+ "Error updating core asset {}: {}",
+ pubkey,
+ e.to_string()
+ )));
+ Self(error)
+ }
+
+ pub fn update_core_collection_error(pubkey: Pubkey, e: T) -> Self
+ where
+ T: ToString,
+ {
+ let mut error = Error::internal_error();
+ error.data = Some(json!(format!(
+ "Error updating core collection {}: {}",
+ pubkey,
+ e.to_string()
+ )));
+ Self(error)
+ }
}
diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs
index 16eca302..daef723a 100644
--- a/crates/core/src/rpc/surfnet_cheatcodes.rs
+++ b/crates/core/src/rpc/surfnet_cheatcodes.rs
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use jsonrpc_core::{BoxFuture, Error, Result, futures::future};
use jsonrpc_derive::rpc;
+use mpl_core::DataBlob;
use solana_account::Account;
use solana_client::rpc_response::{RpcLogsResponse, RpcResponseContext};
use solana_clock::Slot;
@@ -14,9 +15,10 @@ use solana_system_interface::program as system_program;
use solana_transaction::versioned::VersionedTransaction;
use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
use surfpool_types::{
- AccountSnapshot, ClockCommand, ExportSnapshotConfig, GetStreamedAccountsResponse,
- GetSurfnetInfoResponse, Idl, ResetAccountConfig, RpcProfileResultConfig, Scenario,
- SimnetCommand, SimnetEvent, StreamAccountConfig, UiKeyedProfileResult,
+ AccountSnapshot, ClockCommand, CoreAssetUpdate, ExportSnapshotConfig,
+ GetStreamedAccountsResponse, GetSurfnetInfoResponse, Idl, ResetAccountConfig,
+ RpcProfileResultConfig, Scenario, SimnetCommand, SimnetEvent, StreamAccountConfig,
+ UiKeyedProfileResult,
types::{AccountUpdate, SetSomeAccount, SupplyUpdate, TokenAccountUpdate, UuidOrSignature},
};
@@ -236,6 +238,58 @@ pub trait SurfnetCheatcodes {
destination_program_id: String,
) -> BoxFuture>>;
+ /// A "cheat code" method for developers to change the data of Core Assets (metaplex core NFTs).
+ ///
+ /// This method allows developers to, for example, change the ownership of a core Asset by changing its `owner` field.
+ ///
+ /// ## Parameters
+ /// - `pubkey`: The public key of the account to be updated, as a base-58 encoded string.
+ /// - `update`: The `CoreAssetUpdate` struct containing the fields to be updated.
+ ///
+ /// ## Returns
+ /// A `RpcResponse<()>` indicating whether the account update was successful.
+ ///
+ /// ## Example Request
+ /// ```json
+ /// {
+ /// "jsonrpc": "2.0",
+ /// "id": 1,
+ /// "method": "surfnet_updateCoreAsset",
+ /// "params": [
+ /// "",
+ /// {
+ /// "owner": "",
+ /// "name": "some new name",
+ /// "deleteAllPlugins": true,
+ /// "updateAuthority": {
+ /// "Address": ""
+ /// }
+ /// }
+ /// ]
+ /// }
+ ///
+ ///
+ /// ```
+ ///
+ /// ## Example Response
+ /// ```json
+ /// {
+ /// "jsonrpc": "2.0",
+ /// "result": {},
+ /// "id": 1
+ /// }
+ /// ```
+ ///
+ /// # See Also
+ /// - `setAccount`, `setTokenAccount`
+ #[rpc(meta, name = "surfnet_updateCoreAsset")]
+ fn update_core_asset(
+ &self,
+ meta: Self::Metadata,
+ pubkey: String,
+ update: CoreAssetUpdate,
+ ) -> BoxFuture>>;
+
/// Estimates the compute units that a given transaction will consume.
///
/// This method simulates the transaction without committing its state changes
@@ -1300,6 +1354,533 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc {
})
}
+ // TODO: figure out how to keep the external plugins
+ // TODO: make a macro so that the code is not so painful to look at
+ fn update_core_asset(
+ &self,
+ meta: Self::Metadata,
+ pubkey_str: String,
+ update: CoreAssetUpdate,
+ ) -> BoxFuture>> {
+ let pubkey = match verify_pubkey(&pubkey_str) {
+ Ok(res) => res,
+ Err(e) => return e.into(),
+ };
+
+ let SurfnetRpcContext {
+ svm_locker,
+ remote_ctx,
+ } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
+ Ok(res) => res,
+ Err(e) => return e.into(),
+ };
+
+ Box::pin(async move {
+ // Fetch the account. It must exist either locally or in the remote.
+ let SvmAccessContext {
+ slot,
+ inner: account_result_to_update,
+ ..
+ } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
+
+ // treat different cases separately for better error messages
+ match account_result_to_update {
+ GetAccountResult::None(_) => {
+ return Err(SurfpoolError::account_not_found(pubkey).into());
+ }
+ GetAccountResult::FoundProgramAccount(_, _) => {
+ return Err(SurfpoolError::invalid_account_type(
+ pubkey,
+ Some("The account is a program account!"),
+ )
+ .into());
+ }
+ GetAccountResult::FoundTokenAccount(_, _) => {
+ return Err(SurfpoolError::invalid_account_type(
+ pubkey,
+ Some("The account is a token account!"),
+ )
+ .into());
+ }
+ GetAccountResult::FoundAccount(_pubkey, mut account, _update) => {
+ let mut asset = mpl_core::Asset::deserialize(&account.data)
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+
+ if let Some(new_owner_str) = update.owner {
+ let new_owner = verify_pubkey(&new_owner_str)?;
+ // have to do this messy conversion because mpl-core's Pubkey is a different version from the one we are using
+ asset.base.owner = new_owner.to_bytes().into();
+ }
+
+ if let Some(new_update_auth) = update.update_authority {
+ asset.base.update_authority = new_update_auth;
+ }
+
+ if let Some(new_name) = update.name {
+ asset.base.name = new_name;
+ }
+
+ if let Some(new_uri) = update.uri {
+ asset.base.uri = new_uri;
+ }
+
+ if let Some(new_seq) = update.seq {
+ asset.base.seq = new_seq;
+ }
+
+ if let Some(delete_all_plugins) = update.delete_all_plugins {
+ if delete_all_plugins {
+ asset.plugin_header = None;
+ }
+ }
+
+ // there is no client-facing code to serialize assets.... will have to do it myself
+ // this mimics the code from the instruction to create a core asset
+
+ // header is always serialized no matter what
+ let mut new_data = asset
+ .base
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+
+ // if there are plugins, they have to be serialized
+ if let Some(_plugin_header) = asset.plugin_header {
+ // for this to crash the header would need take up 2 GiBs which is impossible
+ let new_header_offset = u64::try_from(asset.base.len()).unwrap();
+
+ // new plugin header
+ let mut plugin_header = mpl_core::accounts::PluginHeaderV1 {
+ key: mpl_core::types::Key::PluginHeaderV1,
+ plugin_registry_offset: new_header_offset
+ + 1 // Plugin Header Key
+ + 8, // Plugin Registry Offset
+ };
+
+ // we can serialize the plugin header right now. however, it's going to keep changing and we'll need to keep serializing it.
+ // I'm going to keep a temporary data vector to serialize the plugins into
+ // this means that in the end, I just have to concatenate: asset header + plugin header + temp data + plugin registry
+ let mut temp_data: Vec = Vec::new();
+
+ // they for some reason make a registry, write it to the account, then change it and write it again
+ // they iterate plugins in a loop and write to the account after every single plugin (???)
+ let mut plugin_registry = mpl_core::accounts::PluginRegistryV1 {
+ key: mpl_core::types::Key::PluginRegistryV1,
+ registry: vec![],
+ external_registry: vec![],
+ };
+
+ // plugins are returned in a stupid data format. will have to make a gigantic if block
+ // they return the header, with an offset that has now become useless, but can't just return the Vec?
+ // they have the audacity to call this struct a list just to make me lose my sanity
+
+ if let Some(royalties) = asset.plugin_list.royalties {
+ // store the old offset of the plugin registry
+ let old_plugin_registry_offset = plugin_header.plugin_registry_offset;
+
+ // make a registry record indicating that this new plugin will be at the position the plugin registry used to be
+ let new_registry_record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::Royalties,
+ offset: old_plugin_registry_offset,
+ authority: mpl_core::types::PluginAuthority::from(
+ royalties.base.authority,
+ ),
+ };
+
+ // push to the registry
+ plugin_registry.registry.push(new_registry_record.clone());
+
+ // write the plugin to the location of the old plugin registry offset
+ // in this case we can completely ignore this and just append it to the temporary data array. the locations will have to coincide
+ let plugin = mpl_core::types::Plugin::Royalties(royalties.royalties);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+
+ // using the length of the new plugin, advance the plugin header's offset to point to the new position of the plugin registry
+ plugin_header.plugin_registry_offset = old_plugin_registry_offset
+ + (u64::try_from(new_registry_record.len())).unwrap();
+ }
+
+ if let Some(freeze_delegate) = asset.plugin_list.freeze_delegate {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::FreezeDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ freeze_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::FreezeDelegate(
+ freeze_delegate.freeze_delegate,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(burn_delegate) = asset.plugin_list.burn_delegate {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::BurnDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ burn_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin =
+ mpl_core::types::Plugin::BurnDelegate(burn_delegate.burn_delegate);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(transfer_delegate) = asset.plugin_list.transfer_delegate {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::TransferDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ transfer_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::TransferDelegate(
+ transfer_delegate.transfer_delegate,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ }
+
+ if let Some(update_delegate) = asset.plugin_list.update_delegate {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::UpdateDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ update_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::UpdateDelegate(
+ update_delegate.update_delegate,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(permanent_freeze_delegate) =
+ asset.plugin_list.permanent_freeze_delegate
+ {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::PermanentFreezeDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ permanent_freeze_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::PermanentFreezeDelegate(
+ permanent_freeze_delegate.permanent_freeze_delegate,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(attributes) = asset.plugin_list.attributes {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::Attributes,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ attributes.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::Attributes(attributes.attributes);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(permanent_transfer_delegate) =
+ asset.plugin_list.permanent_transfer_delegate
+ {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::PermanentTransferDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ permanent_transfer_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::PermanentTransferDelegate(
+ permanent_transfer_delegate.permanent_transfer_delegate,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(permanent_burn_delegate) =
+ asset.plugin_list.permanent_burn_delegate
+ {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::PermanentBurnDelegate,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ permanent_burn_delegate.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::PermanentBurnDelegate(
+ permanent_burn_delegate.permanent_burn_delegate,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(edition) = asset.plugin_list.edition {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::Edition,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ edition.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::Edition(edition.edition);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(master_edition) = asset.plugin_list.master_edition {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::MasterEdition,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ master_edition.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::MasterEdition(
+ master_edition.master_edition,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(add_blocker) = asset.plugin_list.add_blocker {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::AddBlocker,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ add_blocker.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin =
+ mpl_core::types::Plugin::AddBlocker(add_blocker.add_blocker);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(immutable_metadata) = asset.plugin_list.immutable_metadata {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::ImmutableMetadata,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ immutable_metadata.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::ImmutableMetadata(
+ immutable_metadata.immutable_metadata,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(verified_creators) = asset.plugin_list.verified_creators {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::VerifiedCreators,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ verified_creators.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::VerifiedCreators(
+ verified_creators.verified_creators,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(autograph) = asset.plugin_list.autograph {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::Autograph,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ autograph.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::Autograph(autograph.autograph);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(bubblegum_v2) = asset.plugin_list.bubblegum_v2 {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::BubblegumV2,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ bubblegum_v2.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin =
+ mpl_core::types::Plugin::BubblegumV2(bubblegum_v2.bubblegum_v2);
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(freeze_execute) = asset.plugin_list.freeze_execute {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::FreezeExecute,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ freeze_execute.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::FreezeExecute(
+ freeze_execute.freeze_execute,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ if let Some(permanent_freeze_execute) =
+ asset.plugin_list.permanent_freeze_execute
+ {
+ let old_off = plugin_header.plugin_registry_offset;
+ let record = mpl_core::types::RegistryRecord {
+ plugin_type: mpl_core::types::PluginType::PermanentFreezeExecute,
+ offset: old_off,
+ authority: mpl_core::types::PluginAuthority::from(
+ permanent_freeze_execute.base.authority,
+ ),
+ };
+ plugin_registry.registry.push(record.clone());
+ let plugin = mpl_core::types::Plugin::PermanentFreezeExecute(
+ permanent_freeze_execute.permanent_freeze_execute,
+ );
+ let plugin_data = plugin
+ .to_vec()
+ .map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ temp_data.extend_from_slice(&plugin_data);
+ plugin_header.plugin_registry_offset =
+ old_off + u64::try_from(plugin_data.len()).unwrap();
+ }
+
+ // append the plugin data
+ let plugin_header_data = plugin_header.to_vec().map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ new_data.extend_from_slice(&plugin_header_data);
+
+ new_data.extend_from_slice(&temp_data);
+
+ let plugin_registry_data = plugin_registry.to_vec().map_err(|e| SurfpoolError::update_core_asset_error(pubkey, e))?;
+ new_data.extend_from_slice(&plugin_registry_data);
+ }
+
+ account.data = new_data;
+
+ svm_locker.write_account_update(GetAccountResult::FoundAccount(
+ pubkey, account, true,
+ ));
+
+ Ok(RpcResponse {
+ context: RpcResponseContext::new(slot),
+ value: (),
+ })
+ }
+ }
+ })
+ }
+
/// Clones a program account from one program ID to another.
/// A program account contains a pointer to a program data account, which is a PDA derived from the program ID.
/// So, when cloning a program account, we need to clone the program data account as well.
diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml
index 2991c4e6..772bbf69 100644
--- a/crates/types/Cargo.toml
+++ b/crates/types/Cargo.toml
@@ -17,6 +17,7 @@ anchor-lang-idl = { workspace = true }
blake3 = { workspace = true }
chrono = { workspace = true }
crossbeam-channel = { workspace = true }
+mpl-core = { workspace = true }
once_cell = { workspace = true }
schemars = { workspace = true }
schemars_derive = { workspace = true }
diff --git a/crates/types/src/rpc_endpoints.json b/crates/types/src/rpc_endpoints.json
index 9c55eb05..9ff156f3 100644
--- a/crates/types/src/rpc_endpoints.json
+++ b/crates/types/src/rpc_endpoints.json
@@ -691,6 +691,32 @@
]
}
},
+ {
+ "method": "surfnet_stealCore",
+ "description": "A 'cheat code' method for developers to take ownership of core NFTs and collections locally. In core NFTs, this allows setting the owner to any address. In core collections, the authority is changed.",
+ "params": [
+ {
+ "name": "pubkey",
+ "type": "string",
+ "description": "The public key of the account to be updated, as a base-58 encoded string."
+ },
+ {
+ "name": "pubkey",
+ "type": "new_owner",
+ "description": "The public key of the new owner/authority, as a base-58 encoded string."
+ }
+ ],
+ "returns": "A `RpcResponse<()>` indicating whether the account update was successful.",
+ "example": {
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "surfnet_stealCore",
+ "params": [
+ "account_pubkey",
+ "new_owner_pubkey"
+ ]
+ }
+ },
{
"method": "surfnet_cloneProgramAccount",
"description": "Clones a program account from a source to a destination program ID. Since a program account points to a program data account (a PDA), this method clones the program data account as well. It gets the source program and data accounts, calculates the destination program data address, points the destination program account to it, and copies the data.",
diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs
index a93f430a..c5697f04 100644
--- a/crates/types/src/types.rs
+++ b/crates/types/src/types.rs
@@ -884,6 +884,25 @@ impl Serialize for UuidOrSignature {
}
}
+/// Arguments passed to the cheatcode, so you can update the account partially
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CoreAssetUpdate {
+ /// New owner
+ pub owner: Option,
+ /// New update authority
+ pub update_authority: Option,
+ /// New name
+ pub name: Option,
+ /// New off-chain uri
+ pub uri: Option,
+ /// New seq number
+ pub seq: Option