diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 445aaf5ca..656f99c14 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -22,8 +22,8 @@ use surfpool_types::{ StreamAccountsEntry, UiKeyedProfileResult, types::{ AccountUpdate, ConfidentialBalanceKeys, DeriveConfidentialKeysResponse, - GetConfidentialBalanceResponse, SetSomeAccount, SupplyUpdate, TokenAccountUpdate, - UuidOrSignature, + GetConfidentialBalanceResponse, MintUpdate, SetSomeAccount, SupplyUpdate, + TokenAccountUpdate, UuidOrSignature, }, }; @@ -38,8 +38,9 @@ use crate::{ AccountSource, GetAccountResult, locker::SvmAccessContext, svm::AccountUpdatePolicy, }, types::{ - TimeTravelConfig, TokenAccount, build_confidential_token_account_data, - decrypt_confidential_balances, derive_confidential_keys, mint_has_transfer_fee_config, + MintAccount, TimeTravelConfig, TokenAccount, build_confidential_mint_data, + build_confidential_token_account_data, decrypt_confidential_balances, + derive_confidential_keys, mint_has_transfer_fee_config, }, }; @@ -142,6 +143,33 @@ impl TokenAccountUpdateExt for TokenAccountUpdate { } } +pub trait MintUpdateExt { + fn apply(self, mint: &mut MintAccount) -> Result<()>; +} + +impl MintUpdateExt for MintUpdate { + /// Apply the update to the mint's base fields + fn apply(self, mint: &mut MintAccount) -> Result<()> { + if let Some(decimals) = self.decimals { + mint.set_decimals(decimals); + } + if let Some(mint_authority) = self.mint_authority { + match mint_authority { + SetSomeAccount::Account(pubkey) => { + mint.set_mint_authority(COption::Some(verify_pubkey(&pubkey)?)); + } + SetSomeAccount::NoAccount => { + mint.set_mint_authority(COption::None); + } + } + } + if let Some(supply) = self.supply { + mint.set_supply(supply); + } + Ok(()) + } +} + #[rpc] pub trait SurfnetCheatcodes { type Metadata; @@ -374,6 +402,55 @@ pub trait SurfnetCheatcodes { token_program: Option, ) -> BoxFuture>>; + /// A "cheat code" method for developers to set or update a token mint in Surfpool. + /// + /// The mint is created when it does not exist yet; fields left out keep their + /// current value. + /// + /// ## Parameters + /// - `mint`: The public key of the mint, as a base-58 encoded string. + /// - `update`: The `MintUpdate` struct containing the optional fields to update: + /// - `decimals` (u8), `supply` (u64) + /// - `mintAuthority`: a base58 pubkey, or the literal string "null" to clear it + /// - `confidential`: writes the Token-2022 `ConfidentialTransferMint` extension + /// (`authority`, `auditorElgamalPubkey`, `autoApproveNewAccounts`, default + /// true), producing the confidential-capable mint `surfnet_setTokenAccount`'s + /// `confidential` path needs. Requires `token_program` to be Token-2022, and + /// rewrites the account data, so any other extension the mint carried is + /// dropped. Without it, an existing mint's extension bytes are preserved. + /// - `token_program`: Optional token program id, defaulting to the SPL Token program. + /// + /// ## Returns + /// A `RpcResponse<()>` indicating whether the mint update was successful. + /// + /// ### Example Request (a confidential mint with an auditor) + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "id": 1, + /// "method": "surfnet_setMint", + /// "params": [ + /// "mint_pubkey", + /// { "decimals": 6, "confidential": { + /// "authority": "authority_pubkey", + /// "auditorElgamalPubkey": "" + /// } }, + /// "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + /// ] + /// } + /// ``` + /// + /// # See Also + /// - `surfnet_setTokenAccount`, `surfnet_getConfidentialBalance` + #[rpc(meta, name = "surfnet_setMint")] + fn set_mint( + &self, + meta: Self::Metadata, + mint: String, + update: MintUpdate, + token_program: Option, + ) -> BoxFuture>>; + #[rpc(meta, name = "surfnet_cloneProgramAccount")] fn clone_program_account( &self, @@ -1889,6 +1966,122 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { }) } + fn set_mint( + &self, + meta: Self::Metadata, + mint_str: String, + update: MintUpdate, + some_token_program_str: Option, + ) -> BoxFuture>> { + let mint = match verify_pubkey(&mint_str) { + Ok(res) => res, + Err(e) => return e.into(), + }; + + let token_program_id = match some_token_program_str { + Some(token_program_str) => match verify_pubkey(&token_program_str) { + Ok(res) => res, + Err(e) => return e.into(), + }, + None => spl_token_interface::id(), + }; + + 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 { + let confidential = update.confidential.clone(); + + if confidential.is_some() && token_program_id != spl_token_2022_interface::id() { + return Err(Error::invalid_params( + "confidential transfer mints require the Token-2022 program (set tokenProgram to the Token-2022 program id)".to_string(), + )); + } + + let SvmAccessContext { + slot, + inner: mut mint_account, + .. + } = svm_locker + .get_account( + &remote_ctx, + &mint, + Some(Box::new(move |_| { + GetAccountResult::FoundAccount( + mint, + Account { + lamports: 0, + owner: token_program_id, + executable: false, + rent_epoch: 0, + data: MintAccount::new(&token_program_id).pack_into_vec(), + }, + AccountSource::Generated, + ) + })), + ) + .await?; + + let mut mint_data = MintAccount::unpack(mint_account.expected_data()) + .map_err(|e| Error::invalid_params(format!("Failed to unpack mint data: {}", e)))?; + + update.apply(&mut mint_data)?; + + let final_mint_bytes = if let Some(conf) = &confidential { + // The extension lives in a TLV layout the base-only packing + // can't represent, so the account is rewritten from scratch. + let base = match &mint_data { + MintAccount::SplToken2022(base) => *base, + MintAccount::SplToken(_) => { + return Err(Error::invalid_params( + "confidential transfer mints require the Token-2022 program" + .to_string(), + )); + } + }; + build_confidential_mint_data(&base, conf).map_err(Error::invalid_params)? + } else { + mint_data + .pack_into_preserving_extensions(mint_account.expected_data()) + .map_err(|e| { + Error::invalid_params(format!("Failed to pack mint data: {}", e)) + })? + }; + + let lamports = svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .minimum_balance_for_rent_exemption(final_mint_bytes.len()) + }); + + mint_account.apply_update(|account| { + // A mint's address is not derived from the token program, so an + // existing mint may belong to another one, which would never + // read the Token-2022 extension written here. + if confidential.is_some() && account.owner != token_program_id { + return Err(Error::invalid_params(format!( + "mint {mint} is owned by {}, not the Token-2022 program", + account.owner + ))); + } + account.lamports = lamports; + account.data = final_mint_bytes.clone(); + Ok(()) + })?; + svm_locker.apply_account_update(mint_account, AccountUpdatePolicy::Authoritative)?; + + 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. @@ -5589,6 +5782,351 @@ mod tests { assert_eq!(result.pending_balance_credit_counter, 0); } + #[tokio::test(flavor = "multi_thread")] + async fn test_set_mint_writes_confidential_transfer_extension() { + use bytemuck::bytes_of; + use solana_zk_sdk::encryption::elgamal::ElGamalKeypair; + use solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey; + use spl_token_2022_interface::extension::{ + BaseStateWithExtensions, StateWithExtensions, + confidential_transfer::ConfidentialTransferMint, + }; + use surfpool_types::types::ConfidentialTransferMintUpdate; + + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + let mint = Keypair::new(); + let authority = Keypair::new(); + let auditor = PodElGamalPubkey::from(ElGamalKeypair::new_rand().pubkey_owned()); + + client + .rpc + .set_mint( + Some(client.context.clone()), + mint.pubkey().to_string(), + MintUpdate { + decimals: Some(6), + confidential: Some(ConfidentialTransferMintUpdate { + authority: Some(authority.pubkey().to_string()), + auditor_elgamal_pubkey: Some( + bs58::encode(bytes_of(&auditor)).into_string(), + ), + ..Default::default() + }), + ..Default::default() + }, + Some(spl_token_2022_interface::id().to_string()), + ) + .await + .expect("set_mint should succeed"); + + let account = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .get_account(&mint.pubkey()) + .unwrap() + .unwrap() + }); + assert_eq!(account.owner, spl_token_2022_interface::id()); + + let state = + StateWithExtensions::::unpack(&account.data) + .expect("mint should unpack with extensions"); + let ext = state + .get_extension::() + .expect("confidential mint extension should be present"); + + assert_eq!(state.base.decimals, 6); + assert_eq!(bytes_of(&ext.authority), authority.pubkey().as_ref()); + assert_eq!(bytes_of(&ext.auditor_elgamal_pubkey), bytes_of(&auditor)); + assert!( + bool::from(ext.auto_approve_new_accounts), + "new accounts should be auto-approved by default" + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_set_mint_confidential_requires_token_2022() { + use surfpool_types::types::ConfidentialTransferMintUpdate; + + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + + for token_program in [None, Some(spl_token_interface::id().to_string())] { + let mint = Keypair::new(); + let error = client + .rpc + .set_mint( + Some(client.context.clone()), + mint.pubkey().to_string(), + MintUpdate { + confidential: Some(ConfidentialTransferMintUpdate::default()), + ..Default::default() + }, + token_program, + ) + .await + .expect_err("a confidential mint outside Token-2022 should be refused"); + assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams); + + assert!( + client + .context + .svm_locker + .with_svm_reader(|svm_reader| svm_reader + .inner + .get_account(&mint.pubkey()) + .unwrap() + .is_none()), + "the refused mint should not be written" + ); + } + + let legacy_mint = Keypair::new(); + client + .rpc + .set_mint( + Some(client.context.clone()), + legacy_mint.pubkey().to_string(), + MintUpdate { + decimals: Some(6), + ..Default::default() + }, + None, + ) + .await + .expect("set_mint should succeed"); + + let error = client + .rpc + .set_mint( + Some(client.context.clone()), + legacy_mint.pubkey().to_string(), + MintUpdate { + confidential: Some(ConfidentialTransferMintUpdate::default()), + ..Default::default() + }, + Some(spl_token_2022_interface::id().to_string()), + ) + .await + .expect_err( + "a confidential write onto a mint owned by the SPL Token program should be refused", + ); + assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams); + + let account = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .get_account(&legacy_mint.pubkey()) + .unwrap() + .unwrap() + }); + assert_eq!(account.owner, spl_token_interface::id()); + assert_eq!(account.data.len(), Mint::LEN); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_set_mint_writes_plain_spl_token_mint() { + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + let mint = Keypair::new(); + let mint_authority = Keypair::new(); + + client + .rpc + .set_mint( + Some(client.context.clone()), + mint.pubkey().to_string(), + MintUpdate { + decimals: Some(9), + mint_authority: Some(SetSomeAccount::Account( + mint_authority.pubkey().to_string(), + )), + supply: Some(1_000), + confidential: None, + }, + None, + ) + .await + .expect("set_mint should succeed"); + + let account = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .get_account(&mint.pubkey()) + .unwrap() + .unwrap() + }); + assert_eq!(account.owner, spl_token_interface::id()); + assert_eq!(account.data.len(), Mint::LEN); + + let (decimals, supply, authority) = match MintAccount::unpack(&account.data) + .expect("mint should unpack") + { + MintAccount::SplToken2022(base) => (base.decimals, base.supply, base.mint_authority), + MintAccount::SplToken(base) => (base.decimals, base.supply, base.mint_authority), + }; + assert_eq!(decimals, 9); + assert_eq!(supply, 1_000); + assert_eq!(authority, COption::Some(mint_authority.pubkey())); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_set_mint_preserves_existing_extensions() { + use spl_token_2022_interface::extension::{ + BaseStateWithExtensionsMut, ExtensionType, StateWithExtensionsMut, + transfer_fee::TransferFeeConfig, + }; + + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + let mint = Keypair::new(); + + let mint_len = ExtensionType::try_calculate_account_len::< + spl_token_2022_interface::state::Mint, + >(&[ExtensionType::TransferFeeConfig]) + .unwrap(); + let mut data = vec![0u8; mint_len]; + let mut state = + StateWithExtensionsMut::::unpack_uninitialized( + &mut data, + ) + .expect("mint buffer should initialize"); + state.base = spl_token_2022_interface::state::Mint { + decimals: 2, + is_initialized: true, + ..Default::default() + }; + state.pack_base(); + state.init_account_type().unwrap(); + state.init_extension::(false).unwrap(); + drop(state); + + set_account( + &client, + &mint.pubkey(), + &Account { + lamports: 1_000_000, + data, + owner: spl_token_2022_interface::id(), + executable: false, + rent_epoch: 0, + }, + ); + + client + .rpc + .set_mint( + Some(client.context.clone()), + mint.pubkey().to_string(), + MintUpdate { + decimals: Some(4), + ..Default::default() + }, + Some(spl_token_2022_interface::id().to_string()), + ) + .await + .expect("set_mint should succeed"); + + let account = client.context.svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .get_account(&mint.pubkey()) + .unwrap() + .unwrap() + }); + assert_eq!(account.data.len(), mint_len); + assert_eq!(MintAccount::unpack(&account.data).unwrap().decimals(), 4); + assert!( + mint_has_transfer_fee_config(&account.data), + "patching the base must leave the extension tail intact" + ); + } + + /// Lead 3's payoff: a mint minted by the cheatcode, not forked from mainnet, + /// carries a confidential account through the full set-then-read pair. + #[tokio::test(flavor = "multi_thread")] + async fn test_confidential_mint_feeds_set_token_account() { + use bytemuck::bytes_of; + use solana_zk_sdk::encryption::elgamal::ElGamalKeypair; + use solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey; + use surfpool_types::types::{ + ConfidentialTransferAccountUpdate, ConfidentialTransferMintUpdate, + }; + + let client = TestSetup::new(SurfnetCheatcodesRpc::empty()); + let owner = Keypair::new(); + let mint = Keypair::new(); + let token_program = spl_token_2022_interface::id(); + let auditor = PodElGamalPubkey::from(ElGamalKeypair::new_rand().pubkey_owned()); + + client + .rpc + .set_mint( + Some(client.context.clone()), + mint.pubkey().to_string(), + MintUpdate { + decimals: Some(6), + confidential: Some(ConfidentialTransferMintUpdate { + auditor_elgamal_pubkey: Some( + bs58::encode(bytes_of(&auditor)).into_string(), + ), + ..Default::default() + }), + ..Default::default() + }, + Some(token_program.to_string()), + ) + .await + .expect("set_mint should succeed"); + + let token_account = get_associated_token_address_with_program_id( + &owner.pubkey(), + &mint.pubkey(), + &token_program, + ); + let keys = client + .rpc + .derive_confidential_keys( + Some(client.context.clone()), + confidential_key_signature(&owner, &token_account), + ) + .expect("key derivation should succeed") + .value; + + client + .rpc + .set_token_account( + Some(client.context.clone()), + owner.pubkey().to_string(), + mint.pubkey().to_string(), + TokenAccountUpdate { + confidential: Some(ConfidentialTransferAccountUpdate { + elgamal_pubkey: keys.elgamal_pubkey.clone(), + aes_key: Some(keys.aes_key.clone()), + amount: Some(4_200), + ..Default::default() + }), + ..Default::default() + }, + Some(token_program.to_string()), + ) + .await + .expect("set_token_account should succeed"); + + let balance = client + .rpc + .get_confidential_balance( + Some(client.context.clone()), + token_account.to_string(), + ConfidentialBalanceKeys { + aes_key: Some(keys.aes_key), + elgamal_secret_key: Some(keys.elgamal_secret_key), + }, + ) + .await + .expect("get_confidential_balance should succeed") + .value; + + assert_eq!(balance.available, Some(4_200)); + } + /// The pending balance is split across two ciphertexts to keep each within the /// u32 discrete-log decode range, so the recombination is only exercised by a /// value that spans both halves. diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 15f00fbfb..b3e1c474a 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, vec}; +use std::{collections::HashSet, str::FromStr, vec}; use agave_reserved_account_keys::ReservedAccountKeys; use base64::{Engine, prelude::BASE64_STANDARD}; @@ -51,15 +51,17 @@ use solana_zk_sdk_pod::encryption::{ use spl_token_2022_interface::extension::{ BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, StateWithExtensions, StateWithExtensionsMut, - confidential_transfer::{ConfidentialTransferAccount, PENDING_BALANCE_LO_BIT_LENGTH}, + confidential_transfer::{ + ConfidentialTransferAccount, ConfidentialTransferMint, PENDING_BALANCE_LO_BIT_LENGTH, + }, confidential_transfer_fee::ConfidentialTransferFeeAmount, interest_bearing_mint::InterestBearingConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::TransferFeeConfig, }; use surfpool_types::types::{ - ConfidentialBalanceKeys, ConfidentialTransferAccountUpdate, DeriveConfidentialKeysResponse, - GetConfidentialBalanceResponse, + ConfidentialBalanceKeys, ConfidentialTransferAccountUpdate, ConfidentialTransferMintUpdate, + DeriveConfidentialKeysResponse, GetConfidentialBalanceResponse, }; use txtx_addon_kit::indexmap::IndexMap; @@ -1444,6 +1446,119 @@ pub fn build_confidential_token_account_data( Ok(buffer) } +/// Build the raw account data for a Token-2022 mint that carries the +/// confidential-transfer mint extension. +/// +/// Test-only helper backing the `surfnet_setMint` cheatcode: it writes the +/// extension directly, bypassing the on-chain +/// `ConfidentialTransferInitializeMint`. An absent `authority` or +/// `auditor_elgamal_pubkey` is written as the extension's null value. +pub fn build_confidential_mint_data( + base: &spl_token_2022_interface::state::Mint, + conf: &ConfidentialTransferMintUpdate, +) -> Result, String> { + let authority = conf + .authority + .as_deref() + .map(Pubkey::from_str) + .transpose() + .map_err(|e| format!("authority: {e}"))?; + + let auditor_elgamal_pubkey = conf + .auditor_elgamal_pubkey + .as_deref() + .map(|key| { + let bytes = decode_confidential_key(key, 32) + .map_err(|e| format!("auditorElgamalPubkey: {e}"))?; + ElGamalPubkey::try_from(bytes.as_slice()) + .map_err(|e| format!("auditorElgamalPubkey: invalid ElGamal public key ({e})")) + }) + .transpose()?; + + let extension_types = [ExtensionType::ConfidentialTransferMint]; + let mint_len = + ExtensionType::try_calculate_account_len::( + &extension_types, + ) + .map_err(|e| format!("failed to size confidential mint: {e}"))?; + + let mut buffer = vec![0u8; mint_len]; + let mut state = + StateWithExtensionsMut::::unpack_uninitialized( + &mut buffer, + ) + .map_err(|e| format!("failed to init confidential mint buffer: {e}"))?; + + state.base = *base; + state.pack_base(); + state + .init_account_type() + .map_err(|e| format!("failed to set account type: {e}"))?; + + { + let ct = state + .init_extension::(false) + .map_err(|e| format!("failed to init confidential mint extension: {e}"))?; + ct.authority = authority.map(Into::into).unwrap_or_default(); + ct.auto_approve_new_accounts = conf.auto_approve_new_accounts.unwrap_or(true).into(); + ct.auditor_elgamal_pubkey = auditor_elgamal_pubkey + .map(|key| PodElGamalPubkey::from(key).into()) + .unwrap_or_default(); + } + + drop(state); + Ok(buffer) +} + +#[cfg(test)] +mod confidential_mint_tests { + use solana_zk_sdk::encryption::elgamal::ElGamalKeypair; + + use super::*; + + /// The auditor key has to be real ElGamal material: a well-formed 32-byte + /// string that is not a curve point is refused just like a short one, so a + /// mint never ships an auditor key no client can encrypt to. + #[test] + fn an_auditor_key_that_is_not_elgamal_material_is_refused() { + let base = spl_token_2022_interface::state::Mint { + is_initialized: true, + ..Default::default() + }; + + for auditor in [ + bs58::encode([7u8; 16]).into_string(), + bs58::encode([0xffu8; 32]).into_string(), + ] { + let error = build_confidential_mint_data( + &base, + &ConfidentialTransferMintUpdate { + auditor_elgamal_pubkey: Some(auditor), + ..Default::default() + }, + ) + .unwrap_err(); + assert!(error.starts_with("auditorElgamalPubkey:"), "got: {error}"); + } + + let auditor = ElGamalKeypair::new_rand(); + assert!( + build_confidential_mint_data( + &base, + &ConfidentialTransferMintUpdate { + auditor_elgamal_pubkey: Some( + bs58::encode(bytes_of(&PodElGamalPubkey::from(auditor.pubkey_owned()))) + .into_string() + ), + ..Default::default() + }, + ) + .is_ok(), + "a real ElGamal pubkey is accepted" + ); + } +} + /// Decrypt the confidential balances held on a Token-2022 token account. /// /// Backs the `surfnet_getConfidentialBalance` cheatcode, the read half of the @@ -1692,12 +1807,76 @@ impl MintAccount { } } + pub fn new(token_program_id: &Pubkey) -> Self { + if token_program_id == &spl_token_2022_interface::id() { + Self::SplToken2022(spl_token_2022_interface::state::Mint { + is_initialized: true, + ..Default::default() + }) + } else { + Self::SplToken(spl_token_interface::state::Mint { + is_initialized: true, + ..Default::default() + }) + } + } + pub fn decimals(&self) -> u8 { match self { Self::SplToken2022(mint) => mint.decimals, Self::SplToken(mint) => mint.decimals, } } + + pub fn set_decimals(&mut self, decimals: u8) { + match self { + Self::SplToken2022(mint) => mint.decimals = decimals, + Self::SplToken(mint) => mint.decimals = decimals, + } + } + + pub fn set_supply(&mut self, supply: u64) { + match self { + Self::SplToken2022(mint) => mint.supply = supply, + Self::SplToken(mint) => mint.supply = supply, + } + } + + pub fn set_mint_authority(&mut self, mint_authority: COption) { + match self { + Self::SplToken2022(mint) => mint.mint_authority = mint_authority, + Self::SplToken(mint) => mint.mint_authority = mint_authority, + } + } + + pub fn pack_into_vec(&self) -> Vec { + match self { + Self::SplToken2022(mint) => { + let mut dst = [0u8; spl_token_2022_interface::state::Mint::LEN]; + mint.pack_into_slice(&mut dst); + dst.to_vec() + } + Self::SplToken(mint) => { + let mut dst = [0u8; spl_token_interface::state::Mint::LEN]; + mint.pack_into_slice(&mut dst); + dst.to_vec() + } + } + } + + pub fn pack_into_preserving_extensions(&self, original: &[u8]) -> SurfpoolResult> { + let base_len = spl_token_interface::state::Mint::LEN; + if original.len() < base_len { + return Err(SurfpoolError::unpack_mint_account()); + } + + let mut data = original.to_vec(); + match self { + Self::SplToken2022(mint) => mint.pack_into_slice(&mut data[..base_len]), + Self::SplToken(mint) => mint.pack_into_slice(&mut data[..base_len]), + } + Ok(data) + } } pub struct GeyserAccountUpdate { diff --git a/crates/sdk-node/surfpool-sdk/kit/generated/ConfidentialTransferMintUpdate.ts b/crates/sdk-node/surfpool-sdk/kit/generated/ConfidentialTransferMintUpdate.ts new file mode 100644 index 000000000..eef48134e --- /dev/null +++ b/crates/sdk-node/surfpool-sdk/kit/generated/ConfidentialTransferMintUpdate.ts @@ -0,0 +1,28 @@ +// @generated by ts-rs from the Rust types in crates/types. +// Do not edit; run `npm run generate:kit-types` in crates/sdk-node instead. +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Writes the Token-2022 `ConfidentialTransferMint` extension on a mint created + * via `surfnet_setMint`. + * + * This is a test-only cheatcode: it writes the extension directly, bypassing + * the real on-chain `ConfidentialTransferInitializeMint` instruction, so a mint + * with a chosen auditor can be created without forking one from mainnet. + */ +export type ConfidentialTransferMintUpdate = { +/** + * The authority that approves new confidential accounts and updates this + * config (base58). Omitted leaves it null. + */ +authority?: string, +/** + * The auditor's ElGamal public key (base58 or base64, 32 bytes), which every + * confidential transfer on this mint also encrypts its amount to. Omitted + * leaves it null, i.e. no auditor. + */ +auditorElgamalPubkey?: string, +/** + * Whether new confidential accounts are approved on creation (default true). + */ +autoApproveNewAccounts?: boolean, }; diff --git a/crates/sdk-node/surfpool-sdk/kit/generated/MintUpdate.ts b/crates/sdk-node/surfpool-sdk/kit/generated/MintUpdate.ts new file mode 100644 index 000000000..7ac20f677 --- /dev/null +++ b/crates/sdk-node/surfpool-sdk/kit/generated/MintUpdate.ts @@ -0,0 +1,24 @@ +// @generated by ts-rs from the Rust types in crates/types. +// Do not edit; run `npm run generate:kit-types` in crates/sdk-node instead. +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfidentialTransferMintUpdate } from "./ConfidentialTransferMintUpdate.js"; + +export type MintUpdate = { +/** + * providing this value sets the number of decimals of the mint + */ +decimals?: number, +/** + * providing this value sets the mint authority: a base58 pubkey, or the + * literal string "null" to clear the authority + */ +mintAuthority?: string, +/** + * providing this value sets the total supply of the mint + */ +supply?: number | bigint, +/** + * providing this value writes the Token-2022 confidential-transfer mint + * extension (Token-2022 only) + */ +confidential?: ConfidentialTransferMintUpdate, }; diff --git a/crates/sdk-node/surfpool-sdk/kit/generated/index.ts b/crates/sdk-node/surfpool-sdk/kit/generated/index.ts index e610e03d1..24db7e049 100644 --- a/crates/sdk-node/surfpool-sdk/kit/generated/index.ts +++ b/crates/sdk-node/surfpool-sdk/kit/generated/index.ts @@ -6,6 +6,7 @@ export type * from "./AccountUpdate.js"; export type * from "./CheatcodeControlConfig.js"; export type * from "./ConfidentialBalanceKeys.js"; export type * from "./ConfidentialTransferAccountUpdate.js"; +export type * from "./ConfidentialTransferMintUpdate.js"; export type * from "./DeriveConfidentialKeysResponse.js"; export type * from "./ExportSnapshotConfig.js"; export type * from "./ExportSnapshotFilter.js"; @@ -13,6 +14,7 @@ export type * from "./ExportSnapshotScope.js"; export type * from "./GetConfidentialBalanceResponse.js"; export type * from "./GetStreamedAccountsResponse.js"; export type * from "./GetSurfnetInfoResponse.js"; +export type * from "./MintUpdate.js"; export type * from "./OfflineAccountConfig.js"; export type * from "./OverrideInstance.js"; export type * from "./ParsedAccount.js"; diff --git a/crates/sdk-node/surfpool-sdk/kit/generated/methods.ts b/crates/sdk-node/surfpool-sdk/kit/generated/methods.ts index a60473781..b87acbd99 100644 --- a/crates/sdk-node/surfpool-sdk/kit/generated/methods.ts +++ b/crates/sdk-node/surfpool-sdk/kit/generated/methods.ts @@ -22,6 +22,7 @@ export const SURFNET_CHEATCODE_METHODS = [ "surfnet_resetNetwork", "surfnet_resumeClock", "surfnet_setAccount", + "surfnet_setMint", "surfnet_setProgramAuthority", "surfnet_setSupply", "surfnet_setTokenAccount", diff --git a/crates/sdk-node/surfpool-sdk/kit/types/api.ts b/crates/sdk-node/surfpool-sdk/kit/types/api.ts index 0c8de0433..36fc75871 100644 --- a/crates/sdk-node/surfpool-sdk/kit/types/api.ts +++ b/crates/sdk-node/surfpool-sdk/kit/types/api.ts @@ -10,6 +10,7 @@ import type { GetConfidentialBalanceResponse, GetStreamedAccountsResponse, GetSurfnetInfoResponse, + MintUpdate, OfflineAccountConfig, ResetAccountConfig, RpcProfileResultConfig, @@ -111,6 +112,9 @@ export type SurfnetDisableCheatcodeApi = { export type SurfnetSetAccountApi = { setAccount(pubkey: Address, update: AccountUpdate): null; }; +export type SurfnetSetMintApi = { + setMint(mint: Address, update: MintUpdate, tokenProgram?: Address): null; +}; export type SurfnetSetTokenAccountApi = { setTokenAccount(owner: Address, mint: Address, update: TokenAccountUpdate, tokenProgram?: Address): null; }; @@ -217,6 +221,7 @@ export type SurfnetCheatcodesApi = SurfnetCloneProgramAccountApi & SurfnetResetNetworkApi & SurfnetResumeClockApi & SurfnetSetAccountApi & + SurfnetSetMintApi & SurfnetSetProgramAuthorityApi & SurfnetSetSupplyApi & SurfnetSetTokenAccountApi & diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index c951dd8e8..a1aee0987 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -1235,6 +1235,53 @@ pub struct ConfidentialTransferAccountUpdate { pub maximum_pending_balance_credit_counter: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr( + feature = "ts-bindings", + derive(ts_rs::TS), + ts(export, optional_fields) +)] +pub struct MintUpdate { + /// providing this value sets the number of decimals of the mint + pub decimals: Option, + /// providing this value sets the mint authority: a base58 pubkey, or the + /// literal string "null" to clear the authority + #[cfg_attr(feature = "ts-bindings", ts(optional, type = "string"))] + pub mint_authority: Option, + /// providing this value sets the total supply of the mint + #[cfg_attr(feature = "ts-bindings", ts(optional, type = "number | bigint"))] + pub supply: Option, + /// providing this value writes the Token-2022 confidential-transfer mint + /// extension (Token-2022 only) + pub confidential: Option, +} + +/// Writes the Token-2022 `ConfidentialTransferMint` extension on a mint created +/// via `surfnet_setMint`. +/// +/// This is a test-only cheatcode: it writes the extension directly, bypassing +/// the real on-chain `ConfidentialTransferInitializeMint` instruction, so a mint +/// with a chosen auditor can be created without forking one from mainnet. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr( + feature = "ts-bindings", + derive(ts_rs::TS), + ts(export, optional_fields) +)] +pub struct ConfidentialTransferMintUpdate { + /// The authority that approves new confidential accounts and updates this + /// config (base58). Omitted leaves it null. + pub authority: Option, + /// The auditor's ElGamal public key (base58 or base64, 32 bytes), which every + /// confidential transfer on this mint also encrypts its amount to. Omitted + /// leaves it null, i.e. no auditor. + pub auditor_elgamal_pubkey: Option, + /// Whether new confidential accounts are approved on creation (default true). + pub auto_approve_new_accounts: Option, +} + /// The owner's confidential-transfer secrets, passed to /// `surfnet_getConfidentialBalance` so it can decrypt the account. /// @@ -1783,7 +1830,7 @@ pub enum CheatcodeFilter { /// `surfpool-core/src/rpc/surfnet_cheatcodes.rs` asserts it matches the /// methods actually registered by the `SurfnetCheatcodes` trait, so adding, /// removing, or renaming a cheatcode without updating this list fails CI. -pub const SURFNET_CHEATCODE_METHODS: [&str; 28] = [ +pub const SURFNET_CHEATCODE_METHODS: [&str; 29] = [ "surfnet_cloneProgramAccount", "surfnet_deriveConfidentialKeys", "surfnet_disableCheatcode", @@ -1805,6 +1852,7 @@ pub const SURFNET_CHEATCODE_METHODS: [&str; 28] = [ "surfnet_resetNetwork", "surfnet_resumeClock", "surfnet_setAccount", + "surfnet_setMint", "surfnet_setProgramAuthority", "surfnet_setSupply", "surfnet_setTokenAccount",