diff --git a/crates/cli/src/scaffold/anchor.rs b/crates/cli/src/scaffold/anchor.rs index 2b424ef8..6c6ffa68 100644 --- a/crates/cli/src/scaffold/anchor.rs +++ b/crates/cli/src/scaffold/anchor.rs @@ -8,6 +8,7 @@ use std::{ use anyhow::{Result, anyhow}; use convert_case::{Case, Casing}; +use log::debug; use serde::{Deserialize, Serialize}; use txtx_addon_network_svm::templates::{AccountDirEntry, AccountEntry}; use txtx_core::kit::helpers::fs::FileLocation; @@ -78,13 +79,20 @@ pub fn try_get_programs_from_project( .cloned() .unwrap_or_default(); - let mut accounts: Vec = manifest - .test - .as_ref() - .and_then(|test| test.validator.as_ref()) - .and_then(|validator| validator.account.as_ref()) - .cloned() - .unwrap_or_default(); + let mut accounts: Vec = if test_suite_paths.is_empty() { + manifest + .test + .as_ref() + .and_then(|test| test.validator.as_ref()) + .and_then(|validator| validator.account.as_ref()) + .cloned() + .unwrap_or_default() + } else { + debug!( + "Test suite paths provided, deferring to Test.toml files for account configuration" + ); + vec![] + }; let mut accounts_dirs = manifest .test diff --git a/crates/core/src/surfnet/remote.rs b/crates/core/src/surfnet/remote.rs index c4a668b2..6dcaff28 100644 --- a/crates/core/src/surfnet/remote.rs +++ b/crates/core/src/surfnet/remote.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use serde_json::json; use solana_account::Account; @@ -154,8 +154,20 @@ impl SurfnetRemoteClient { .get_multiple_accounts(pubkeys) .await .map_err(SurfpoolError::get_multiple_accounts)?; - - let mut accounts_result = vec![]; + debug!("Fetched {:?} accounts from remote", pubkeys); + debug!( + "Found accounts for pubkeys: {:#?}", + remote_accounts + .iter() + .zip(pubkeys) + .filter_map(|(account, pubkey)| if account.is_some() { + Some(pubkey) + } else { + None + }) + .collect::>() + ); + let mut results_map: HashMap = HashMap::new(); let mut mint_accounts_src: Vec<(Pubkey, Account, Pubkey)> = vec![]; let mut program_accounts_src: Vec<(Pubkey, Account, Pubkey)> = vec![]; for (pubkey, remote_account) in pubkeys.iter().zip(remote_accounts) { @@ -164,31 +176,42 @@ impl SurfnetRemoteClient { if let Ok(token_account) = TokenAccount::unpack(&remote_account.data) { mint_accounts_src.push((*pubkey, remote_account, token_account.mint())); } else { - accounts_result.push(GetAccountResult::FoundAccount( + results_map.insert( *pubkey, - remote_account, - // Mark this account as needing to be updated in the SVM, since we fetched it - true, - )); + GetAccountResult::FoundAccount( + *pubkey, + remote_account, + // Mark this account as needing to be updated in the SVM, since we fetched it + true, + ), + ); } } else if remote_account.executable { let program_data_address = get_program_data_address(pubkey); - program_accounts_src.push((*pubkey, remote_account, program_data_address)); } else { - accounts_result.push(GetAccountResult::FoundAccount( + results_map.insert( *pubkey, - remote_account, - // Mark this account as needing to be updated in the SVM, since we fetched it - true, - )); + GetAccountResult::FoundAccount( + *pubkey, + remote_account, + // Mark this account as needing to be updated in the SVM, since we fetched it + true, + ), + ); } } else { - accounts_result.push(GetAccountResult::None(*pubkey)); + results_map.insert(*pubkey, GetAccountResult::None(*pubkey)); } } - if !(mint_accounts_src.is_empty() || program_accounts_src.is_empty()) { + debug!( + "Identified {} mint accounts and {} program accounts to fetch for remote accounts", + mint_accounts_src.len(), + program_accounts_src.len() + ); + + if !(mint_accounts_src.is_empty() && program_accounts_src.is_empty()) { let mint_acc_src_len = mint_accounts_src.len(); let mut account_buffer = mint_accounts_src.clone(); account_buffer.extend_from_slice(&program_accounts_src); @@ -202,23 +225,53 @@ impl SurfnetRemoteClient { .map_err(SurfpoolError::get_multiple_accounts)? .value; + debug!( + "Fetched {} additional accounts from remote", + binding_remote_accounts.len() + ); + debug!( + "Found additional accounts for pubkeys: {:#?}", + binding_remote_accounts + .iter() + .zip(account_pubkeys) + .filter_map(|(account, pubkey)| if account.is_some() { + Some(pubkey) + } else { + None + }) + .collect::>() + ); + for (index, remote_account) in binding_remote_accounts.iter().enumerate() { if index < mint_acc_src_len { - // mint accounts to be pushed - accounts_result.push(GetAccountResult::FoundTokenAccount( - (account_buffer[index].0, account_buffer[index].1.clone()), - (account_buffer[index].2, remote_account.clone()), - )); + // mint accounts to be inserted + results_map.insert( + account_buffer[index].0, + GetAccountResult::FoundTokenAccount( + (account_buffer[index].0, account_buffer[index].1.clone()), + (account_buffer[index].2, remote_account.clone()), + ), + ); } else { - accounts_result.push(GetAccountResult::FoundProgramAccount( - (account_buffer[index].0, account_buffer[index].1.clone()), - (account_buffer[index].2, remote_account.clone()), - )); + results_map.insert( + account_buffer[index].0, + GetAccountResult::FoundProgramAccount( + (account_buffer[index].0, account_buffer[index].1.clone()), + (account_buffer[index].2, remote_account.clone()), + ), + ); } } } - Ok(accounts_result) + Ok(pubkeys + .iter() + .map(|pk| { + results_map + .remove(pk) + .unwrap_or(GetAccountResult::None(*pk)) + }) + .collect()) } pub async fn get_transaction( diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index 26639985..a7e07f22 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -4740,6 +4740,132 @@ async fn test_closed_accounts(test_type: TestType) { } } +#[test_case(TestType::sqlite(); "with on-disk sqlite db")] +#[test_case(TestType::in_memory(); "with in-memory sqlite db")] +#[test_case(TestType::no_db(); "with no db")] +#[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))] +#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")] +#[tokio::test(flavor = "multi_thread")] +async fn test_remote_get_multiple_accounts_only_program_accounts(test_type: TestType) { + let program_pubkey = Pubkey::new_unique(); + let another_test_type = match &test_type { + TestType::OnDiskSqlite(_) => TestType::sqlite(), + TestType::InMemorySqlite => TestType::in_memory(), + TestType::NoDb => TestType::no_db(), + #[cfg(feature = "postgres")] + TestType::Postgres { url, .. } => TestType::Postgres { + url: url.clone(), + surfnet_id: crate::storage::tests::random_surfnet_id(), + }, + }; + + // Start datasource surfnet A (offline, no remote) + let (datasource_url, datasource_svm_locker) = + start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet"); + println!("Datasource surfnet started at {}", datasource_url); + + // Insert a proper upgradeable program into A. + // Using write_program ensures the program and program-data accounts are + // stored with the correct BPF loader owner and serialized state, which is + // required by LiteSVM's account validation. + datasource_svm_locker + .write_program(program_pubkey, None, 0, &[1, 2, 3], &None) + .await + .expect("Failed to write program account"); + + // Start surfnet B pointing to A as remote + let (surfnet_url, _surfnet_svm_locker) = + start_surfnet(vec![], Some(datasource_url), another_test_type) + .expect("Failed to start surfnet"); + println!("Surfnet B started at {}", surfnet_url); + + let rpc_client = RpcClient::new(surfnet_url); + + // Fetch the executable account via get_multiple_accounts. + let accounts = rpc_client + .get_multiple_accounts(&[program_pubkey]) + .await + .expect("Failed to get multiple accounts"); + + let account = accounts[0] + .as_ref() + .expect("Program account should be found (not None)"); + assert!(account.executable, "Account should be executable"); + println!("Program account successfully fetched via get_multiple_accounts"); +} + +#[test_case(TestType::sqlite(); "with on-disk sqlite db")] +#[test_case(TestType::in_memory(); "with in-memory sqlite db")] +#[test_case(TestType::no_db(); "with no db")] +#[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))] +#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")] +#[tokio::test(flavor = "multi_thread")] +async fn test_remote_get_multiple_accounts_ordering(test_type: TestType) { + let program_pubkey = Pubkey::new_unique(); + let plain_pubkey = Pubkey::new_unique(); + let another_test_type = match &test_type { + TestType::OnDiskSqlite(_) => TestType::sqlite(), + TestType::InMemorySqlite => TestType::in_memory(), + TestType::NoDb => TestType::no_db(), + #[cfg(feature = "postgres")] + TestType::Postgres { url, .. } => TestType::Postgres { + url: url.clone(), + surfnet_id: crate::storage::tests::random_surfnet_id(), + }, + }; + + // Start datasource surfnet A (offline, no remote) + let (datasource_url, datasource_svm_locker) = + start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet"); + + // Insert a program account on A + datasource_svm_locker + .write_program(program_pubkey, None, 0, &[1, 2, 3], &None) + .await + .expect("Failed to write program account"); + + // Insert a plain SOL account on A + datasource_svm_locker + .airdrop(&plain_pubkey, LAMPORTS_PER_SOL) + .expect("Failed to airdrop to plain account"); + + // Start surfnet B pointing to A as remote + let (surfnet_url, _) = start_surfnet(vec![], Some(datasource_url), another_test_type) + .expect("Failed to start surfnet B"); + + let rpc_client = RpcClient::new(surfnet_url); + + // Fetch with program FIRST, plain SECOND — this is the ordering that the old code broke + let accounts = rpc_client + .get_multiple_accounts(&[program_pubkey, plain_pubkey]) + .await + .expect("Failed to get multiple accounts"); + + assert_eq!(accounts.len(), 2); + + // Index 0 must be the program account (executable) + let prog_account = accounts[0] + .as_ref() + .expect("Program account should be found at index 0"); + assert!( + prog_account.executable, + "accounts[0] should be executable (program)" + ); + + // Index 1 must be the plain account (not executable, has lamports) + let plain_account = accounts[1] + .as_ref() + .expect("Plain account should be found at index 1"); + assert!( + !plain_account.executable, + "accounts[1] should not be executable (plain)" + ); + assert_eq!( + plain_account.lamports, LAMPORTS_PER_SOL, + "accounts[1] should have airdrop lamports" + ); +} + // websocket rpc methods tests #[test_case(SignatureSubscriptionType::processed() ; "processed commitment")]