diff --git a/Cargo.lock b/Cargo.lock index 5c2130e879..13cf4d465e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4166,7 +4166,8 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litesvm" version = "0.7.1" -source = "git+https://github.com/Lightprotocol/litesvm?rev=a04cb80b6847eb720c840a5e5d9a6f74ce630cc6#a04cb80b6847eb720c840a5e5d9a6f74ce630cc6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bca37ac374948b348e29c74b324dc36f18bbbd1ccf80e2046d967521cbd143" dependencies = [ "agave-feature-set", "agave-precompiles", diff --git a/Cargo.toml b/Cargo.toml index eb405bf0e9..cad193907a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -249,4 +249,3 @@ solana-program-runtime = { git = "https://github.com/Lightprotocol/agave", rev = solana-bpf-loader-program = { git = "https://github.com/Lightprotocol/agave", rev = "3a9e4e0a4411df4d9961aaa7c9f190d3fa15bc21" } # Patch solana-program-memory to use older version where is_nonoverlapping is public solana-program-memory = { git = "https://github.com/anza-xyz/solana-sdk", rev = "1c1d667f161666f12f5a43ebef8eda9470a8c6ee" } -litesvm = { git = "https://github.com/Lightprotocol/litesvm", rev = "a04cb80b6847eb720c840a5e5d9a6f74ce630cc6" } diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 86bdf66358..65a6dceaca 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -20,7 +20,7 @@ use light_ctoken_types::{ use solana_pubkey::Pubkey; #[cfg(feature = "devenv")] -use crate::LightProgramTest; +use crate::{litesvm_extensions::LiteSvmExtensions, LightProgramTest}; #[cfg(feature = "devenv")] pub type CompressibleAccountStore = HashMap; diff --git a/sdk-libs/program-test/src/lib.rs b/sdk-libs/program-test/src/lib.rs index a736cbfcc7..57f2c5fa9d 100644 --- a/sdk-libs/program-test/src/lib.rs +++ b/sdk-libs/program-test/src/lib.rs @@ -136,6 +136,7 @@ pub mod compressible; #[cfg(feature = "devenv")] pub mod forester; pub mod indexer; +pub mod litesvm_extensions; pub mod logging; pub mod program_test; pub mod utils; @@ -144,4 +145,5 @@ pub use light_client::{ indexer::{AddressWithTree, Indexer}, rpc::{Rpc, RpcError}, }; +pub use litesvm_extensions::LiteSvmExtensions; pub use program_test::{config::ProgramTestConfig, LightProgramTest}; diff --git a/sdk-libs/program-test/src/litesvm_extensions.rs b/sdk-libs/program-test/src/litesvm_extensions.rs new file mode 100644 index 0000000000..1f63c743ea --- /dev/null +++ b/sdk-libs/program-test/src/litesvm_extensions.rs @@ -0,0 +1,199 @@ +use litesvm::LiteSVM; +use solana_account::ReadableAccount; +use solana_sdk::{account::Account, pubkey::Pubkey}; + +/// Extension trait for LiteSVM to add utility methods. +/// +/// This trait provides additional functionality on top of the base LiteSVM, +/// such as querying all accounts owned by a specific program. +pub trait LiteSvmExtensions { + /// Returns all accounts owned by the provided program id. + /// + /// This method iterates through the internal accounts database and filters + /// accounts by their owner field. + /// + /// # Arguments + /// + /// * `program_id` - The program ID to filter accounts by + /// + /// # Returns + /// + /// A vector of tuples containing (Pubkey, Account) for all accounts owned by the program + fn get_program_accounts(&self, program_id: &Pubkey) -> Vec<(Pubkey, Account)>; +} + +impl LiteSvmExtensions for LiteSVM { + fn get_program_accounts(&self, program_id: &Pubkey) -> Vec<(Pubkey, Account)> { + self.accounts_db() + .inner + .iter() + .filter(|(_, account)| account.owner() == program_id) + .map(|(pubkey, account)| (*pubkey, Account::from(account.clone()))) + .collect() + } +} + +#[cfg(test)] +mod tests { + use solana_sdk::{ + native_token::LAMPORTS_PER_SOL, + signature::{Keypair, Signer}, + system_instruction::{create_account, transfer}, + system_program, + transaction::{Transaction, VersionedTransaction}, + }; + + use super::*; + + #[test] + fn test_get_program_accounts() { + let mut svm = LiteSVM::new(); + let payer_keypair = Keypair::new(); + let payer = payer_keypair.pubkey(); + + // Fund the payer + svm.airdrop(&payer, 10 * LAMPORTS_PER_SOL).unwrap(); + + // Establish baseline of system accounts + let baseline_system_accounts = svm.get_program_accounts(&system_program::id()); + let baseline_count = baseline_system_accounts.len(); + + // Create multiple accounts owned by system program + let num_system_accounts = 5; + let mut system_owned_accounts = vec![]; + + for i in 0..num_system_accounts { + let new_account_keypair = Keypair::new(); + let new_account = new_account_keypair.pubkey(); + let space = 10 + i; + let rent_amount = svm.minimum_balance_for_rent_exemption(space); + + let instruction = create_account( + &payer, + &new_account, + rent_amount, + space as u64, + &system_program::id(), + ); + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer), + &[&payer_keypair, &new_account_keypair], + svm.latest_blockhash(), + ); + + svm.send_transaction(VersionedTransaction::from(tx)) + .unwrap(); + system_owned_accounts.push((new_account, rent_amount, space)); + } + + // Create a custom program and some accounts owned by it + let custom_program_id = Pubkey::new_unique(); + let num_custom_accounts = 3; + let mut custom_owned_accounts = vec![]; + + for i in 0..num_custom_accounts { + let new_account_keypair = Keypair::new(); + let new_account = new_account_keypair.pubkey(); + let space = 20 + i * 2; + let rent_amount = svm.minimum_balance_for_rent_exemption(space); + + let instruction = create_account( + &payer, + &new_account, + rent_amount, + space as u64, + &custom_program_id, + ); + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer), + &[&payer_keypair, &new_account_keypair], + svm.latest_blockhash(), + ); + + svm.send_transaction(VersionedTransaction::from(tx)) + .unwrap(); + custom_owned_accounts.push((new_account, rent_amount, space)); + } + + // Do some transfers to create new accounts + for i in 0..3 { + let to = Pubkey::new_unique(); + let amount = (i + 1) * 1000; + let instruction = transfer(&payer, &to, amount); + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer), + &[&payer_keypair], + svm.latest_blockhash(), + ); + svm.send_transaction(VersionedTransaction::from(tx)) + .unwrap(); + } + + // Test get_program_accounts for system program + let system_accounts = svm.get_program_accounts(&system_program::id()); + + // Should contain baseline + 5 created accounts + 3 transfer recipients + let expected_count = baseline_count + num_system_accounts + 3; + assert_eq!( + system_accounts.len(), + expected_count, + "Expected {} system accounts (baseline {} + {} created + 3 transfers), got {}", + expected_count, + baseline_count, + num_system_accounts, + system_accounts.len() + ); + + // Verify all our created system accounts are present with correct data + for (pubkey, expected_lamports, expected_space) in &system_owned_accounts { + let found = system_accounts + .iter() + .find(|(pk, _)| pk == pubkey) + .expect("System account should be found"); + + assert_eq!(found.1.lamports, *expected_lamports); + assert_eq!(found.1.data.len(), *expected_space); + assert_eq!(found.1.owner, system_program::id()); + } + + // Verify individual get_account returns the same data + for (pubkey, _, _) in &system_owned_accounts { + let individual_account = svm.get_account(pubkey).unwrap(); + let from_program_accounts = + system_accounts.iter().find(|(pk, _)| pk == pubkey).unwrap(); + + assert_eq!( + individual_account.lamports, + from_program_accounts.1.lamports + ); + assert_eq!(individual_account.data, from_program_accounts.1.data); + assert_eq!(individual_account.owner, from_program_accounts.1.owner); + } + + // Test get_program_accounts for custom program + let custom_accounts = svm.get_program_accounts(&custom_program_id); + assert_eq!(custom_accounts.len(), num_custom_accounts); + + // Verify all custom accounts are present with correct data + for (pubkey, expected_lamports, expected_space) in &custom_owned_accounts { + let found = custom_accounts + .iter() + .find(|(pk, _)| pk == pubkey) + .expect("Custom account should be found"); + + assert_eq!(found.1.lamports, *expected_lamports); + assert_eq!(found.1.data.len(), *expected_space); + assert_eq!(found.1.owner, custom_program_id); + } + + // Test get_program_accounts for non-existent program + let nonexistent_program = Pubkey::new_unique(); + let no_accounts = svm.get_program_accounts(&nonexistent_program); + assert_eq!(no_accounts.len(), 0); + } +} diff --git a/sdk-libs/program-test/src/program_test/rpc.rs b/sdk-libs/program-test/src/program_test/rpc.rs index 9a2c85b48d..0da5c6c077 100644 --- a/sdk-libs/program-test/src/program_test/rpc.rs +++ b/sdk-libs/program-test/src/program_test/rpc.rs @@ -28,6 +28,7 @@ use solana_transaction_status_client_types::TransactionStatus; use crate::{ indexer::{TestIndexer, TestIndexerExtensions}, + litesvm_extensions::LiteSvmExtensions, program_test::LightProgramTest, }; @@ -56,9 +57,9 @@ impl Rpc for LightProgramTest { async fn get_program_accounts( &self, - _program_id: &Pubkey, + program_id: &Pubkey, ) -> Result, RpcError> { - unimplemented!("get_program_accounts") + Ok(self.context.get_program_accounts(program_id)) } async fn confirm_transaction(&self, _transaction: Signature) -> Result {