From 535147a347cf2a937c4d0d2aa3fecc3ff6028a75 Mon Sep 17 00:00:00 2001 From: Ivan Ribeiro Date: Tue, 28 Oct 2025 19:47:48 +0000 Subject: [PATCH 1/3] Added the steal_core RPC endpoint --- crates/core/src/error.rs | 14 ++ crates/core/src/rpc/surfnet_cheatcodes.rs | 149 ++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 370670ab..fabc6689 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, diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 16eca302..3a048d52 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -236,6 +236,46 @@ pub trait SurfnetCheatcodes { destination_program_id: String, ) -> BoxFuture>>; + /// A "cheat code" method for developers to take ownership of core NFTs and collections. + /// + /// This method allows developers to change the ownership of a core NFT or collection by changing `owner` and `update_authority` fields, respectively. + /// + /// ## Parameters + /// - `pubkey`: The public key of the account to be updated, as a base-58 encoded string. + /// - `new_owner`: 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 Request + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "id": 1, + /// "method": "surfnet_stealCore", + /// "params": ["account_pubkey", "new_owner_pubkey"] + /// } + /// ``` + /// + /// ## Example Response + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": {}, + /// "id": 1 + /// } + /// ``` + /// + /// # See Also + /// - `setAccount`, `setTokenAccount` + #[rpc(meta, name = "surfnet_stealCore")] + fn steal_core( + &self, + meta: Self::Metadata, + pubkey: String, + new_owner: String, + ) -> BoxFuture>>; + /// Estimates the compute units that a given transaction will consume. /// /// This method simulates the transaction without committing its state changes @@ -1300,6 +1340,115 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { }) } + fn steal_core( + &self, + meta: Self::Metadata, + pubkey_str: String, + new_owner_str: String, + ) -> BoxFuture>> { + let pubkey = match verify_pubkey(&pubkey_str) { + Ok(res) => res, + Err(e) => return e.into(), + }; + let new_owner = match verify_pubkey(&new_owner_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?; + + match account_result_to_update { + // error if the account did not exist + GetAccountResult::None(_) => { + return Err(SurfpoolError::account_not_found(pubkey).into()); + } + // error if it is a program or a token account + 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) => { + // I don't want to drag dependencies or add a lot of code so I'll process the data here + // DATA: + // [0]: Key + // [1-33] the owner / the authority + // [33-...] rest of the header + plugin information + let data = &mut account.data; + if data.len() < 33 { + return Err(SurfpoolError::invalid_account_data( + pubkey, + data, + Some("The data does not look like a core NFT or a core collection"), + ) + .into()); + } + + // Log information, and detect if the account is a core NFT or collection + // I'm not going to deserialize the entire thing as this is extremely cumbersome + // I also don't check that the account is owned by the metplex core program + // Since this is a dev tool I think have few restrictions is a plus + match data[0] { + 1 => { + let _ = svm_locker + .simnet_events_tx() + .send(SimnetEvent::info(format!("Account {pubkey} is a core NFT"))); + } + 5 => { + let _ = svm_locker + .simnet_events_tx() + .send(SimnetEvent::info(format!( + "Account {pubkey} is a core collection" + ))); + } + _ => { + return Err(SurfpoolError::invalid_account_data( + pubkey, + data, + Some("The data does not look like a core NFT or a core collection"), + ) + .into()); + } + }; + + // change the data related to ownership. leave remaining data untouched + data[1..33].copy_from_slice(new_owner.as_array()); + 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. From 137ac68b77972e6143ccc702282adae6429f200a Mon Sep 17 00:00:00 2001 From: Ivan Ribeiro Date: Wed, 29 Oct 2025 14:12:25 +0000 Subject: [PATCH 2/3] docs for stealCore --- crates/core/src/rpc/surfnet_cheatcodes.rs | 9 ++++---- crates/types/src/rpc_endpoints.json | 26 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 3a048d52..b6737a6a 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -1371,12 +1371,11 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { .. } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?; + // treat different cases separately for better error messages match account_result_to_update { - // error if the account did not exist GetAccountResult::None(_) => { return Err(SurfpoolError::account_not_found(pubkey).into()); } - // error if it is a program or a token account GetAccountResult::FoundProgramAccount(_, _) => { return Err(SurfpoolError::invalid_account_type( pubkey, @@ -1395,8 +1394,8 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { // I don't want to drag dependencies or add a lot of code so I'll process the data here // DATA: // [0]: Key - // [1-33] the owner / the authority - // [33-...] rest of the header + plugin information + // [1-33]: the owner / the authority + // [33-...]: rest of the header + plugin information let data = &mut account.data; if data.len() < 33 { return Err(SurfpoolError::invalid_account_data( @@ -1410,7 +1409,7 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { // Log information, and detect if the account is a core NFT or collection // I'm not going to deserialize the entire thing as this is extremely cumbersome // I also don't check that the account is owned by the metplex core program - // Since this is a dev tool I think have few restrictions is a plus + // Since this is a dev tool I think having few restrictions is a plus match data[0] { 1 => { let _ = svm_locker 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.", From 80e69f803810ee797922ae058883808a02c391db Mon Sep 17 00:00:00 2001 From: Ivan Ribeiro Date: Thu, 27 Nov 2025 21:04:57 +0000 Subject: [PATCH 3/3] Changed stealCore to updateCoreAsset, allowing changing the entire header --- Cargo.lock | 86 +++- Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/error.rs | 26 ++ crates/core/src/rpc/surfnet_cheatcodes.rs | 543 +++++++++++++++++++--- crates/types/Cargo.toml | 1 + crates/types/src/types.rs | 19 + 7 files changed, 606 insertions(+), 71 deletions(-) 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 fabc6689..3293945f 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -460,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 b6737a6a..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,13 +238,13 @@ pub trait SurfnetCheatcodes { destination_program_id: String, ) -> BoxFuture>>; - /// A "cheat code" method for developers to take ownership of core NFTs and collections. + /// A "cheat code" method for developers to change the data of Core Assets (metaplex core NFTs). /// - /// This method allows developers to change the ownership of a core NFT or collection by changing `owner` and `update_authority` fields, respectively. + /// 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. - /// - `new_owner`: The public key of the new owner/authority, 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. @@ -252,9 +254,21 @@ pub trait SurfnetCheatcodes { /// { /// "jsonrpc": "2.0", /// "id": 1, - /// "method": "surfnet_stealCore", - /// "params": ["account_pubkey", "new_owner_pubkey"] + /// "method": "surfnet_updateCoreAsset", + /// "params": [ + /// "", + /// { + /// "owner": "", + /// "name": "some new name", + /// "deleteAllPlugins": true, + /// "updateAuthority": { + /// "Address": "" + /// } + /// } + /// ] /// } + /// + /// /// ``` /// /// ## Example Response @@ -268,12 +282,12 @@ pub trait SurfnetCheatcodes { /// /// # See Also /// - `setAccount`, `setTokenAccount` - #[rpc(meta, name = "surfnet_stealCore")] - fn steal_core( + #[rpc(meta, name = "surfnet_updateCoreAsset")] + fn update_core_asset( &self, meta: Self::Metadata, pubkey: String, - new_owner: String, + update: CoreAssetUpdate, ) -> BoxFuture>>; /// Estimates the compute units that a given transaction will consume. @@ -1340,20 +1354,18 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { }) } - fn steal_core( + // 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, - new_owner_str: String, + update: CoreAssetUpdate, ) -> BoxFuture>> { let pubkey = match verify_pubkey(&pubkey_str) { Ok(res) => res, Err(e) => return e.into(), }; - let new_owner = match verify_pubkey(&new_owner_str) { - Ok(res) => res, - Err(e) => return e.into(), - }; let SurfnetRpcContext { svm_locker, @@ -1391,50 +1403,471 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { .into()); } GetAccountResult::FoundAccount(_pubkey, mut account, _update) => { - // I don't want to drag dependencies or add a lot of code so I'll process the data here - // DATA: - // [0]: Key - // [1-33]: the owner / the authority - // [33-...]: rest of the header + plugin information - let data = &mut account.data; - if data.len() < 33 { - return Err(SurfpoolError::invalid_account_data( - pubkey, - data, - Some("The data does not look like a core NFT or a core collection"), - ) - .into()); + 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; + } } - // Log information, and detect if the account is a core NFT or collection - // I'm not going to deserialize the entire thing as this is extremely cumbersome - // I also don't check that the account is owned by the metplex core program - // Since this is a dev tool I think having few restrictions is a plus - match data[0] { - 1 => { - let _ = svm_locker - .simnet_events_tx() - .send(SimnetEvent::info(format!("Account {pubkey} is a core NFT"))); + // 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(); } - 5 => { - let _ = svm_locker - .simnet_events_tx() - .send(SimnetEvent::info(format!( - "Account {pubkey} is a core collection" - ))); + + 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(); } - _ => { - return Err(SurfpoolError::invalid_account_data( - pubkey, - data, - Some("The data does not look like a core NFT or a core collection"), - ) - .into()); + + 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(); } - }; - // change the data related to ownership. leave remaining data untouched - data[1..33].copy_from_slice(new_owner.as_array()); + 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, )); 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/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>, + /// Delete all of the plugins + // Option so it works even if you pass nothing in. None is the same as Some(false) + pub delete_all_plugins: Option, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub enum DataIndexingCommand { ProcessCollection(Uuid),