diff --git a/Cargo.lock b/Cargo.lock index 3d3dcbd..4e39c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,16 +456,13 @@ checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" [[package]] name = "async-compression" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75" +checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" dependencies = [ - "brotli", "compression-codecs", "compression-core", - "flate2", "futures-core", - "memchr", "pin-project-lite", "tokio", ] @@ -803,23 +800,21 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905" +checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" dependencies = [ "brotli", "compression-core", "flate2", - "futures-core", "memchr", - "pin-project-lite", ] [[package]] name = "compression-core" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" [[package]] name = "console_error_panic_hook" @@ -1322,7 +1317,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", ] [[package]] @@ -2219,9 +2214,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -5174,6 +5169,34 @@ dependencies = [ "solana-address-book", "solana-sdk", "strum", + "testsvm-assertions", + "testsvm-core", + "testsvm-spl", +] + +[[package]] +name = "testsvm-assertions" +version = "0.1.0" +dependencies = [ + "anchor-spl", + "anyhow", + "litesvm", + "solana-sdk", + "testsvm-core", + "testsvm-spl", +] + +[[package]] +name = "testsvm-core" +version = "0.1.1" +dependencies = [ + "anchor-lang", + "anchor-utils", + "anyhow", + "colored", + "litesvm", + "solana-address-book", + "solana-sdk", ] [[package]] @@ -5189,6 +5212,18 @@ dependencies = [ "testsvm", ] +[[package]] +name = "testsvm-spl" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "anyhow", + "solana-address-book", + "solana-sdk", + "testsvm-core", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5463,11 +5498,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -5889,13 +5924,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.3", -] +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] name = "writeable" diff --git a/crates/solana-address-book/src/address_book.rs b/crates/solana-address-book/src/address_book.rs index c7ce771..735fb4f 100644 --- a/crates/solana-address-book/src/address_book.rs +++ b/crates/solana-address-book/src/address_book.rs @@ -1,6 +1,6 @@ //! Core address book implementation for managing Solana addresses with labels and roles. -use crate::pda_seeds::{SeedPart, find_pda_with_bump_and_strings}; +use crate::pda_seeds::find_pda_with_bump_and_strings; use crate::registered_address::{AddressRole, RegisteredAddress}; use anchor_lang::prelude::*; use anchor_lang::solana_program::system_program; @@ -174,65 +174,6 @@ impl AddressBook { self.add(pubkey, label, RegisteredAddress::wallet(pubkey)) } - /// Adds a token mint address to the address book. - /// - /// # Errors - /// - /// Returns an error if the label already exists with a different address. - /// - /// # Example - /// - /// ``` - /// use solana_address_book::AddressBook; - /// use anchor_lang::prelude::*; - /// - /// let mut book = AddressBook::new(); - /// let mint = Pubkey::new_unique(); - /// - /// book.add_mint(mint, "usdc_mint".to_string()).unwrap(); - /// assert_eq!(book.get_label(&mint), "usdc_mint"); - /// ``` - pub fn add_mint(&mut self, pubkey: Pubkey, label: String) -> Result<()> { - self.add(pubkey, label, RegisteredAddress::mint(pubkey)) - } - - /// Adds an Associated Token Account (ATA) address to the address book. - /// - /// # Arguments - /// - /// * `pubkey` - The ATA's public key - /// * `label` - Human-readable label for the ATA - /// * `mint` - The token mint public key - /// * `owner` - The owner's public key - /// - /// # Errors - /// - /// Returns an error if the label already exists with a different address. - /// - /// # Example - /// - /// ``` - /// use solana_address_book::AddressBook; - /// use anchor_lang::prelude::*; - /// - /// let mut book = AddressBook::new(); - /// let ata = Pubkey::new_unique(); - /// let mint = Pubkey::new_unique(); - /// let owner = Pubkey::new_unique(); - /// - /// book.add_ata(ata, "alice_usdc".to_string(), mint, owner).unwrap(); - /// assert_eq!(book.get_label(&ata), "alice_usdc"); - /// ``` - pub fn add_ata( - &mut self, - pubkey: Pubkey, - label: String, - mint: Pubkey, - owner: Pubkey, - ) -> Result<()> { - self.add(pubkey, label, RegisteredAddress::ata(pubkey, mint, owner)) - } - /// Adds a custom role address to the address book. /// /// Use this for addresses that don't fit into the standard categories. @@ -357,7 +298,7 @@ impl AddressBook { /// # Example /// /// ``` - /// use solana_address_book::{AddressBook, SeedPart}; + /// use solana_address_book::AddressBook; /// use anchor_lang::prelude::*; /// /// let mut book = AddressBook::new(); @@ -366,7 +307,7 @@ impl AddressBook { /// /// let (pda, bump) = book.find_pda_with_bump( /// "user_vault", - /// &[&"vault" as &dyn SeedPart, &user as &dyn SeedPart], + /// &[b"vault", user.as_ref()], /// program /// ).unwrap(); /// @@ -375,7 +316,7 @@ impl AddressBook { pub fn find_pda_with_bump( &mut self, label: &str, - seeds: &[&dyn SeedPart], + seeds: &[&[u8]], program_id: Pubkey, ) -> Result<(Pubkey, u8)> { // Use the helper function from pda_seeds module @@ -580,13 +521,13 @@ impl AddressBook { /// # Example /// /// ``` - /// use solana_address_book::AddressBook; + /// use solana_address_book::{AddressBook, RegisteredAddress}; /// use anchor_lang::prelude::*; /// /// let mut book = AddressBook::new(); /// let token = Pubkey::new_unique(); /// - /// book.add_mint(token, "my_token".to_string()).unwrap(); + /// book.add(token, "my_token".to_string(), RegisteredAddress::mint(token)).unwrap(); /// /// let text = format!("Transfer to {}", token); /// let formatted = book.replace_addresses_in_text(&text); @@ -628,12 +569,13 @@ impl AddressBook { /// # Example /// /// ``` - /// use solana_address_book::AddressBook; + /// use solana_address_book::{AddressBook, RegisteredAddress}; /// use anchor_lang::prelude::*; /// /// let mut book = AddressBook::new(); /// book.add_wallet(Pubkey::new_unique(), "alice".to_string()).unwrap(); - /// book.add_mint(Pubkey::new_unique(), "usdc".to_string()).unwrap(); + /// let mint = Pubkey::new_unique(); + /// book.add(mint, "usdc".to_string(), RegisteredAddress::mint(mint)).unwrap(); /// /// // Prints a formatted table of all addresses /// book.print_all(); @@ -863,31 +805,35 @@ mod tests { } #[test] - fn test_add_mint() { + fn test_add_mint() -> Result<()> { let mut book = AddressBook::new(); let pubkey = Pubkey::new_unique(); - book.add_mint(pubkey, "test_mint".to_string()).unwrap(); + book.add( + pubkey, + "test_mint".to_string(), + RegisteredAddress::mint(pubkey), + )?; let (label, registered) = book.get_first(&pubkey).unwrap(); assert_eq!(*label, "test_mint"); matches!(registered.role, AddressRole::Mint); + + Ok(()) } #[test] - fn test_add_ata() { + fn test_add_ata() -> Result<()> { let mut book = AddressBook::new(); let ata_pubkey = Pubkey::new_unique(); let mint_pubkey = Pubkey::new_unique(); let owner_pubkey = Pubkey::new_unique(); - book.add_ata( + book.add( ata_pubkey, "test_ata".to_string(), - mint_pubkey, - owner_pubkey, - ) - .unwrap(); + RegisteredAddress::ata(ata_pubkey, mint_pubkey, owner_pubkey), + )?; let (label, registered) = book.get_first(&ata_pubkey).unwrap(); assert_eq!(*label, "test_ata"); @@ -896,7 +842,8 @@ mod tests { assert_eq!(*owner, owner_pubkey); } else { panic!("Expected ATA role"); - } + }; + Ok(()) } #[test] @@ -968,7 +915,8 @@ mod tests { book.add_wallet(wallet1, "wallet1".to_string()).unwrap(); book.add_wallet(wallet2, "wallet2".to_string()).unwrap(); - book.add_mint(mint, "mint1".to_string()).unwrap(); + book.add(mint, "mint1".to_string(), RegisteredAddress::mint(mint)) + .unwrap(); let wallets = book.get_all_by_role_type("wallet"); assert_eq!(wallets.len(), 2); @@ -983,9 +931,7 @@ mod tests { #[test] fn test_pda_creation() { let program_id = Pubkey::new_unique(); - let seeds: Vec<&dyn SeedPart> = vec![&"test", &"seed"]; - - let (_pubkey, bump, registered) = RegisteredAddress::pda(&seeds, &program_id); + let (_pubkey, bump, registered) = RegisteredAddress::pda(&[b"test", b"seed"], &program_id); if let AddressRole::Pda { seeds: pda_seeds, diff --git a/crates/solana-address-book/src/lib.rs b/crates/solana-address-book/src/lib.rs index f705763..68fd99f 100644 --- a/crates/solana-address-book/src/lib.rs +++ b/crates/solana-address-book/src/lib.rs @@ -32,7 +32,7 @@ //! //! // Add a token mint //! let token_mint = Pubkey::new_unique(); -//! book.add_mint(token_mint, "usdc_mint".to_string()).unwrap(); +//! book.add(token_mint, "usdc_mint".to_string(), RegisteredAddress::mint(token_mint)).unwrap(); //! //! // Get a formatted label for display //! println!("User address: {}", book.format_address(&user)); @@ -43,7 +43,7 @@ //! ### Managing Different Address Types //! //! ```rust -//! use solana_address_book::AddressBook; +//! use solana_address_book::{AddressBook, RegisteredAddress}; //! use anchor_lang::prelude::*; //! //! let mut book = AddressBook::new(); @@ -56,7 +56,7 @@ //! let ata = Pubkey::new_unique(); //! let mint = Pubkey::new_unique(); //! let owner = Pubkey::new_unique(); -//! book.add_ata(ata, "alice_usdc_ata".to_string(), mint, owner).unwrap(); +//! book.add(ata, "alice_usdc_ata".to_string(), RegisteredAddress::ata(ata, mint, owner)).unwrap(); //! //! // Add a Program Derived Address (PDA) //! let pda = Pubkey::new_unique(); @@ -109,23 +109,23 @@ //! ### PDA Creation and Registration //! //! ```rust -//! use solana_address_book::{AddressBook, RegisteredAddress, SeedPart}; +//! use solana_address_book::{AddressBook, RegisteredAddress}; //! use anchor_lang::prelude::*; //! //! let mut book = AddressBook::new(); //! let program_id = Pubkey::new_unique(); //! //! // Create and register a PDA in one step +//! let user = Pubkey::new_unique(); //! let (pda_key, bump) = book.find_pda_with_bump( //! "user_vault", -//! &[&"vault", &Pubkey::new_unique()], +//! &[b"vault", user.as_ref()], //! program_id //! ).unwrap(); //! //! // Or create a PDA manually -//! let seeds: Vec<&dyn SeedPart> = vec![&"config", &"v1"]; //! let (pubkey, bump, registered) = RegisteredAddress::pda( -//! &seeds, +//! &[b"config", b"v1"], //! &program_id //! ); //! book.add(pubkey, "config_account".to_string(), registered).unwrap(); @@ -134,12 +134,12 @@ //! ### Text Processing and Display //! //! ```rust -//! use solana_address_book::AddressBook; +//! use solana_address_book::{AddressBook, RegisteredAddress}; //! use anchor_lang::prelude::*; //! //! let mut book = AddressBook::new(); //! let token = Pubkey::new_unique(); -//! book.add_mint(token, "my_token".to_string()).unwrap(); +//! book.add(token, "my_token".to_string(), RegisteredAddress::mint(token)).unwrap(); //! //! // Replace addresses in text with their labels //! let log = format!("Transfer from {} to {}", Pubkey::new_unique(), token); @@ -155,7 +155,7 @@ //! This crate is designed to work seamlessly with Solana testing frameworks: //! //! ```rust -//! use solana_address_book::AddressBook; +//! use solana_address_book::{AddressBook, RegisteredAddress}; //! use anchor_lang::prelude::*; //! //! fn setup_test_environment() -> AddressBook { @@ -170,7 +170,8 @@ //! //! // Track all test tokens //! let test_token = Pubkey::new_unique(); -//! book.add_mint(test_token, "test_token".to_string()).unwrap(); +//! book.add(test_token, "test_token".to_string(), RegisteredAddress::mint(test_token)) +//! .unwrap(); //! //! book //! } @@ -181,7 +182,5 @@ pub mod pda_seeds; pub mod registered_address; pub use address_book::AddressBook; -pub use pda_seeds::{ - DerivedPda, SeedPart, find_pda_with_bump, find_pda_with_bump_and_strings, seed_to_string, -}; +pub use pda_seeds::{DerivedPda, find_pda_with_bump_and_strings, seed_to_string}; pub use registered_address::{AddressRole, RegisteredAddress}; diff --git a/crates/solana-address-book/src/pda_seeds.rs b/crates/solana-address-book/src/pda_seeds.rs index eca45e8..6261bbb 100644 --- a/crates/solana-address-book/src/pda_seeds.rs +++ b/crates/solana-address-book/src/pda_seeds.rs @@ -37,14 +37,8 @@ impl DerivedPda { } } -/// Trait for types that can be used as PDA seeds -pub trait SeedPart: AsRef<[u8]> {} - -// Blanket implementation for all types that can be referenced as byte slices -impl + ?Sized> SeedPart for T {} - /// Convert a seed to a string representation for debugging -pub fn seed_to_string(seed: &dyn SeedPart) -> String { +pub fn seed_to_string>(seed: T) -> String { // Try to convert common types to readable strings let bytes = seed.as_ref(); @@ -66,47 +60,18 @@ pub fn seed_to_string(seed: &dyn SeedPart) -> String { hex::encode(bytes) } -/// Find a PDA with bump given seeds and program ID -/// -/// This function calculates a Program Derived Address (PDA) and its bump seed -/// from the provided seeds and program ID. -/// -/// # Arguments -/// * `seeds` - Array of seed parts that implement the SeedPart trait -/// * `program_id` - The program ID to derive the address for -/// -/// # Returns -/// * `(Pubkey, u8)` - The derived public key and bump seed -/// -/// # Example -/// ``` -/// use anchor_lang::prelude::*; -/// use solana_address_book::pda_seeds::{find_pda_with_bump, SeedPart}; -/// -/// let program_id = Pubkey::new_unique(); -/// let seeds: Vec<&dyn SeedPart> = vec![&"user", &"profile"]; -/// let (pda, bump) = find_pda_with_bump(&seeds, &program_id); -/// ``` -pub fn find_pda_with_bump(seeds: &[&dyn SeedPart], program_id: &Pubkey) -> (Pubkey, u8) { - // Convert seeds to byte slices for PDA calculation - let seed_bytes: Vec<&[u8]> = seeds.iter().map(|s| s.as_ref()).collect(); - - // Find the PDA and bump - Pubkey::find_program_address(&seed_bytes, program_id) -} - /// Find a PDA with bump and return along with seed strings for display /// /// This function is similar to `find_pda_with_bump` but also returns /// string representations of the seeds for debugging/display purposes. /// /// # Arguments -/// * `seeds` - Array of seed parts that implement the SeedPart trait +/// * `seeds` - Array of items that can be referenced as byte slices /// * `program_id` - The program ID to derive the address for /// /// # Returns /// * `DerivedPda` - Struct containing the derived public key, bump seed, seed strings, and raw seeds -pub fn find_pda_with_bump_and_strings(seeds: &[&dyn SeedPart], program_id: &Pubkey) -> DerivedPda { +pub fn find_pda_with_bump_and_strings(seeds: &[&[u8]], program_id: &Pubkey) -> DerivedPda { // Convert seeds to byte slices for PDA calculation let seed_bytes: Vec<&[u8]> = seeds.iter().map(|s| s.as_ref()).collect(); @@ -114,10 +79,10 @@ pub fn find_pda_with_bump_and_strings(seeds: &[&dyn SeedPart], program_id: &Pubk let (pubkey, bump) = Pubkey::find_program_address(&seed_bytes, program_id); // Convert seeds to strings for display - let seed_strings: Vec = seeds.iter().map(|s| seed_to_string(*s)).collect(); + let seed_strings: Vec = seeds.iter().map(seed_to_string).collect(); // Store owned copies of the seed bytes - let seeds_owned: Vec> = seed_bytes.iter().map(|s| s.to_vec()).collect(); + let seeds_owned: Vec> = seeds.iter().map(|s| s.as_ref().to_vec()).collect(); DerivedPda { key: pubkey, @@ -135,12 +100,11 @@ mod tests { fn test_string_seed() { let program_id = Pubkey::new_unique(); let seed = "test_seed"; - let seeds: Vec<&dyn SeedPart> = vec![&seed]; - let (pda, bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, bump) = Pubkey::find_program_address(&[seed.as_bytes()], &program_id); // Verify the PDA is deterministic - let (pda2, bump2) = find_pda_with_bump(&seeds, &program_id); + let (pda2, bump2) = Pubkey::find_program_address(&[seed.as_bytes()], &program_id); assert_eq!(pda, pda2); assert_eq!(bump, bump2); @@ -153,13 +117,13 @@ mod tests { let program_id = Pubkey::new_unique(); let seed1 = "user"; let seed2 = "profile"; - let seeds: Vec<&dyn SeedPart> = vec![&seed1, &seed2]; + let seeds: Vec<&[u8]> = vec![seed1.as_bytes(), seed2.as_bytes()]; - let (pda, _bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, _bump) = Pubkey::find_program_address(&seeds, &program_id); // Verify different seed order produces different PDA - let seeds_reversed: Vec<&dyn SeedPart> = vec![&seed2, &seed1]; - let (pda_reversed, _) = find_pda_with_bump(&seeds_reversed, &program_id); + let seeds_reversed: Vec<&[u8]> = vec![seed2.as_bytes(), seed1.as_bytes()]; + let (pda_reversed, _) = Pubkey::find_program_address(&seeds_reversed, &program_id); assert_ne!(pda, pda_reversed); } @@ -167,12 +131,12 @@ mod tests { fn test_pubkey_seed() { let program_id = Pubkey::new_unique(); let user_pubkey = Pubkey::new_unique(); - let seeds: Vec<&dyn SeedPart> = vec![&user_pubkey]; + let seeds: Vec<&[u8]> = vec![user_pubkey.as_ref()]; - let (pda, bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, bump) = Pubkey::find_program_address(&seeds, &program_id); // Verify the PDA is deterministic with pubkey seed - let (pda2, bump2) = find_pda_with_bump(&seeds, &program_id); + let (pda2, bump2) = Pubkey::find_program_address(&seeds, &program_id); assert_eq!(pda, pda2); assert_eq!(bump, bump2); } @@ -185,9 +149,9 @@ mod tests { let id: u64 = 12345; let id_bytes = id.to_le_bytes(); - let seeds: Vec<&dyn SeedPart> = vec![&prefix, &owner, &id_bytes]; + let seeds: Vec<&[u8]> = vec![prefix.as_bytes(), owner.as_ref(), &id_bytes]; - let (pda, _bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, _bump) = Pubkey::find_program_address(&seeds, &program_id); assert!(!pda.is_on_curve()); } @@ -195,9 +159,9 @@ mod tests { fn test_byte_array_seed() { let program_id = Pubkey::new_unique(); let bytes: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; - let seeds: Vec<&dyn SeedPart> = vec![&bytes]; + let seeds: Vec<&[u8]> = vec![&bytes]; - let (pda, _bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, _bump) = Pubkey::find_program_address(&seeds, &program_id); assert!(!pda.is_on_curve()); } @@ -205,9 +169,9 @@ mod tests { fn test_slice_seed() { let program_id = Pubkey::new_unique(); let vec_bytes = vec![9, 8, 7, 6, 5, 4, 3, 2, 1]; - let seeds: Vec<&dyn SeedPart> = vec![&vec_bytes]; + let seeds: Vec<&[u8]> = vec![&vec_bytes]; - let (pda, _bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, _bump) = Pubkey::find_program_address(&seeds, &program_id); assert!(!pda.is_on_curve()); } @@ -219,7 +183,7 @@ mod tests { let seed3: u32 = 42; let seed3_bytes = seed3.to_le_bytes(); - let seeds: Vec<&dyn SeedPart> = vec![&seed1, &seed2, &seed3_bytes]; + let seeds: Vec<&[u8]> = vec![seed1.as_bytes(), seed2.as_ref(), &seed3_bytes]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); @@ -229,7 +193,7 @@ mod tests { assert_eq!(derived_pda.seed_strings[1], seed2.to_string()); // Verify the PDA matches what we'd get from the basic function - let (pda2, bump2) = find_pda_with_bump(&seeds, &program_id); + let (pda2, bump2) = Pubkey::find_program_address(&seeds, &program_id); assert_eq!(derived_pda.key, pda2); assert_eq!(derived_pda.bump, bump2); @@ -252,9 +216,9 @@ mod tests { #[test] fn test_empty_seeds() { let program_id = Pubkey::new_unique(); - let seeds: Vec<&dyn SeedPart> = vec![]; + let seeds: Vec<&[u8]> = vec![]; - let (pda, _bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, _bump) = Pubkey::find_program_address(&seeds, &program_id); // Empty seeds should still produce a valid PDA assert!(!pda.is_on_curve()); @@ -265,9 +229,9 @@ mod tests { let program_id = Pubkey::new_unique(); // Max seed length is 32 bytes per seed let max_seed = vec![0u8; 32]; - let seeds: Vec<&dyn SeedPart> = vec![&max_seed]; + let seeds: Vec<&[u8]> = vec![&max_seed]; - let (pda, _bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, _bump) = Pubkey::find_program_address(&seeds, &program_id); assert!(!pda.is_on_curve()); } @@ -276,10 +240,10 @@ mod tests { let program_id1 = Pubkey::new_unique(); let program_id2 = Pubkey::new_unique(); let seed = "same_seed"; - let seeds: Vec<&dyn SeedPart> = vec![&seed]; + let seeds: Vec<&[u8]> = vec![seed.as_bytes()]; - let (pda1, _) = find_pda_with_bump(&seeds, &program_id1); - let (pda2, _) = find_pda_with_bump(&seeds, &program_id2); + let (pda1, _) = Pubkey::find_program_address(&seeds, &program_id1); + let (pda2, _) = Pubkey::find_program_address(&seeds, &program_id2); // Same seeds with different programs should produce different PDAs assert_ne!(pda1, pda2); @@ -289,11 +253,11 @@ mod tests { fn test_bump_determinism() { let program_id = Pubkey::new_unique(); let seed = "deterministic"; - let seeds: Vec<&dyn SeedPart> = vec![&seed]; + let seeds: Vec<&[u8]> = vec![seed.as_bytes()]; // Run multiple times to ensure determinism let results: Vec<(Pubkey, u8)> = (0..10) - .map(|_| find_pda_with_bump(&seeds, &program_id)) + .map(|_| Pubkey::find_program_address(&seeds, &program_id)) .collect(); // All results should be identical @@ -308,9 +272,9 @@ mod tests { // Test with a known program ID to ensure consistency let program_id = Pubkey::default(); // All zeros let seed = "test"; - let seeds: Vec<&dyn SeedPart> = vec![&seed]; + let seeds: Vec<&[u8]> = vec![seed.as_bytes()]; - let (pda, bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, bump) = Pubkey::find_program_address(&seeds, &program_id); // The PDA should be consistent for these known inputs let (expected_pda, expected_bump) = @@ -325,7 +289,7 @@ mod tests { let program_id = Pubkey::new_unique(); let seed1 = "vault"; let seed2 = Pubkey::new_unique(); - let seeds: Vec<&dyn SeedPart> = vec![&seed1, &seed2]; + let seeds: Vec<&[u8]> = vec![seed1.as_ref(), seed2.as_ref()]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); @@ -351,7 +315,11 @@ mod tests { let byte_seed: u64 = 999; let byte_seed_bytes = byte_seed.to_le_bytes(); - let seeds: Vec<&dyn SeedPart> = vec![&string_seed, &pubkey_seed, &byte_seed_bytes]; + let seeds: Vec<&[u8]> = vec![ + string_seed.as_bytes(), + pubkey_seed.as_ref(), + &byte_seed_bytes, + ]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); // Check all fields are populated correctly @@ -389,7 +357,7 @@ mod tests { ); // Using our helper with the same seeds - let seeds: Vec<&dyn SeedPart> = vec![&"Miner", &replica_quarry, &merge_miner]; + let seeds: Vec<&[u8]> = vec![b"Miner", replica_quarry.as_ref(), merge_miner.as_ref()]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); // Verify they produce the same result @@ -428,7 +396,12 @@ mod tests { // Using our helper let vault_id_bytes = vault_id.to_le_bytes(); - let seeds: Vec<&dyn SeedPart> = vec![&"vault", &authority, &token_mint, &vault_id_bytes]; + let seeds: Vec<&[u8]> = vec![ + b"vault", + authority.as_ref(), + token_mint.as_ref(), + &vault_id_bytes, + ]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); // Must produce identical results @@ -454,7 +427,11 @@ mod tests { ); // Using our helper - let seeds: Vec<&dyn SeedPart> = vec![&"metadata", &metadata_program_id, &mint_pubkey]; + let seeds: Vec<&[u8]> = vec![ + b"metadata", + metadata_program_id.as_ref(), + mint_pubkey.as_ref(), + ]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &metadata_program_id); // Verify exact match @@ -482,7 +459,7 @@ mod tests { ); // Using our helper - let seeds: Vec<&dyn SeedPart> = vec![&wallet, &token_program_id, &mint]; + let seeds: Vec<&[u8]> = vec![wallet.as_ref(), token_program_id.as_ref(), mint.as_ref()]; let derived_pda = find_pda_with_bump_and_strings(&seeds, &associated_token_program_id); // Must match the expected ATA @@ -524,11 +501,15 @@ mod tests { // Using our helper with same seeds let escrow_id_bytes = escrow_id.to_le_bytes(); let timestamp_bytes = timestamp.to_le_bytes(); - let seeds: Vec<&dyn SeedPart> = - vec![&"escrow", &initializer, &escrow_id_bytes, ×tamp_bytes]; + let seeds: Vec<&[u8]> = vec![ + b"escrow", + initializer.as_ref(), + &escrow_id_bytes, + ×tamp_bytes, + ]; // First, use the basic function - let (pda_basic, bump_basic) = find_pda_with_bump(&seeds, &program_id); + let (pda_basic, bump_basic) = Pubkey::find_program_address(&seeds, &program_id); assert_eq!(pda_basic, expected_escrow_pda); assert_eq!(bump_basic, expected_bump); diff --git a/crates/solana-address-book/src/registered_address.rs b/crates/solana-address-book/src/registered_address.rs index fb9b3e5..926a76d 100644 --- a/crates/solana-address-book/src/registered_address.rs +++ b/crates/solana-address-book/src/registered_address.rs @@ -1,6 +1,6 @@ //! Registered address types and utilities for the address book. -use crate::pda_seeds::{SeedPart, find_pda_with_bump_and_strings}; +use crate::pda_seeds::find_pda_with_bump_and_strings; use anchor_lang::prelude::*; /// Role type for registered addresses, defining the purpose of each address @@ -168,15 +168,14 @@ impl RegisteredAddress { /// # Example /// ``` /// use anchor_lang::prelude::*; - /// use solana_address_book::{RegisteredAddress, SeedPart}; + /// use solana_address_book::RegisteredAddress; /// /// let program_id = Pubkey::new_unique(); /// let user = Pubkey::new_unique(); - /// let seeds: Vec<&dyn SeedPart> = vec![&"vault", &user]; /// - /// let (pda_key, bump, registered) = RegisteredAddress::pda(&seeds, &program_id); + /// let (pda_key, bump, registered) = RegisteredAddress::pda(&[b"vault", user.as_ref()], &program_id); /// ``` - pub fn pda(seeds: &[&dyn SeedPart], program_id: &Pubkey) -> (Pubkey, u8, Self) { + pub fn pda(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8, Self) { let derived_pda = find_pda_with_bump_and_strings(seeds, program_id); ( @@ -317,9 +316,7 @@ mod tests { #[test] fn test_pda_creation() { let program_id = Pubkey::new_unique(); - let seeds: Vec<&dyn SeedPart> = vec![&"test", &"seed"]; - - let (pubkey, bump, registered) = RegisteredAddress::pda(&seeds, &program_id); + let (pubkey, bump, registered) = RegisteredAddress::pda(&[b"test", b"seed"], &program_id); assert_eq!(registered.key, pubkey); if let AddressRole::Pda { diff --git a/crates/testsvm-assertions/Cargo.toml b/crates/testsvm-assertions/Cargo.toml new file mode 100644 index 0000000..25445bc --- /dev/null +++ b/crates/testsvm-assertions/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "testsvm-assertions" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +authors = ["Ian Macalinao "] +description = "Assertion helpers for testing transaction results in TestSVM" +documentation = "https://docs.rs/testsvm-assertions" +homepage = "https://github.com/macalinao/testsvm" +repository = "https://github.com/macalinao/testsvm" +readme = "README.md" +keywords = [ + "ian-macalinao", + "solana", + "svm", + "testing", + "assertions", + "testsvm" +] +categories = ["development-tools::testing"] + +[dependencies] +testsvm-core = { version = "0.1.1", path = "../testsvm-core" } +anyhow = "1.0" +litesvm = "0.6.1" +solana-sdk = "2.2.1" + +[dev-dependencies] +testsvm-spl = { version = "0.1.0", path = "../testsvm-spl" } +anchor-spl = { version = "0.31.1", features = ["token"] } diff --git a/crates/testsvm-assertions/README.md b/crates/testsvm-assertions/README.md new file mode 100644 index 0000000..f1af24c --- /dev/null +++ b/crates/testsvm-assertions/README.md @@ -0,0 +1,18 @@ +# testsvm-assertions + +[![Crates.io](https://img.shields.io/crates/v/testsvm-assertions.svg)](https://crates.io/crates/testsvm-assertions) +[![Documentation](https://docs.rs/testsvm-assertions/badge.svg)](https://docs.rs/testsvm-assertions) + +Assertion helpers for testing transaction results in TestSVM. + +This crate provides traits and types for asserting expected transaction outcomes, including methods for verifying that transactions succeed or fail with specific errors. These assertions are particularly useful in test environments where you need to verify that your program behaves correctly under various conditions. + +## Features + +- **Success/Failure Assertions**: Verify transactions succeed or fail as expected +- **Error Matching**: Check for specific error types including Anchor errors +- **Type-safe API**: Compile-time guarantees for assertion chains + +## License + +Copyright (c) 2025 Ian Macalinao. Licensed under the Apache License, Version 2.0. diff --git a/crates/testsvm/src/assertions.rs b/crates/testsvm-assertions/src/lib.rs similarity index 97% rename from crates/testsvm/src/assertions.rs rename to crates/testsvm-assertions/src/lib.rs index ff84185..8683ccb 100644 --- a/crates/testsvm/src/assertions.rs +++ b/crates/testsvm-assertions/src/lib.rs @@ -16,8 +16,7 @@ use anyhow::*; use litesvm::types::TransactionMetadata; use solana_sdk::instruction::InstructionError; - -use crate::{TXError, TXResult}; +use testsvm_core::prelude::*; /// Provides assertion methods for failed transactions. /// @@ -172,7 +171,9 @@ pub trait TXResultAssertions { /// # Example /// /// ```rust - /// # use testsvm::{TestSVM, TXResultAssertions}; + /// # use testsvm_core::prelude::*; + /// # use testsvm_spl::prelude::*; + /// # use testsvm_assertions::*; /// # use solana_sdk::signature::Signer; /// # use anyhow::Result; /// # fn main() -> Result<()> { @@ -222,7 +223,9 @@ pub trait TXResultAssertions { /// # Example /// /// ```rust - /// # use testsvm::{TestSVM, TXResultAssertions}; + /// # use testsvm_core::prelude::*; + /// # use testsvm_spl::prelude::*; + /// # use testsvm_assertions::*; /// # use solana_sdk::signature::Signer; /// # use anyhow::Result; /// # fn main() -> Result<()> { diff --git a/crates/testsvm-core/Cargo.toml b/crates/testsvm-core/Cargo.toml new file mode 100644 index 0000000..85f3fca --- /dev/null +++ b/crates/testsvm-core/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "testsvm-core" +version = "0.1.1" +authors = ["Ian Macalinao "] +edition = "2024" +license = "Apache-2.0" +description = "Core TestSVM implementation for Solana program testing" +repository = "https://github.com/macalinao/testsvm" +documentation = "https://docs.rs/testsvm-core" +homepage = "https://github.com/macalinao/testsvm" +readme = "README.md" +keywords = ["ian-macalinao", "solana", "testing", "blockchain", "testsvm"] +categories = ["development-tools::testing", "cryptography::cryptocurrencies"] + +[dependencies] +anyhow = "1.0" +litesvm = "0.6" +solana-sdk = "2.2" +solana-address-book = { path = "../solana-address-book", version = "0.1.1" } +anchor-lang = "0.31" +anchor-utils = { path = "../anchor-utils", version = "0.1.0" } +colored = "3" diff --git a/crates/testsvm-core/README.md b/crates/testsvm-core/README.md new file mode 100644 index 0000000..5d99bc9 --- /dev/null +++ b/crates/testsvm-core/README.md @@ -0,0 +1,26 @@ +# TestSVM Core + +[![Crates.io](https://img.shields.io/crates/v/testsvm-core.svg)](https://crates.io/crates/testsvm-core) +[![Documentation](https://docs.rs/testsvm-core/badge.svg)](https://docs.rs/testsvm-core) + +Core implementation of the TestSVM testing framework for Solana programs. This crate provides the fundamental building blocks for testing Solana programs in a simulated environment, wrapping LiteSVM with enhanced functionality for transaction management, account creation, and debugging capabilities. + +## Features + +- **LiteSVM Wrapper**: Enhanced wrapper around LiteSVM with additional testing utilities +- **Transaction Management**: Simplified transaction sending with automatic fee payer management +- **Account Creation**: Helper functions for creating and funding test accounts +- **Address Book Integration**: Built-in address book for tracking and labeling accounts +- **Clock Control**: Utilities for manipulating blockchain time in tests +- **Enhanced Debugging**: Colored output and detailed transaction result formatting + +## Core Components + +- **TestSVM**: Main struct wrapping LiteSVM with payer and address book management +- **AccountRef**: Reference wrapper for account data with convenient accessors +- **TXResult**: Enhanced transaction result type with detailed error information +- **Address Book**: Integrated address labeling system for better debugging + +## License + +Copyright (c) 2025 Ian Macalinao. Licensed under the Apache License, Version 2.0. \ No newline at end of file diff --git a/crates/testsvm/src/account_ref.rs b/crates/testsvm-core/src/account_ref.rs similarity index 90% rename from crates/testsvm/src/account_ref.rs rename to crates/testsvm-core/src/account_ref.rs index 60e71e1..3e7b836 100644 --- a/crates/testsvm/src/account_ref.rs +++ b/crates/testsvm-core/src/account_ref.rs @@ -90,9 +90,7 @@ impl AsRef<[u8]> for AccountRef { mod tests { use crate::AccountRef; use anchor_lang::prelude::*; - use solana_address_book::pda_seeds::{ - SeedPart, find_pda_with_bump, find_pda_with_bump_and_strings, - }; + use solana_address_book::pda_seeds::find_pda_with_bump_and_strings; // Dummy type for testing #[derive(Debug, Clone)] @@ -123,9 +121,8 @@ mod tests { let account_ref: AccountRef = AccountRef::new(account_pubkey); // Test that AccountRef can be used as a seed - let seeds: Vec<&dyn SeedPart> = vec![&"prefix", &account_ref]; - - let (pda, bump) = find_pda_with_bump(&seeds, &program_id); + let (pda, bump) = + Pubkey::find_program_address(&[b"prefix", account_ref.key.as_ref()], &program_id); // Verify it matches manual calculation let (expected_pda, expected_bump) = @@ -135,7 +132,8 @@ mod tests { assert_eq!(bump, expected_bump); // Test with find_pda_with_bump_and_strings - let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); + let derived_pda = + find_pda_with_bump_and_strings(&[b"prefix", account_ref.as_ref()], &program_id); assert_eq!(derived_pda.key, expected_pda); assert_eq!(derived_pda.bump, expected_bump); @@ -158,10 +156,15 @@ mod tests { let nonce_bytes = nonce.to_le_bytes(); // Use multiple AccountRefs in PDA derivation - let seeds: Vec<&dyn SeedPart> = - vec![&"vault", &vault_account, &owner_account, &nonce_bytes]; - - let derived_pda = find_pda_with_bump_and_strings(&seeds, &program_id); + let derived_pda = find_pda_with_bump_and_strings( + &[ + b"vault", + vault_account.as_ref(), + owner_account.as_ref(), + nonce_bytes.as_ref(), + ], + &program_id, + ); // Manual verification let (expected_pda, expected_bump) = Pubkey::find_program_address( diff --git a/crates/testsvm/src/testsvm.rs b/crates/testsvm-core/src/lib.rs similarity index 52% rename from crates/testsvm/src/testsvm.rs rename to crates/testsvm-core/src/lib.rs index 14a380f..fa1cc74 100644 --- a/crates/testsvm/src/testsvm.rs +++ b/crates/testsvm-core/src/lib.rs @@ -1,26 +1,16 @@ -//! # TestSVM Implementation +//! # TestSVM Core //! -//! Core implementation of the TestSVM testing framework wrapper. +//! Core implementation of the TestSVM testing framework for Solana programs. //! -//! This module contains the main `TestSVM` struct and its implementation, providing -//! a comprehensive testing environment for Solana programs. It wraps LiteSVM with -//! additional functionality for transaction management, account creation, token operations, +//! This crate provides the fundamental `TestSVM` struct that wraps LiteSVM +//! with additional functionality for transaction management, account creation, //! and enhanced debugging capabilities. -//! -//! ## Architecture -//! -//! - **LiteSVM Wrapper**: Extends LiteSVM with developer-friendly APIs -//! - **Default Fee Payer**: Automatic transaction fee management -//! - **Address Book**: Integrated labeling system for all accounts -//! - **Transaction Result**: Rich error reporting and transaction analysis -//! - **Token Operations**: Built-in SPL Token program support use std::{ env, path::{Path, PathBuf}, }; -use anchor_spl::token; use anyhow::*; use litesvm::LiteSVM; use solana_sdk::{ @@ -30,7 +20,18 @@ use solana_sdk::{ transaction::Transaction, }; -use crate::{AccountRef, AddressBook, SeedPart, TXError, TXResult, new_funded_account}; +pub use solana_address_book::AddressBook; + +mod tx_result; +pub use tx_result::{TXError, TXResult}; + +mod account_ref; +pub use account_ref::AccountRef; + +mod litesvm_helpers; +use litesvm_helpers::new_funded_account; + +pub mod prelude; /// Test SVM wrapper for LiteSVM with payer management and Anchor helpers pub struct TestSVM { @@ -111,47 +112,6 @@ impl TestSVM { Ok(keypair) } - /// Create a mint with the test SVM's payer and add to address book - pub fn create_mint( - &mut self, - name: &str, - decimals: u8, - authority: &Pubkey, - ) -> Result> { - let mint = Keypair::new(); - - let rent = self - .svm - .minimum_balance_for_rent_exemption(token::Mint::LEN); // Mint account size - - let create_account_ix = solana_sdk::system_instruction::create_account( - &self.default_fee_payer.pubkey(), - &mint.pubkey(), - rent, - anchor_spl::token::Mint::LEN as u64, // Mint account size - &anchor_spl::token::ID, - ); - - let init_mint_ix = anchor_spl::token::spl_token::instruction::initialize_mint( - &anchor_spl::token::ID, - &mint.pubkey(), - authority, - Some(authority), // Set freeze authority to same as mint authority - decimals, - ) - .context("Failed to create initialize mint instruction")?; - - self.execute_ixs_with_signers(&[create_account_ix, init_mint_ix], &[&mint]) - .map_err(|e| anyhow!("Failed to create mint: {}", e))?; - - // Add the mint to the address book - let mint_pubkey = mint.pubkey(); - let label = format!("mint:{name}"); - self.address_book.add_mint(mint_pubkey, label)?; - - Ok(AccountRef::new(mint_pubkey)) - } - /// Get the default fee payer's public key pub fn default_fee_payer(&self) -> Pubkey { self.default_fee_payer.pubkey() @@ -172,35 +132,6 @@ impl TestSVM { /// /// This method loads a program binary from the fixtures directory. The fixture file /// should be located at `fixtures/programs/{fixture_name}.so` relative to your project root. - /// - /// # Arguments - /// * `fixture_name` - The name of the fixture file (without the .so extension) - /// * `pubkey` - The public key to assign to the program - /// - /// # Example - /// ```rust,no_run - /// # use testsvm::TestSVM; - /// # use solana_sdk::pubkey::Pubkey; - /// # use anyhow::Result; - /// # fn main() -> Result<()> { - /// # let mut env = TestSVM::init()?; - /// # let program_id = Pubkey::new_unique(); - /// // This will load the file from fixtures/programs/my_program.so - /// env.add_program_fixture("my_program", program_id)?; - /// # Ok(()) - /// # } - /// ``` - /// - /// # File Structure - /// Your project should have the following structure: - /// ```text - /// project_root/ - /// ├── fixtures/ - /// │ └── programs/ - /// │ ├── my_program.so - /// │ └── other_program.so - /// └── src/ - /// ``` pub fn add_program_fixture(&mut self, fixture_name: &str, pubkey: Pubkey) -> Result<()> { let path = env::var("CARGO_MANIFEST_DIR") .map(PathBuf::from) @@ -222,63 +153,16 @@ impl TestSVM { Ok(()) } - /// Create an associated token account instruction and add to address book - /// Returns the instruction and the ATA address - pub fn create_ata_ix( - &mut self, - label: &str, - owner: &Pubkey, - mint: &Pubkey, - ) -> Result<( - solana_sdk::instruction::Instruction, - AccountRef, - )> { - let ata = anchor_spl::associated_token::get_associated_token_address(owner, mint); - - // Add to address book (ignore error if duplicate) - self.address_book - .add_ata(ata, label.to_string(), *mint, *owner)?; - - let ix = anchor_spl::associated_token::spl_associated_token_account::instruction::create_associated_token_account_idempotent( - &self.default_fee_payer(), - owner, - mint, - &anchor_spl::token::ID, - ); - - Ok((ix, AccountRef::new(ata))) - } - - /// Find a PDA with bump and add it to the address book - pub fn find_pda_with_bump( - &mut self, - label: &str, - seeds: &[&dyn SeedPart], - program_id: Pubkey, - ) -> Result<(Pubkey, u8)> { - self.address_book - .find_pda_with_bump(label, seeds, program_id) - } - - /// Find a PDA and add it to the address book - pub fn get_pda_key( - &mut self, - label: &str, - seeds: &[&dyn SeedPart], - program_id: Pubkey, - ) -> Result { - let (pubkey, _bump) = self.find_pda_with_bump(label, seeds, program_id)?; - Ok(pubkey) - } - - /// Find a PDA and return an AccountRef with proper type information + /// Finds a program derived address and return an [AccountRef] with proper type information. pub fn get_pda( &mut self, label: &str, - seeds: &[&dyn SeedPart], + seeds: &[&[u8]], program_id: Pubkey, ) -> Result> { - let pubkey = self.get_pda_key(label, seeds, program_id)?; + let (pubkey, _bump) = self + .address_book + .find_pda_with_bump(label, seeds, program_id)?; Ok(AccountRef::new(pubkey)) } diff --git a/crates/testsvm/src/litesvm_helpers.rs b/crates/testsvm-core/src/litesvm_helpers.rs similarity index 100% rename from crates/testsvm/src/litesvm_helpers.rs rename to crates/testsvm-core/src/litesvm_helpers.rs diff --git a/crates/testsvm-core/src/prelude.rs b/crates/testsvm-core/src/prelude.rs new file mode 100644 index 0000000..6b5e0c8 --- /dev/null +++ b/crates/testsvm-core/src/prelude.rs @@ -0,0 +1,30 @@ +//! # TestSVM Core Prelude +//! +//! Common imports for TestSVM users. This module re-exports the most commonly used types +//! and traits from testsvm-core for convenient access. + +// Core TestSVM types +pub use crate::{AccountRef, TXError, TXResult, TestSVM}; + +// Address book types +pub use solana_address_book::{AddressBook, AddressRole, RegisteredAddress}; + +// Commonly used Anchor types +pub use anchor_lang::{ + AccountDeserialize, AccountSerialize, Discriminator, InstructionData, Key, ToAccountInfos, + ToAccountMetas, +}; + +pub use anyhow::Result; + +// Commonly used Solana SDK types +pub use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_program, sysvar, + transaction::Transaction, +}; + +// Re-export anchor_instruction helper from anchor-utils +pub use anchor_utils::anchor_instruction; diff --git a/crates/testsvm/src/tx_result.rs b/crates/testsvm-core/src/tx_result.rs similarity index 99% rename from crates/testsvm/src/tx_result.rs rename to crates/testsvm-core/src/tx_result.rs index b5b7234..9d87d25 100644 --- a/crates/testsvm/src/tx_result.rs +++ b/crates/testsvm-core/src/tx_result.rs @@ -22,7 +22,7 @@ use colored::Colorize; use litesvm::types::{FailedTransactionMetadata, TransactionMetadata}; use solana_sdk::transaction::Transaction; -use crate::AddressBook; +use solana_address_book::AddressBook; /// Error type representing a failed transaction with detailed metadata. /// diff --git a/crates/testsvm-quarry/README.md b/crates/testsvm-quarry/README.md index f533af4..d3017c6 100644 --- a/crates/testsvm-quarry/README.md +++ b/crates/testsvm-quarry/README.md @@ -1,76 +1,24 @@ -# testsvm-quarry +# TestSVM Quarry [![Crates.io](https://img.shields.io/crates/v/testsvm-quarry.svg)](https://crates.io/crates/testsvm-quarry) [![Documentation](https://docs.rs/testsvm-quarry/badge.svg)](https://docs.rs/testsvm-quarry) -Testing utilities for the Quarry protocol on Solana using the TestSVM framework. - -## Overview - -This crate provides testing utilities and helpers for interacting with the Quarry mining protocol in a test environment. It includes functions for setting up Quarry programs, managing mining operations, and testing reward distribution mechanisms. - -## Installation - -Add this to your `Cargo.toml`: - -```toml -[dependencies] -testsvm-quarry = "0.1.0" -``` - -## Setup - -Before using this crate, you need to download the Quarry program binaries. Run the following commands to fetch the required programs: - -```bash -# Set your project root directory -export ROOT_DIR=/path/to/your/project - -# Download Quarry programs -solana program dump QMMD16kjauP5knBwxNUJRZ1Z5o3deBuFrqVjBVmmqto $ROOT_DIR/fixtures/programs/quarry_merge_mine.so -solana program dump QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB $ROOT_DIR/fixtures/programs/quarry_mine.so -solana program dump QMWoBmAyJLAsA1Lh9ugMTw2gciTihncciphzdNzdZYV $ROOT_DIR/fixtures/programs/quarry_mint_wrapper.so -``` - -## Usage - -```rust -use testsvm::TestSVM; -use testsvm_quarry::setup::setup_quarry_programs; - -fn main() -> anyhow::Result<()> { - let mut env = TestSVM::new(); - - // Setup Quarry programs - setup_quarry_programs(&mut env)?; - - // Your test logic here - - Ok(()) -} -``` +Testing utilities for the Quarry protocol on Solana using the TestSVM framework. This crate provides comprehensive testing utilities and helpers for interacting with the Quarry mining protocol in a simulated test environment, enabling developers to test mining operations, reward distributions, and merge mining functionality. ## Features - **Program Setup**: Easy setup of Quarry mining programs in test environment -- **Mining Operations**: Test mining rewards and distributions -- **Merge Mining**: Support for testing merge mining functionality -- **Mint Wrapper**: Testing utilities for the Quarry mint wrapper - -## Program IDs - -The crate includes the following Quarry program IDs: - -- **Quarry Mine**: `QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB` -- **Quarry Merge Mine**: `QMMD16kjauP5knBwxNUJRZ1Z5o3deBuFrqVjBVmmqto` -- **Quarry Mint Wrapper**: `QMWoBmAyJLAsA1Lh9ugMTw2gciTihncciphzdNzdZYV` +- **Mining Operations**: Test mining rewards and distributions with realistic scenarios +- **Merge Mining**: Full support for testing merge mining functionality +- **Mint Wrapper**: Testing utilities for the Quarry mint wrapper operations +- **Reward Testing**: Simulate and test reward distribution mechanisms +- **Integration Ready**: Seamless integration with TestSVM framework -## Dependencies +## Supported Programs -This crate depends on: -- `testsvm` - Core testing framework for Solana SVM -- `anchor-lang` - Anchor framework for Solana program development -- `solana-sdk` - Solana SDK for blockchain interactions +- **Quarry Mine**: Core mining program for reward distribution +- **Quarry Merge Mine**: Merge mining functionality for multiple reward sources +- **Quarry Mint Wrapper**: Mint wrapper for token management in mining operations ## License diff --git a/crates/testsvm-quarry/src/lib.rs b/crates/testsvm-quarry/src/lib.rs index 64479d6..f0e00f5 100644 --- a/crates/testsvm-quarry/src/lib.rs +++ b/crates/testsvm-quarry/src/lib.rs @@ -39,12 +39,14 @@ pub mod prelude; pub mod setup; pub mod test_merge_miner; pub mod test_merge_pool; +pub mod test_mint_wrapper; pub mod test_quarry; pub mod test_rewarder; pub use setup::*; pub use test_merge_miner::*; pub use test_merge_pool::*; +pub use test_mint_wrapper::*; pub use test_quarry::*; pub use test_rewarder::*; diff --git a/crates/testsvm-quarry/src/setup.rs b/crates/testsvm-quarry/src/setup.rs index d09231d..c723874 100644 --- a/crates/testsvm-quarry/src/setup.rs +++ b/crates/testsvm-quarry/src/setup.rs @@ -13,7 +13,7 @@ //! - **quarry_mint_wrapper**: Wrapped token minting capabilities use anyhow::Result; -use testsvm::TestSVM; +use testsvm::prelude::*; use crate::quarry_mine; diff --git a/crates/testsvm-quarry/src/test_merge_miner.rs b/crates/testsvm-quarry/src/test_merge_miner.rs index 554cb94..fce1ffa 100644 --- a/crates/testsvm-quarry/src/test_merge_miner.rs +++ b/crates/testsvm-quarry/src/test_merge_miner.rs @@ -16,7 +16,7 @@ use anyhow::Result; use solana_sdk::pubkey::Pubkey; -use testsvm::{AccountRef, TestSVM, anchor_instruction}; +use testsvm::prelude::*; use crate::{quarry_merge_mine, quarry_mine}; @@ -45,7 +45,7 @@ impl TestMergeMiner { // Get primary miner PDA let primary_miner = env.get_pda::( &format!("{label}_primary_miner"), - &[&"Miner", quarry, &self.merge_miner.key], + &[b"Miner", quarry.as_ref(), self.merge_miner.key.as_ref()], crate::quarry_mine::ID, )?; @@ -98,7 +98,7 @@ impl TestMergeMiner { // Get replica miner PDA let replica_miner = env.get_pda::( &format!("{label}_replica_miner"), - &[&"Miner", quarry, &self.merge_miner.key], + &[b"Miner", quarry.as_ref(), self.merge_miner.key.as_ref()], crate::quarry_mine::ID, )?; diff --git a/crates/testsvm-quarry/src/test_merge_pool.rs b/crates/testsvm-quarry/src/test_merge_pool.rs index 05e4780..3c50433 100644 --- a/crates/testsvm-quarry/src/test_merge_pool.rs +++ b/crates/testsvm-quarry/src/test_merge_pool.rs @@ -14,10 +14,10 @@ //! - **Token Operations**: Handle wrapped token minting and distribution //! - **Type Safety**: Strongly typed account references for all pool components -use anchor_lang::{InstructionData, prelude::*}; +use anchor_lang::prelude::*; use anyhow::Result; use solana_sdk::instruction::Instruction; -use testsvm::{AccountRef, TestSVM, anchor_instruction}; +use testsvm::prelude::*; use crate::{TestMergeMiner, quarry_merge_mine, quarry_mine}; @@ -39,14 +39,14 @@ impl TestMergePool { // Calculate merge pool PDA let pool = env.get_pda::( &format!("merge_pool[{label}].pool"), - &[&"MergePool", &primary_mint.key], + &[b"MergePool", primary_mint.key.as_ref()], quarry_merge_mine::ID, )?; // Calculate replica mint PDA let replica_mint = env.get_pda::( &format!("merge_pool[{label}].replica_mint"), - &[&"ReplicaMint", &pool.key], + &[b"ReplicaMint", pool.key.as_ref()], quarry_merge_mine::ID, )?; @@ -86,7 +86,7 @@ impl TestMergePool { // Calculate merge miner PDA let merge_miner = env.get_pda( &format!("merge_miner[{label}]"), - &[&"MergeMiner", &self.pool.key, &owner], + &[b"MergeMiner", self.pool.key.as_ref(), owner.as_ref()], quarry_merge_mine::ID, )?; @@ -147,7 +147,7 @@ impl TestMergePool { // Get primary miner PDA let primary_miner = env.get_pda::( "primary_miner", - &[&"Miner", primary_quarry, &merge_miner], + &[b"Miner", primary_quarry.as_ref(), merge_miner.as_ref()], crate::quarry_mine::ID, )?; diff --git a/crates/testsvm-quarry/src/test_mint_wrapper.rs b/crates/testsvm-quarry/src/test_mint_wrapper.rs new file mode 100644 index 0000000..4ae4621 --- /dev/null +++ b/crates/testsvm-quarry/src/test_mint_wrapper.rs @@ -0,0 +1,171 @@ +//! # Test Mint Wrapper +//! +//! Testing utilities for the Quarry Mint Wrapper program. +//! +//! This module provides the `TestMintWrapper` struct which simplifies the creation +//! and management of mint wrappers in test environments. It handles: +//! +//! - **Mint Wrapper Creation**: Initialize mint wrappers with proper configuration +//! - **Token Management**: Create and manage reward token mints +//! - **Minter Management**: Create and configure minters with allowances +//! - **Authority Control**: Manage mint wrapper authority and permissions + +use crate::quarry_mint_wrapper; +use anchor_lang::prelude::*; +use anyhow::{Context, Result}; +use solana_sdk::signature::{Keypair, Signer}; +use testsvm::prelude::*; + +/// Test mint wrapper with labeled accounts +pub struct TestMintWrapper { + pub label: String, + pub mint_wrapper: AccountRef, + pub mint_wrapper_base: Keypair, + pub reward_token_mint: AccountRef, + pub authority: Pubkey, +} + +impl TestMintWrapper { + /// Create a new mint wrapper with the specified label and authority + pub fn new(env: &mut TestSVM, label: &str, authority: &Keypair) -> Result { + let mint_wrapper_base = env.new_wallet(&format!("mint_wrapper[{label}].base"))?; + + // Calculate mint wrapper PDA + let mint_wrapper: AccountRef = env.get_pda( + &format!("mint_wrapper[{label}]"), + &[b"MintWrapper", mint_wrapper_base.pubkey().as_ref()], + quarry_mint_wrapper::ID, + )?; + + // Create reward token mint with mint wrapper as authority + let reward_token_mint = env + .create_mint( + &format!("mint_wrapper[{label}].reward_token"), + 6, + &mint_wrapper.key, + ) + .context("Failed to create reward token mint")?; + + // Create the mint wrapper + let create_wrapper_ix = anchor_instruction( + quarry_mint_wrapper::ID, + quarry_mint_wrapper::client::accounts::NewWrapperV2 { + base: mint_wrapper_base.pubkey(), + mint_wrapper: mint_wrapper.key, + admin: authority.pubkey(), + token_mint: reward_token_mint.key, + token_program: anchor_spl::token::ID, + payer: env.default_fee_payer(), + system_program: solana_sdk::system_program::ID, + }, + quarry_mint_wrapper::client::args::NewWrapperV2 { hard_cap: u64::MAX }, + ); + + env.execute_ixs_with_signers(&[create_wrapper_ix], &[&mint_wrapper_base])?; + + Ok(TestMintWrapper { + label: label.to_string(), + mint_wrapper, + mint_wrapper_base, + reward_token_mint, + authority: authority.pubkey(), + }) + } + + /// Create a minter for the specified authority with the given allowance + pub fn create_minter( + &self, + env: &mut TestSVM, + minter_authority: &Pubkey, + allowance: u64, + admin: &Keypair, + ) -> Result> { + // Calculate minter PDA + let minter = env.get_pda( + &format!("mint_wrapper[{}].minter[{}]", self.label, minter_authority), + &[ + b"MintWrapperMinter", + self.mint_wrapper.key.as_ref(), + minter_authority.as_ref(), + ], + quarry_mint_wrapper::ID, + )?; + + // Create the minter + let create_minter_ix = anchor_instruction( + quarry_mint_wrapper::ID, + quarry_mint_wrapper::client::accounts::NewMinterV2 { + new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { + mint_wrapper: self.mint_wrapper.key, + admin: admin.pubkey(), + }, + new_minter_authority: *minter_authority, + minter: minter.key, + payer: env.default_fee_payer(), + system_program: solana_sdk::system_program::ID, + }, + quarry_mint_wrapper::client::args::NewMinterV2 {}, + ); + + // Set the allowance + let set_allowance_ix = anchor_instruction( + quarry_mint_wrapper::ID, + quarry_mint_wrapper::client::accounts::MinterUpdate { + minter: minter.key, + minter_update_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { + mint_wrapper: self.mint_wrapper.key, + admin: admin.pubkey(), + }, + }, + quarry_mint_wrapper::client::args::MinterUpdate { allowance }, + ); + + env.execute_ixs_with_signers(&[create_minter_ix, set_allowance_ix], &[admin])?; + + Ok(minter) + } + + /// Transfer mint wrapper authority to a new authority + pub fn transfer_authority( + &mut self, + env: &mut TestSVM, + new_authority: &Pubkey, + current_authority: &Keypair, + ) -> Result<()> { + let transfer_authority_ix = anchor_instruction( + quarry_mint_wrapper::ID, + quarry_mint_wrapper::client::accounts::TransferAdmin { + mint_wrapper: self.mint_wrapper.key, + admin: current_authority.pubkey(), + next_admin: *new_authority, + }, + quarry_mint_wrapper::client::args::TransferAdmin {}, + ); + + env.execute_ixs_with_signers(&[transfer_authority_ix], &[current_authority])?; + + // Update the authority field + self.authority = *new_authority; + + Ok(()) + } + + /// Accept mint wrapper authority transfer + pub fn accept_authority(&mut self, env: &mut TestSVM, new_authority: &Keypair) -> Result<()> { + let accept_authority_ix = anchor_instruction( + quarry_mint_wrapper::ID, + quarry_mint_wrapper::client::accounts::AcceptAdmin { + mint_wrapper: self.mint_wrapper.key, + pending_admin: new_authority.pubkey(), + }, + quarry_mint_wrapper::client::args::AcceptAdmin {}, + ); + + env.execute_ixs_with_signers(&[accept_authority_ix], &[new_authority])?; + + // Update the authority field + self.authority = new_authority.pubkey(); + + Ok(()) + } +} diff --git a/crates/testsvm-quarry/src/test_quarry.rs b/crates/testsvm-quarry/src/test_quarry.rs index 6869d73..646089b 100644 --- a/crates/testsvm-quarry/src/test_quarry.rs +++ b/crates/testsvm-quarry/src/test_quarry.rs @@ -19,7 +19,7 @@ use anyhow::Result; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signer}; use std::fmt; -use testsvm::{AccountRef, TestSVM, anchor_instruction}; +use testsvm::prelude::*; /// Test quarry with labeled accounts #[derive(Debug)] @@ -48,7 +48,7 @@ impl TestQuarry { )> { let miner = env.get_pda( &format!("miner_{label}"), - &[b"Miner", &self.quarry.key.as_ref(), &user.pubkey().as_ref()], + &[b"Miner", self.quarry.key.as_ref(), user.pubkey().as_ref()], quarry_mine::ID, )?; @@ -169,7 +169,7 @@ impl TestQuarry { let (minter, _) = Pubkey::find_program_address( &[ b"MintWrapperMinter", - rewarder.mint_wrapper.key.as_ref(), + rewarder.mint_wrapper.mint_wrapper.key.as_ref(), rewarder.rewarder.key.as_ref(), ], &quarry_mint_wrapper::ID, @@ -178,10 +178,10 @@ impl TestQuarry { let claim_ix = anchor_instruction( quarry_mine::ID, quarry_mine::client::accounts::ClaimRewardsV2 { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, mint_wrapper_program: quarry_mint_wrapper::ID, minter, - rewards_token_mint: rewarder.reward_token_mint.key, + rewards_token_mint: rewarder.mint_wrapper.reward_token_mint.key, rewards_token_account: user_rewards_account.key, claim_fee_token_account: rewarder.claim_fee_token_account.key, claim: quarry_mine::client::accounts::Claim { diff --git a/crates/testsvm-quarry/src/test_rewarder.rs b/crates/testsvm-quarry/src/test_rewarder.rs index c972558..18f83c6 100644 --- a/crates/testsvm-quarry/src/test_rewarder.rs +++ b/crates/testsvm-quarry/src/test_rewarder.rs @@ -14,68 +14,34 @@ //! - **Reward Configuration**: Set annual reward rates and distribution parameters //! - **Authority Control**: Manage rewarder authority and pause states -use crate::{quarry_mine, quarry_mint_wrapper}; -use anchor_lang::prelude::*; -use anyhow::{Context, Result}; -use solana_sdk::signature::{Keypair, Signer}; -use testsvm::{AccountRef, TXResult, TestSVM, anchor_instruction}; +use crate::{TestMintWrapper, quarry_mine, quarry_mint_wrapper}; +use testsvm::prelude::*; /// Test rewarder with labeled accounts pub struct TestRewarder { pub label: String, pub rewarder: AccountRef, - pub mint_wrapper: AccountRef, + pub mint_wrapper: TestMintWrapper, pub minter: AccountRef, - pub reward_token_mint: AccountRef, pub claim_fee_token_account: AccountRef, pub authority: Pubkey, - // Keep the base keypairs for signing - pub mint_wrapper_base: Keypair, + // Keep the base keypair for signing pub rewarder_base: Keypair, } impl TestRewarder { /// Create a new rewarder with the specified label and authority pub fn new_rewarder(env: &mut TestSVM, label: &str, authority: &Keypair) -> Result { - let mint_wrapper_base = env.new_wallet(&format!("rewarder[{label}].mint_wrapper_base"))?; - let rewarder_base = env.new_wallet(&format!("rewarder[{label}].rewarder_base"))?; - - // Calculate mint wrapper PDA - let (mint_wrapper, mint_wrapper_bump) = env.find_pda_with_bump( - &format!("rewarder[{label}].mint_wrapper"), - &[&"MintWrapper", &mint_wrapper_base.pubkey()], - quarry_mint_wrapper::ID, - )?; - - // Create reward token mint with mint wrapper as authority - let reward_token_mint = env - .create_mint(&format!("rewarder[{label}].reward_token"), 6, &mint_wrapper) - .context("Failed to create reward token mint")?; + // Create the mint wrapper using TestMintWrapper + let mint_wrapper = TestMintWrapper::new(env, &format!("rewarder[{label}]"), authority)?; - let create_wrapper_ix = anchor_instruction( - quarry_mint_wrapper::ID, - quarry_mint_wrapper::client::accounts::NewWrapper { - base: mint_wrapper_base.pubkey(), - mint_wrapper, - admin: authority.pubkey(), - token_mint: reward_token_mint.key, - token_program: anchor_spl::token::ID, - payer: env.default_fee_payer(), - system_program: solana_sdk::system_program::ID, - }, - quarry_mint_wrapper::client::args::NewWrapper { - bump: mint_wrapper_bump, - hard_cap: u64::MAX, - }, - ); - - env.execute_ixs_with_signers(&[create_wrapper_ix], &[&mint_wrapper_base])?; + let rewarder_base = env.new_wallet(&format!("rewarder[{label}].rewarder_base"))?; // Calculate rewarder PDA let rewarder = env.get_pda( &format!("rewarder[{label}].rewarder"), - &[&"Rewarder", &rewarder_base.pubkey()], + &[b"Rewarder", rewarder_base.pubkey().as_ref()], quarry_mine::ID, )?; @@ -83,7 +49,7 @@ impl TestRewarder { let (create_ata_ix, claim_fee_token_account) = env.create_ata_ix( &format!("rewarder[{label}].claim_fee_tokens"), &rewarder.key, - &reward_token_mint.key, + &mint_wrapper.reward_token_mint.key, )?; env.execute_ixs(&[create_ata_ix])?; @@ -97,8 +63,8 @@ impl TestRewarder { initial_authority: authority.pubkey(), payer: env.default_fee_payer(), system_program: solana_sdk::system_program::ID, - mint_wrapper, - rewards_token_mint: reward_token_mint.key, + mint_wrapper: mint_wrapper.mint_wrapper.key, + rewards_token_mint: mint_wrapper.reward_token_mint.key, claim_fee_token_account: claim_fee_token_account.key, }, quarry_mine::client::args::NewRewarderV2 {}, @@ -106,52 +72,15 @@ impl TestRewarder { env.execute_ixs_with_signers(&[create_rewarder_ix], &[&rewarder_base])?; - // Create and approve minter for the mint wrapper - let minter = env.get_pda( - &format!("rewarder[{label}].minter"), - &[&"MintWrapperMinter", &mint_wrapper, &rewarder], - quarry_mint_wrapper::ID, - )?; - - let create_minter_ix = anchor_instruction( - quarry_mint_wrapper::ID, - quarry_mint_wrapper::client::accounts::NewMinterV2 { - new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper, - admin: authority.pubkey(), - }, - new_minter_authority: rewarder.key, - minter: minter.key, - payer: env.default_fee_payer(), - system_program: solana_sdk::system_program::ID, - }, - quarry_mint_wrapper::client::args::NewMinterV2 {}, - ); - - let set_allowance_ix = anchor_instruction( - quarry_mint_wrapper::ID, - quarry_mint_wrapper::client::accounts::MinterUpdate { - minter: minter.key, - minter_update_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper, - admin: authority.pubkey(), - }, - }, - quarry_mint_wrapper::client::args::MinterUpdate { - allowance: u64::MAX, - }, - ); - - env.execute_ixs_with_signers(&[create_minter_ix, set_allowance_ix], &[authority])?; + // Create minter for the rewarder with max allowance + let minter = mint_wrapper.create_minter(env, &rewarder.key, u64::MAX, authority)?; Ok(TestRewarder { label: label.to_string(), rewarder, - mint_wrapper: AccountRef::new(mint_wrapper), + mint_wrapper, minter, - reward_token_mint, authority: authority.pubkey(), - mint_wrapper_base, rewarder_base, claim_fee_token_account, }) @@ -168,9 +97,13 @@ impl TestRewarder { let quarry_label = format!("rewarder[{}].quarry[{}]", self.label, quarry_name); // Calculate quarry PDA - let quarry = env.get_pda_key( + let quarry = env.get_pda( &format!("{quarry_label}.quarry"), - &[&"Quarry", &self.rewarder.key, staked_token_mint], + &[ + b"Quarry", + self.rewarder.key.as_ref(), + staked_token_mint.as_ref(), + ], quarry_mine::ID, )?; @@ -180,7 +113,7 @@ impl TestRewarder { let create_quarry_ix = anchor_instruction( quarry_mine::ID, quarry_mine::client::accounts::CreateQuarryV2 { - quarry, + quarry: quarry.key, auth: TransferAuthority { authority: authority.pubkey(), rewarder: self.rewarder.key, @@ -199,7 +132,7 @@ impl TestRewarder { authority: authority.pubkey(), rewarder: self.rewarder.key, }, - quarry, + quarry: quarry.key, }, quarry_mine::client::args::SetRewardsShare { new_share: 1 }, ); @@ -208,7 +141,7 @@ impl TestRewarder { Ok(crate::TestQuarry { label: quarry_label, - quarry: AccountRef::new(quarry), + quarry, rewarder: self.rewarder.key, staked_token_mint: AccountRef::new(*staked_token_mint), }) @@ -249,7 +182,7 @@ impl TestRewarder { &self, env: &TestSVM, ) -> Result { - self.mint_wrapper.load(env) + self.mint_wrapper.mint_wrapper.load(env) } /// Helper to set annual rewards rate for a quarry @@ -285,7 +218,7 @@ impl TestRewarder { let (minter, minter_bump) = Pubkey::find_program_address( &[ b"MintWrapperMinter", - self.mint_wrapper.key.as_ref(), + self.mint_wrapper.mint_wrapper.key.as_ref(), self.rewarder.key.as_ref(), ], &quarry_mint_wrapper::ID, @@ -295,7 +228,7 @@ impl TestRewarder { quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::NewMinter { new_minter_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper: self.mint_wrapper.key, + mint_wrapper: self.mint_wrapper.mint_wrapper.key, admin: authority.pubkey(), }, new_minter_authority: self.rewarder.key, @@ -315,7 +248,7 @@ impl TestRewarder { format!("rewarder[{}].minter[{}]", self.label, label), vec![ "MintWrapperMinter".to_string(), - self.mint_wrapper.key.to_string(), + self.mint_wrapper.mint_wrapper.key.to_string(), self.rewarder.key.to_string(), ], quarry_mint_wrapper::ID, diff --git a/crates/testsvm-quarry/src/tests/mod.rs b/crates/testsvm-quarry/src/tests/mod.rs index e7e322c..f5a7924 100644 --- a/crates/testsvm-quarry/src/tests/mod.rs +++ b/crates/testsvm-quarry/src/tests/mod.rs @@ -1,6 +1,6 @@ pub mod common; pub mod test_claim_rewards; pub mod test_deposit_withdraw; -pub mod test_mint_wrapper; +pub mod test_mint_wrapper_actions; pub mod test_quarry_setup; pub mod test_rewarder_management; diff --git a/crates/testsvm-quarry/src/tests/test_claim_rewards.rs b/crates/testsvm-quarry/src/tests/test_claim_rewards.rs index 3113695..6006223 100644 --- a/crates/testsvm-quarry/src/tests/test_claim_rewards.rs +++ b/crates/testsvm-quarry/src/tests/test_claim_rewards.rs @@ -75,7 +75,7 @@ fn test_claim_rewards() -> Result<()> { let (create_ata_ix, user_rewards_account) = env.create_ata_ix( "user_rewards", &user.pubkey(), - &rewarder.reward_token_mint.key, + &rewarder.mint_wrapper.reward_token_mint.key, )?; env.execute_ixs(&[create_ata_ix])?; @@ -172,7 +172,7 @@ fn test_claim_rewards_wrong_authority() -> Result<()> { let (create_ata_ix, wrong_user_rewards) = env.create_ata_ix( "wrong_user_rewards", &wrong_user.pubkey(), - &rewarder.reward_token_mint.key, + &rewarder.mint_wrapper.reward_token_mint.key, )?; env.execute_ixs(&[create_ata_ix])?; @@ -224,7 +224,7 @@ fn test_claim_rewards_no_stake() -> Result<()> { let (create_ata_ix, user_rewards) = env.create_ata_ix( "user_rewards", &user.pubkey(), - &rewarder.reward_token_mint.key, + &rewarder.mint_wrapper.reward_token_mint.key, )?; env.execute_ixs(&[create_ata_ix])?; @@ -315,10 +315,16 @@ fn test_claim_rewards_multiple_users() -> Result<()> { quarry.update_quarry_rewards(&mut env)?; // Create reward accounts - let (ix1, rewards1) = - env.create_ata_ix("rewards1", &user1.pubkey(), &rewarder.reward_token_mint.key)?; - let (ix2, rewards2) = - env.create_ata_ix("rewards2", &user2.pubkey(), &rewarder.reward_token_mint.key)?; + let (ix1, rewards1) = env.create_ata_ix( + "rewards1", + &user1.pubkey(), + &rewarder.mint_wrapper.reward_token_mint.key, + )?; + let (ix2, rewards2) = env.create_ata_ix( + "rewards2", + &user2.pubkey(), + &rewarder.mint_wrapper.reward_token_mint.key, + )?; env.execute_ixs(&[ix1, ix2])?; // Both users claim rewards diff --git a/crates/testsvm-quarry/src/tests/test_mint_wrapper.rs b/crates/testsvm-quarry/src/tests/test_mint_wrapper_actions.rs similarity index 88% rename from crates/testsvm-quarry/src/tests/test_mint_wrapper.rs rename to crates/testsvm-quarry/src/tests/test_mint_wrapper_actions.rs index 0d1f402..764e0a6 100644 --- a/crates/testsvm-quarry/src/tests/test_mint_wrapper.rs +++ b/crates/testsvm-quarry/src/tests/test_mint_wrapper_actions.rs @@ -20,9 +20,9 @@ fn test_create_minter_incorrect_authority() -> Result<()> { let minter_pda: AccountRef = env.get_pda( "unauthorized_minter", &[ - &"MintWrapperMinter", - &rewarder.mint_wrapper.key, - &other_rewarder.rewarder.key, + b"MintWrapperMinter", + rewarder.mint_wrapper.mint_wrapper.key.as_ref(), + other_rewarder.rewarder.key.as_ref(), ], quarry_mint_wrapper::ID, )?; @@ -31,7 +31,7 @@ fn test_create_minter_incorrect_authority() -> Result<()> { quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::NewMinterV2 { new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, admin: unauthorized.pubkey(), // Wrong authority }, new_minter_authority: other_rewarder.rewarder.key, @@ -52,9 +52,9 @@ fn test_create_minter_incorrect_authority() -> Result<()> { let authorized_minter_pda: AccountRef = env.get_pda( "authorized_minter", &[ - &"MintWrapperMinter", - &rewarder.mint_wrapper.key, - &new_minter_authority.pubkey(), + b"MintWrapperMinter", + rewarder.mint_wrapper.mint_wrapper.key.as_ref(), + new_minter_authority.pubkey().as_ref(), ], quarry_mint_wrapper::ID, )?; @@ -63,7 +63,7 @@ fn test_create_minter_incorrect_authority() -> Result<()> { quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::NewMinterV2 { new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, admin: authority.pubkey(), // Correct authority }, new_minter_authority: new_minter_authority.pubkey(), @@ -96,30 +96,27 @@ fn test_update_minter_allowance_incorrect_authority() -> Result<()> { let _rewarder_base = env.new_wallet("rewarder_base")?; // Calculate mint wrapper PDA - let (mint_wrapper, mint_wrapper_bump) = env.find_pda_with_bump( + let mint_wrapper: AccountRef = env.get_pda( "mint_wrapper", - &[&"MintWrapper", &mint_wrapper_base.pubkey()], + &[b"MintWrapper", mint_wrapper_base.pubkey().as_ref()], quarry_mint_wrapper::ID, )?; // Create reward token mint with mint wrapper as authority - let reward_token_mint = env.create_mint("reward_token", 6, &mint_wrapper)?; + let reward_token_mint = env.create_mint("reward_token", 6, &mint_wrapper.key)?; let create_wrapper_ix = anchor_instruction( quarry_mint_wrapper::ID, - quarry_mint_wrapper::client::accounts::NewWrapper { + quarry_mint_wrapper::client::accounts::NewWrapperV2 { base: mint_wrapper_base.pubkey(), - mint_wrapper, + mint_wrapper: mint_wrapper.key, admin: authority.pubkey(), token_mint: reward_token_mint.key, token_program: anchor_spl::token::ID, payer: env.default_fee_payer(), system_program: solana_sdk::system_program::ID, }, - quarry_mint_wrapper::client::args::NewWrapper { - bump: mint_wrapper_bump, - hard_cap: u64::MAX, - }, + quarry_mint_wrapper::client::args::NewWrapperV2 { hard_cap: u64::MAX }, ); env.execute_ixs_with_signers(&[create_wrapper_ix], &[&mint_wrapper_base])?; @@ -128,9 +125,9 @@ fn test_update_minter_allowance_incorrect_authority() -> Result<()> { let minter: AccountRef = env.get_pda( "minter", &[ - &"MintWrapperMinter", - &mint_wrapper, - &new_minter_authority.pubkey(), + b"MintWrapperMinter", + mint_wrapper.key.as_ref(), + new_minter_authority.pubkey().as_ref(), ], quarry_mint_wrapper::ID, )?; @@ -139,7 +136,7 @@ fn test_update_minter_allowance_incorrect_authority() -> Result<()> { quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::NewMinterV2 { new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper, + mint_wrapper: mint_wrapper.key, admin: authority.pubkey(), }, new_minter_authority: new_minter_authority.pubkey(), @@ -155,7 +152,7 @@ fn test_update_minter_allowance_incorrect_authority() -> Result<()> { quarry_mint_wrapper::client::accounts::MinterUpdate { minter: minter.key, minter_update_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper, + mint_wrapper: mint_wrapper.key, admin: authority.pubkey(), }, }, @@ -172,7 +169,7 @@ fn test_update_minter_allowance_incorrect_authority() -> Result<()> { quarry_mint_wrapper::client::accounts::MinterUpdate { minter: minter.key, minter_update_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper, + mint_wrapper: mint_wrapper.key, admin: unauthorized.pubkey(), // Wrong authority }, }, @@ -191,7 +188,7 @@ fn test_update_minter_allowance_incorrect_authority() -> Result<()> { quarry_mint_wrapper::client::accounts::MinterUpdate { minter: minter.key, minter_update_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper, + mint_wrapper: mint_wrapper.key, admin: authority.pubkey(), // Correct authority }, }, @@ -237,7 +234,7 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { let unauthorized_transfer_ix = anchor_instruction( quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::TransferAdmin { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, admin: unauthorized.pubkey(), // Wrong current admin next_admin: new_authority.pubkey(), }, @@ -252,7 +249,7 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { let transfer_ix = anchor_instruction( quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::TransferAdmin { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, admin: original_authority.pubkey(), next_admin: new_authority.pubkey(), }, @@ -279,7 +276,7 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { let unauthorized_accept_ix = anchor_instruction( quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::AcceptAdmin { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, pending_admin: unauthorized.pubkey(), // Wrong pending admin }, quarry_mint_wrapper::client::args::AcceptAdmin {}, @@ -293,7 +290,7 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { let accept_ix = anchor_instruction( quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::AcceptAdmin { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, pending_admin: new_authority.pubkey(), }, quarry_mint_wrapper::client::args::AcceptAdmin {}, @@ -320,9 +317,9 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { let test_minter: AccountRef = env.get_pda( "test_minter", &[ - &"MintWrapperMinter", - &rewarder.mint_wrapper.key, - &test_minter_authority.pubkey(), + b"MintWrapperMinter", + rewarder.mint_wrapper.mint_wrapper.key.as_ref(), + test_minter_authority.pubkey().as_ref(), ], quarry_mint_wrapper::ID, )?; @@ -331,7 +328,7 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::NewMinterV2 { new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, admin: original_authority.pubkey(), // Old authority }, new_minter_authority: test_minter_authority.pubkey(), @@ -351,7 +348,7 @@ fn test_transfer_mint_wrapper_authority() -> Result<()> { quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::NewMinterV2 { new_minter_v2_auth: quarry_mint_wrapper::client::accounts::NewMinterAuth { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, admin: new_authority.pubkey(), // New authority }, new_minter_authority: test_minter_authority.pubkey(), @@ -392,7 +389,7 @@ fn test_perform_mint_incorrect_authority() -> Result<()> { let (create_ata_ix, destination) = env.create_ata_ix( "destination", &destination_owner.pubkey(), - &rewarder.reward_token_mint.into(), + &rewarder.mint_wrapper.reward_token_mint.into(), )?; env.execute_ixs(&[create_ata_ix])?; @@ -401,9 +398,9 @@ fn test_perform_mint_incorrect_authority() -> Result<()> { let mint_ix = anchor_instruction( quarry_mint_wrapper::ID, quarry_mint_wrapper::client::accounts::PerformMint { - mint_wrapper: rewarder.mint_wrapper.key, + mint_wrapper: rewarder.mint_wrapper.mint_wrapper.key, minter_authority: unauthorized.pubkey(), // Wrong minter authority - token_mint: rewarder.reward_token_mint.key, + token_mint: rewarder.mint_wrapper.reward_token_mint.key, destination: destination.into(), minter: rewarder.minter.key, token_program: anchor_spl::token::ID, @@ -427,15 +424,3 @@ fn test_perform_mint_incorrect_authority() -> Result<()> { Ok(()) } - -fn anchor_instruction( - program_id: Pubkey, - accounts: impl anchor_lang::ToAccountMetas, - data: T, -) -> solana_sdk::instruction::Instruction { - solana_sdk::instruction::Instruction::new_with_bytes( - program_id, - &data.data(), - accounts.to_account_metas(None), - ) -} diff --git a/crates/testsvm-quarry/src/tests/test_quarry_setup.rs b/crates/testsvm-quarry/src/tests/test_quarry_setup.rs index e33292f..d6d1a5f 100644 --- a/crates/testsvm-quarry/src/tests/test_quarry_setup.rs +++ b/crates/testsvm-quarry/src/tests/test_quarry_setup.rs @@ -23,10 +23,13 @@ fn test_complete_quarry_integration() -> Result<()> { let test_rewarder = TestRewarder::new_rewarder(&mut env, "main", &authority)?; println!(" ✓ Created rewarder: {}", test_rewarder.rewarder); - println!(" ✓ Created mint wrapper: {}", test_rewarder.mint_wrapper); + println!( + " ✓ Created mint wrapper: {}", + test_rewarder.mint_wrapper.mint_wrapper + ); println!( " ✓ Created reward token: {}", - test_rewarder.reward_token_mint + test_rewarder.mint_wrapper.reward_token_mint ); // Step 4: Create staked token mint @@ -56,12 +59,20 @@ fn test_complete_quarry_integration() -> Result<()> { // Step 7: Verify accounts that were created assert!( - test_rewarder.reward_token_mint.maybe_load(&env)?.is_some(), + test_rewarder + .mint_wrapper + .reward_token_mint + .maybe_load(&env)? + .is_some(), "Reward token mint should exist" ); assert!( - test_rewarder.mint_wrapper.maybe_load(&env)?.is_some(), + test_rewarder + .mint_wrapper + .mint_wrapper + .maybe_load(&env)? + .is_some(), "Mint wrapper should exist" ); @@ -86,7 +97,7 @@ fn test_complete_quarry_integration() -> Result<()> { // Fetch and verify mint wrapper let mint_wrapper = test_rewarder.fetch_mint_wrapper(&env)?; assert_eq!( - mint_wrapper.token_mint, test_rewarder.reward_token_mint.key, + mint_wrapper.token_mint, test_rewarder.mint_wrapper.reward_token_mint.key, "MintWrapper should have correct token mint" ); assert_eq!( @@ -104,7 +115,7 @@ fn test_complete_quarry_integration() -> Result<()> { "Rewarder should have correct authority" ); assert_eq!( - rewarder.mint_wrapper, test_rewarder.mint_wrapper.key, + rewarder.mint_wrapper, test_rewarder.mint_wrapper.mint_wrapper.key, "Rewarder should reference correct mint wrapper" ); println!(" ✓ Rewarder data verified"); diff --git a/crates/testsvm-spl/Cargo.toml b/crates/testsvm-spl/Cargo.toml new file mode 100644 index 0000000..1f67f15 --- /dev/null +++ b/crates/testsvm-spl/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "testsvm-spl" +version = "0.1.0" +authors = ["Ian Macalinao "] +edition = "2024" +license = "Apache-2.0" +description = "SPL helpers for TestSVM" +documentation = "https://docs.rs/testsvm-spl" +homepage = "https://github.com/macalinao/testsvm" +repository = "https://github.com/macalinao/testsvm" +readme = "README.md" +keywords = [ + "ian-macalinao", + "spl", + "token", + "solana", + "testing", + "blockchain", + "testsvm" +] +categories = ["development-tools::testing", "cryptography::cryptocurrencies"] + +[dependencies] +anyhow = "1.0" +solana-sdk = "2.2" +anchor-spl = "0.31" +anchor-lang = "0.31" +testsvm-core = { path = "../testsvm-core", version = "0.1.1" } +solana-address-book = { path = "../solana-address-book", version = "0.1.1" } diff --git a/crates/testsvm-spl/README.md b/crates/testsvm-spl/README.md new file mode 100644 index 0000000..d87e058 --- /dev/null +++ b/crates/testsvm-spl/README.md @@ -0,0 +1,26 @@ +# TestSVM SPL + +[![Crates.io](https://img.shields.io/crates/v/testsvm-spl.svg)](https://crates.io/crates/testsvm-spl) +[![Documentation](https://docs.rs/testsvm-spl/badge.svg)](https://docs.rs/testsvm-spl) + +SPL Token helper functions for the TestSVM testing framework. This crate provides the `TestSVMSPLHelpers` trait that extends TestSVM with comprehensive SPL Token functionality for creating mints, token accounts, and performing token operations in test environments. + +## Features + +- **Mint Creation**: Create SPL token mints with automatic address book registration +- **ATA Management**: Create and manage Associated Token Accounts with proper labeling +- **Token Operations**: Helper functions for common token operations like minting and transfers +- **Address Book Integration**: Automatic registration of mints and ATAs for debugging +- **Authority Management**: Support for mint and freeze authority configuration +- **Decimals Support**: Full support for tokens with configurable decimal places + +## Core Components + +- **TestSVMSPLHelpers**: Trait extending TestSVM with SPL Token functionality +- **Mint Creation**: Create mints with automatic rent calculation and initialization +- **ATA Instructions**: Generate instructions for Associated Token Account creation +- **Address Registration**: Automatic labeling of token-related accounts + +## License + +Copyright (c) 2025 Ian Macalinao. Licensed under the Apache License, Version 2.0. \ No newline at end of file diff --git a/crates/testsvm-spl/src/lib.rs b/crates/testsvm-spl/src/lib.rs new file mode 100644 index 0000000..9af7f4a --- /dev/null +++ b/crates/testsvm-spl/src/lib.rs @@ -0,0 +1,226 @@ +//! # TestSVM SPL Helpers +//! +//! SPL Token helper functions for the TestSVM testing framework. +//! +//! This crate provides the `TestSVMSPLHelpers` trait that extends TestSVM +//! with SPL Token-specific functionality for creating mints, token accounts, +//! and performing token operations. + +use anchor_spl::token; +use anyhow::{Context, Result, anyhow}; +use testsvm_core::prelude::*; + +pub mod prelude; + +/// SPL Token helper functions for TestSVM +pub trait TestSVMSPLHelpers { + /// Create a mint with the test SVM's payer and add to address book + /// + /// # Arguments + /// + /// * `name` - Name for the mint in the address book + /// * `decimals` - Number of decimals for the token + /// * `authority` - Mint and freeze authority for the token + /// + /// # Example + /// + /// ``` + /// use testsvm_core::prelude::*; + /// use testsvm_spl::TestSVMSPLHelpers; + /// + /// # fn main() -> anyhow::Result<()> { + /// let mut svm = TestSVM::init()?; + /// let authority = Keypair::new(); + /// + /// // Create a mint with 6 decimals (like USDC) + /// let mint = svm.create_mint("usdc", 6, &authority.pubkey())?; + /// + /// // The mint is automatically added to the address book + /// assert!(svm.address_book.contains(&mint.key)); + /// + /// // Read the mint information from chain + /// let mint_data: anchor_spl::token::Mint = mint.load(&svm)?; + /// assert_eq!(mint_data.decimals, 6); + /// assert_eq!(mint_data.mint_authority.unwrap(), authority.pubkey()); + /// assert_eq!(mint_data.supply, 0); + /// assert!(mint_data.is_initialized); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example - Minting tokens + /// + /// ``` + /// use testsvm_core::prelude::*; + /// use testsvm_spl::TestSVMSPLHelpers; + /// use anchor_spl::token; + /// + /// # fn main() -> anyhow::Result<()> { + /// let mut svm = TestSVM::init()?; + /// let authority = svm.new_wallet("authority")?; + /// let user = svm.new_wallet("user")?; + /// + /// // Create a mint + /// let mint = svm.create_mint("my_token", 9, &authority.pubkey())?; + /// + /// // Create an ATA for the user + /// let (create_ata_ix, user_ata) = svm.create_ata_ix( + /// "user_ata", + /// &user.pubkey(), + /// &mint.key + /// )?; + /// svm.execute_ixs(&[create_ata_ix])?; + /// + /// // Mint 1000 tokens to the user (with 9 decimals) + /// let mint_amount = 1000 * 10u64.pow(9); + /// let mint_ix = token::spl_token::instruction::mint_to( + /// &token::ID, + /// &mint.key, + /// &user_ata.key, + /// &authority.pubkey(), + /// &[], + /// mint_amount, + /// )?; + /// svm.execute_ixs_with_signers(&[mint_ix], &[&authority])?; + /// + /// // Verify the mint supply was updated + /// let mint_data: token::Mint = mint.load(&svm)?; + /// assert_eq!(mint_data.supply, mint_amount); + /// + /// // Verify the user received the tokens + /// let ata_data: token::TokenAccount = user_ata.load(&svm)?; + /// assert_eq!(ata_data.amount, mint_amount); + /// # Ok(()) + /// # } + /// ``` + fn create_mint( + &mut self, + name: &str, + decimals: u8, + authority: &Pubkey, + ) -> Result>; + + /// Create an associated token account instruction and add to address book + /// + /// Returns the instruction and the ATA address. The instruction must be executed + /// separately to actually create the account on-chain. + /// + /// # Arguments + /// + /// * `label` - Label for the ATA in the address book + /// * `owner` - Owner of the associated token account + /// * `mint` - Mint for which to create the ATA + /// + /// # Example + /// + /// ``` + /// use testsvm_core::prelude::*; + /// use testsvm_spl::TestSVMSPLHelpers; + /// + /// # fn main() -> anyhow::Result<()> { + /// let mut svm = TestSVM::init()?; + /// let authority = Keypair::new(); + /// let user = Keypair::new(); + /// + /// // Create a mint first + /// let mint = svm.create_mint("token", 9, &authority.pubkey())?; + /// + /// // Create an ATA instruction for the user + /// let (ix, ata) = svm.create_ata_ix("user_token_ata", &user.pubkey(), &mint.key)?; + /// + /// // Execute the instruction to create the ATA on-chain + /// svm.execute_ixs(&[ix])?; + /// + /// // The ATA is automatically added to the address book + /// assert!(svm.address_book.contains(&ata.key)); + /// + /// // Read the ATA information from chain + /// let ata_data: anchor_spl::token::TokenAccount = ata.load(&svm)?; + /// assert_eq!(ata_data.owner, user.pubkey()); + /// assert_eq!(ata_data.mint, mint.key); + /// assert_eq!(ata_data.amount, 0); + /// assert_eq!(ata_data.state, anchor_spl::token::spl_token::state::AccountState::Initialized); + /// # Ok(()) + /// # } + /// ``` + fn create_ata_ix( + &mut self, + label: &str, + owner: &Pubkey, + mint: &Pubkey, + ) -> Result<( + solana_sdk::instruction::Instruction, + AccountRef, + )>; +} + +impl TestSVMSPLHelpers for TestSVM { + fn create_mint( + &mut self, + name: &str, + decimals: u8, + authority: &Pubkey, + ) -> Result> { + let mint = Keypair::new(); + + let rent = self + .svm + .minimum_balance_for_rent_exemption(token::Mint::LEN); + + let create_account_ix = solana_sdk::system_instruction::create_account( + &self.default_fee_payer.pubkey(), + &mint.pubkey(), + rent, + anchor_spl::token::Mint::LEN as u64, + &anchor_spl::token::ID, + ); + + let init_mint_ix = anchor_spl::token::spl_token::instruction::initialize_mint( + &anchor_spl::token::ID, + &mint.pubkey(), + authority, + Some(authority), // Set freeze authority to same as mint authority + decimals, + ) + .context("Failed to create initialize mint instruction")?; + + // Add the mint to the address book + let mint_pubkey = mint.pubkey(); + let label = format!("mint:{name}"); + self.address_book + .add(mint_pubkey, label, RegisteredAddress::mint(mint_pubkey))?; + + self.execute_ixs_with_signers(&[create_account_ix, init_mint_ix], &[&mint]) + .map_err(|e| anyhow!("Failed to create mint: {}", e))?; + + Ok(AccountRef::new(mint_pubkey)) + } + + fn create_ata_ix( + &mut self, + label: &str, + owner: &Pubkey, + mint: &Pubkey, + ) -> Result<( + solana_sdk::instruction::Instruction, + AccountRef, + )> { + let ata = anchor_spl::associated_token::get_associated_token_address(owner, mint); + + // Add to address book + self.address_book.add( + ata, + label.to_string(), + RegisteredAddress::ata(ata, *mint, *owner), + )?; + + let ix = anchor_spl::associated_token::spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.default_fee_payer(), + owner, + mint, + &anchor_spl::token::ID, + ); + + Ok((ix, AccountRef::new(ata))) + } +} diff --git a/crates/testsvm-spl/src/prelude.rs b/crates/testsvm-spl/src/prelude.rs new file mode 100644 index 0000000..7e70330 --- /dev/null +++ b/crates/testsvm-spl/src/prelude.rs @@ -0,0 +1,15 @@ +//! # TestSVM SPL Prelude +//! +//! Common imports for TestSVM SPL users. This module re-exports everything +//! from the testsvm-core prelude plus SPL-specific helpers. +//! +//! # Usage +//! +//! ```rust +//! use testsvm_spl::prelude::*; +//! ``` +//! +//! This will import everything from testsvm-core plus: +//! - `TestSVMSPLHelpers` - SPL Token helper trait + +pub use crate::TestSVMSPLHelpers; diff --git a/crates/testsvm/Cargo.toml b/crates/testsvm/Cargo.toml index c174bbb..d08c48e 100644 --- a/crates/testsvm/Cargo.toml +++ b/crates/testsvm/Cargo.toml @@ -15,6 +15,9 @@ categories = ["development-tools::testing", "simulation"] [dependencies] solana-address-book = { version = "0.1.1", path = "../solana-address-book" } anchor-utils = { version = "0.1.0", path = "../anchor-utils" } +testsvm-assertions = { version = "0.1.0", path = "../testsvm-assertions" } +testsvm-core = { version = "0.1.1", path = "../testsvm-core" } +testsvm-spl = { version = "0.1.0", path = "../testsvm-spl" } anyhow = "1.0" anchor-lang = "0.31.1" anchor-spl = { version = "0.31.1", features = ["token"] } diff --git a/crates/testsvm/src/lib.rs b/crates/testsvm/src/lib.rs index fa85be5..962f615 100644 --- a/crates/testsvm/src/lib.rs +++ b/crates/testsvm/src/lib.rs @@ -17,11 +17,7 @@ //! ## Quick Start //! //! ```rust,no_run -//! use testsvm::TestSVM; -//! use solana_sdk::pubkey::Pubkey; -//! use solana_sdk::transaction::Transaction; -//! use solana_sdk::signature::Signer; -//! # use anyhow::Result; +//! use testsvm::prelude::*; //! # fn main() -> Result<()> { //! //! // Create a new test environment @@ -56,7 +52,7 @@ //! ## Working with Programs //! //! ```rust,no_run -//! use testsvm::TestSVM; +//! use testsvm::prelude::*; //! use solana_sdk::pubkey::Pubkey; //! # use anyhow::Result; //! # fn main() -> Result<()> { @@ -81,9 +77,7 @@ //! ## Account Management //! //! ```rust -//! use testsvm::{TestSVM, AccountRef}; -//! use solana_sdk::signature::Signer; -//! # use anyhow::Result; +//! use testsvm::prelude::*; //! # fn main() -> Result<()> { //! //! let mut env = TestSVM::init()?; @@ -108,10 +102,7 @@ //! ## Transaction Building and Execution //! //! ```rust -//! use testsvm::{TestSVM, TXResultAssertions}; -//! use solana_sdk::transaction::Transaction; -//! use solana_sdk::signature::Signer; -//! # use anyhow::Result; +//! use testsvm::prelude::*; //! # fn main() -> Result<()> { //! //! let mut env = TestSVM::init()?; @@ -142,9 +133,7 @@ //! ## Debugging and Analysis //! //! ```rust -//! use testsvm::TestSVM; -//! use solana_sdk::transaction::Transaction; -//! # use anyhow::Result; +//! use testsvm::prelude::*; //! # fn main() -> Result<()> { //! //! let mut env = TestSVM::init()?; @@ -178,15 +167,10 @@ //! ## Integration with Anchor //! //! ```rust,no_run -//! use testsvm::{TestSVM, TXResultAssertions}; -//! use anchor_lang::prelude::*; -//! use testsvm::anchor_instruction; -//! # use anyhow::Result; +//! use testsvm::prelude::*; //! //! // Example program module (would be generated by Anchor) //! // declare_program!(my_program) would generate something similar to: -//! # use anchor_lang::prelude::*; -//! # use solana_sdk::pubkey::Pubkey; //! # pub mod my_program { //! # use solana_sdk::pubkey::Pubkey; //! # pub const ID: Pubkey = Pubkey::new_from_array([0; 32]); @@ -232,17 +216,11 @@ //! # } //! ``` -pub mod account_ref; -pub mod assertions; -pub mod litesvm_helpers; pub mod prelude; -pub mod testsvm; -pub mod tx_result; +// Re-export from other crates pub use ::anchor_utils::*; pub use ::solana_address_book::*; -pub use account_ref::*; -pub use assertions::*; -pub use litesvm_helpers::*; -pub use testsvm::*; -pub use tx_result::*; +pub use testsvm_assertions; +pub use testsvm_core; +pub use testsvm_spl; diff --git a/crates/testsvm/src/prelude.rs b/crates/testsvm/src/prelude.rs index 42ef558..d2c6a6b 100644 --- a/crates/testsvm/src/prelude.rs +++ b/crates/testsvm/src/prelude.rs @@ -10,29 +10,7 @@ //! ``` // Core TestSVM types -pub use crate::{ - AccountRef, TXError, TXErrorAssertions, TXResult, TXResultAssertions, TXSuccessAssertions, - TestSVM, -}; - -// Address book exports -pub use crate::{AddressBook, AddressRole, RegisteredAddress}; - -// PDA utilities -pub use crate::{DerivedPda, SeedPart}; - -// Helper functions -pub use crate::anchor_instruction; - -// Re-export commonly used Solana SDK types -pub use solana_sdk::{ - instruction::Instruction, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_program, sysvar, - transaction::Transaction, -}; - -// Re-export Anchor types that are commonly used -pub use anchor_lang::{InstructionData, Key, ToAccountMetas, prelude::*}; pub use anchor_spl; +pub use testsvm_assertions::{TXErrorAssertions, TXResultAssertions, TXSuccessAssertions}; +pub use testsvm_core::prelude::*; +pub use testsvm_spl::prelude::*; diff --git a/devenv.lock b/devenv.lock index b365196..7dce35e 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1756101922, + "lastModified": 1756415044, "owner": "cachix", "repo": "devenv", - "rev": "372c975fd0d5b7fc1ffbb15c75a21d7f9ea97603", + "rev": "c570189b38b549141179647da3ddde249ac50fec", "type": "github" }, "original": { @@ -94,27 +94,7 @@ "nixpkgs": "nixpkgs", "pre-commit-hooks": [ "git-hooks" - ], - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" ] - }, - "locked": { - "lastModified": 1756348497, - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "0adf92c70d23fb4f703aea5d3ebb51ac65994f7f", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" } } }, diff --git a/devenv.yaml b/devenv.yaml index 37e8589..6bf1e6c 100644 --- a/devenv.yaml +++ b/devenv.yaml @@ -1,8 +1,3 @@ inputs: nixpkgs: url: github:cachix/devenv-nixpkgs/rolling - rust-overlay: - url: github:oxalica/rust-overlay - inputs: - nixpkgs: - follows: nixpkgs