use std::{str::FromStr, sync::Arc, time::Duration};
use base64::Engine;
use crossbeam_channel::{unbounded, unbounded as crossbeam_unbounded};
use ed25519_dalek::Signer as DalekSigner;
use jsonrpc_core::{
Error, Result as JsonRpcResult,
futures::future::{self, join_all},
};
use jsonrpc_core_client::transports::http;
use solana_account::Account;
use solana_account_decoder::{UiAccountData, UiAccountEncoding, parse_account_data::ParsedAccount};
use solana_address_lookup_table_interface::state::{AddressLookupTable, LookupTableMeta};
use solana_client::{
nonblocking::rpc_client::RpcClient,
rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig},
rpc_response::RpcLogsResponse,
};
use solana_clock::{Clock, Slot};
use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_ed25519_program::new_ed25519_instruction_with_signature;
use solana_epoch_info::EpochInfo;
use solana_epoch_schedule::EpochSchedule;
use solana_hash::Hash;
use solana_keypair::Keypair;
use solana_message::{
AddressLookupTableAccount, Message, MessageHeader, VersionedMessage, legacy,
v0::{self, MessageAddressTableLookup},
};
use solana_pubkey::Pubkey;
use solana_rpc_client_api::response::Response as RpcResponse;
use solana_signer::Signer;
use solana_system_interface::{
instruction as system_instruction, instruction::transfer, program as system_program,
};
use solana_transaction::{Transaction, versioned::VersionedTransaction};
use surfpool_types::{
CheatcodeConfig, CheatcodeControlConfig, CheatcodeFilter, DEFAULT_SLOT_TIME_MS, Idl,
RpcProfileDepth, RpcProfileResultConfig, SimnetCommand, SimnetEvent, SurfpoolConfig,
UiAccountChange, UiAccountProfileState, UiKeyedProfileResult,
types::{
BlockProductionMode, RpcConfig, SimnetConfig, SubgraphConfig, TransactionStatusEvent,
UuidOrSignature,
},
};
use test_case::test_case;
use tokio::{sync::RwLock, task};
use uuid::Uuid;
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
use crate::{
error::SurfpoolError,
rpc::{
RunloopContext,
full::{FullClient, SurfpoolRpcSendTransactionConfig},
minimal::MinimalClient,
surfnet_cheatcodes::{SurfnetCheatcodes, SurfnetCheatcodesRpc},
},
runloops::start_local_surfnet_runloop,
storage::tests::TestType,
surfnet::{
FINALIZATION_SLOT_THRESHOLD, PluginCommand, SignatureSubscriptionType,
locker::SurfnetSvmLocker, svm::SurfnetSvm,
},
tests::helpers::get_free_port,
types::{TimeTravelConfig, TransactionLoadedAddresses},
};
fn wait_for_ready_and_connected(simnet_events_rx: &crossbeam_channel::Receiver<SimnetEvent>) {
let mut ready = false;
let mut connected = false;
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::Ready(_)) => {
println!("Simnet is ready");
ready = true;
}
Ok(SimnetEvent::Connected(_)) => {
println!("Simnet is connected");
connected = true;
}
_ => (),
}
if ready && connected {
break;
}
}
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test]
async fn test_simnet_ready(test_type: TestType) {
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
block_production_mode: BlockProductionMode::Manual, ..SimnetConfig::default()
}],
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
svm_locker,
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::Ready(_))
| Ok(SimnetEvent::Connected(_))
| Ok(SimnetEvent::EpochInfoUpdate(_)) => break,
e => panic!("Expected startup event: {e:?}"),
}
}
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test]
async fn test_simnet_ticks(test_type: TestType) {
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let (test_tx, test_rx) = unbounded();
let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
svm_locker,
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
let _ = hiro_system_kit::thread_named("ticks").spawn(move || {
let mut ticks = 0;
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::SystemClockUpdated(_)) => ticks += 1,
_ => (),
}
if ticks > 100 {
let _ = test_tx.send(true);
}
}
});
match test_rx.recv_timeout(Duration::from_secs(20)) {
Ok(_) => (),
Err(e) => panic!("not enough ticks: {e:?}"),
}
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test]
async fn test_simnet_some_sol_transfers(test_type: TestType) {
let n_addresses = 10;
let airdrop_keypairs = (0..n_addresses).map(|_| Keypair::new()).collect::<Vec<_>>();
let airdrop_addresses: Vec<Pubkey> = airdrop_keypairs.iter().map(|kp| kp.pubkey()).collect();
let airdrop_token_amount = LAMPORTS_PER_SOL;
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
airdrop_addresses: airdrop_addresses.clone(),
airdrop_token_amount,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
svm_locker,
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
wait_for_ready_and_connected(&simnet_events_rx);
let minimal_client =
http::connect::<MinimalClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let full_client =
http::connect::<FullClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let recent_blockhash = full_client
.get_latest_blockhash(None)
.await
.map(|r| {
Hash::from_str(r.value.blockhash.as_str()).expect("Failed to deserialize blockhash")
})
.expect("Failed to get blockhash");
let balances = join_all(
airdrop_addresses
.iter()
.map(|pk| minimal_client.get_balance(pk.to_string(), None)),
)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("Failed to fetch balances");
assert!(
balances.iter().all(|b| b.value == airdrop_token_amount),
"All addresses did not receive the airdrop"
);
let _transfers = join_all(airdrop_keypairs.iter().map(|kp| {
let msg = Message::new_with_blockhash(
&[system_instruction::transfer(
&kp.pubkey(),
&airdrop_addresses[0],
airdrop_token_amount / 2,
)],
Some(&kp.pubkey()),
&recent_blockhash,
);
let Ok(tx) = VersionedTransaction::try_new(
VersionedMessage::Legacy(msg),
&vec![kp.insecure_clone()],
) else {
return Box::pin(future::err(Error::invalid_params("tx")));
};
let Ok(encoded) = bincode::serialize(&tx) else {
return Box::pin(future::err(Error::invalid_params("encoded")));
};
let data = bs58::encode(encoded).into_string();
Box::pin(future::ready(Ok(full_client.send_transaction(data, None))))
}))
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("Transfers failed");
let expected = airdrop_addresses.len();
let _ = task::spawn_blocking(move || {
let mut processed = 0;
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::TransactionProcessed(..)) => processed += 1,
_ => (),
}
if processed == expected {
break;
}
}
})
.await;
let final_balances = join_all(
airdrop_addresses
.iter()
.map(|pk| minimal_client.get_balance(pk.to_string(), None)),
)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("Failed to fetch final balances");
assert!(
final_balances.iter().enumerate().all(|(i, b)| {
if i == 0 {
b.value > airdrop_token_amount
} else {
b.value < airdrop_token_amount / 2
}
}), "Some transfers failed"
);
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_add_alt_entries_fetching(test_type: TestType) {
let payer = Keypair::new();
let pk = payer.pubkey();
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let airdrop_token_amount = LAMPORTS_PER_SOL;
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
airdrop_addresses: vec![pk], airdrop_token_amount,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
println!("Initializing SVM, binding to port {}", bind_port);
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = Arc::new(RwLock::new(surfnet_svm));
let moved_svm_locker = svm_locker.clone();
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
SurfnetSvmLocker(moved_svm_locker),
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
let svm_locker = SurfnetSvmLocker(svm_locker);
wait_for_ready_and_connected(&simnet_events_rx);
let full_client =
http::connect::<FullClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let recent_blockhash = full_client
.get_latest_blockhash(None)
.await
.map(|r| {
Hash::from_str(r.value.blockhash.as_str()).expect("Failed to deserialize blockhash")
})
.expect("Failed to get blockhash");
let random_address = Pubkey::from_str_const("7zdYkYf7yD83j3TLXmkhxn6LjQP9y9bQ4pjfpquP8Hqw");
let instruction = transfer(&pk, &random_address, 100);
let alt_address = Pubkey::from_str_const("5KcPJehcpBLcPde2UhmY4dE9zCrv2r9AKFmW5CGtY1io");
let address_lookup_table_account = AddressLookupTableAccount {
key: alt_address,
addresses: vec![random_address],
};
let tx = VersionedTransaction::try_new(
VersionedMessage::V0(
v0::Message::try_compile(
&payer.pubkey(),
&[instruction],
&[address_lookup_table_account],
recent_blockhash,
)
.expect("Failed to compile message"),
),
&[payer],
)
.expect("Failed to create transaction");
let Ok(encoded) = bincode::serialize(&tx) else {
panic!("Failed to serialize transaction");
};
let data = bs58::encode(encoded).into_string();
let _ = match full_client.send_transaction(data, None).await {
Ok(res) => println!("Send transaction result: {}", res),
Err(err) => println!("Send transaction error result: {}", err),
};
let mut processed = 0;
let expected = 1;
let mut alt_updated = false;
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::TransactionProcessed(..)) => processed += 1,
Ok(SimnetEvent::AccountUpdate(_, account)) => {
if account == alt_address {
alt_updated = true;
}
}
Ok(SimnetEvent::ClockUpdate(_)) => {
}
Ok(SimnetEvent::SystemClockUpdated(_)) => {
}
other => println!("Unexpected event: {:?}", other),
}
if processed == expected && alt_updated {
break;
}
}
let alts = tx.message.address_table_lookups().clone().unwrap();
let mut acc_keys = tx.message.static_account_keys().to_vec();
let mut alt_pubkeys = alts.iter().map(|msg| msg.account_key).collect::<Vec<_>>();
let mut table_entries = join_all(alts.iter().map(|msg| async {
let mut loaded_addresses = TransactionLoadedAddresses::new();
svm_locker
.get_lookup_table_addresses(&None, msg, &mut loaded_addresses)
.await?;
Ok::<_, SurfpoolError>(
loaded_addresses
.all_loaded_addresses()
.into_iter()
.map(|p| *p)
.collect::<Vec<Pubkey>>(),
)
}))
.await
.into_iter()
.collect::<Result<Vec<Vec<Pubkey>>, SurfpoolError>>()
.unwrap()
.into_iter()
.flatten()
.collect();
acc_keys.append(&mut alt_pubkeys);
acc_keys.append(&mut table_entries);
assert!(
acc_keys.iter().all(|key| {
svm_locker
.get_account_local(key)
.inner
.map_account()
.is_ok()
}),
"account not found"
);
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_simulate_add_alt_entries_fetching(test_type: TestType) {
let payer = Keypair::new();
let pk = payer.pubkey();
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let airdrop_token_amount = LAMPORTS_PER_SOL;
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
airdrop_addresses: vec![pk], airdrop_token_amount,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = Arc::new(RwLock::new(surfnet_svm));
let moved_svm_locker = svm_locker.clone();
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
SurfnetSvmLocker(moved_svm_locker),
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
let svm_locker = SurfnetSvmLocker(svm_locker);
wait_for_ready_and_connected(&simnet_events_rx);
let full_client =
http::connect::<FullClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let random_address = Pubkey::from_str_const("7zdYkYf7yD83j3TLXmkhxn6LjQP9y9bQ4pjfpquP8Hqw");
let instruction = transfer(&pk, &random_address, 100);
let recent_blockhash = svm_locker.with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
let alt_address = Pubkey::from_str_const("5KcPJehcpBLcPde2UhmY4dE9zCrv2r9AKFmW5CGtY1io");
let address_lookup_table_account = AddressLookupTableAccount {
key: alt_address,
addresses: vec![random_address],
};
let tx = VersionedTransaction::try_new(
VersionedMessage::V0(
v0::Message::try_compile(
&payer.pubkey(),
&[instruction],
&[address_lookup_table_account],
recent_blockhash,
)
.expect("Failed to compile message"),
),
&[payer],
)
.expect("Failed to create transaction");
let Ok(encoded) = bincode::serialize(&tx) else {
panic!("Failed to serialize transaction");
};
let data = bs58::encode(encoded).into_string();
let simulation_res = full_client
.simulate_transaction(data.clone(), None)
.await
.unwrap();
assert_eq!(
simulation_res.value.err, None,
"Unexpected simulation error"
);
assert!(
simulation_res.value.loaded_accounts_data_size.is_some(),
"Expected loaded_accounts_data_size to be present"
);
assert_eq!(
simulation_res.value.loaded_accounts_data_size.unwrap(),
140134,
"Incorrect loaded_accounts_data_size value"
);
let simulation_res2 = full_client
.simulate_transaction(
data,
Some(RpcSimulateTransactionConfig {
sig_verify: false,
replace_recent_blockhash: false,
commitment: Some(CommitmentConfig::confirmed()),
encoding: None,
accounts: None,
min_context_slot: None,
inner_instructions: false,
}),
)
.await
.unwrap();
assert_eq!(
simulation_res2.value.err, None,
"Unexpected simulation error"
);
assert!(
simulation_res2.value.loaded_accounts_data_size.is_some(),
"Expected loaded_accounts_data_size to be present"
);
assert!(
simulation_res2.value.loaded_accounts_data_size.unwrap() > 0,
"Expected loaded_accounts_data_size to be greater than 0"
);
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_simulate_transaction_no_signers(test_type: TestType) {
let payer = Keypair::new();
let pk = payer.pubkey();
let lamports = LAMPORTS_PER_SOL;
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let airdrop_token_amount = LAMPORTS_PER_SOL;
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
airdrop_addresses: vec![pk], airdrop_token_amount,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = Arc::new(RwLock::new(surfnet_svm));
let moved_svm_locker = svm_locker.clone();
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
SurfnetSvmLocker(moved_svm_locker),
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
let svm_locker = SurfnetSvmLocker(svm_locker);
wait_for_ready_and_connected(&simnet_events_rx);
let full_client =
http::connect::<FullClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let _ = full_client
.request_airdrop(payer.pubkey().to_string(), 2 * lamports, None)
.await;
let recent_blockhash = svm_locker
.get_latest_blockhash(&CommitmentConfig::confirmed())
.unwrap();
let mut msg = legacy::Message::new(
&[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
Some(&payer.pubkey()),
);
msg.recent_blockhash = recent_blockhash;
let tx = Transaction::new_unsigned(msg);
let simulation_res = full_client
.simulate_transaction(
bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
Some(RpcSimulateTransactionConfig {
sig_verify: false,
replace_recent_blockhash: false,
commitment: Some(CommitmentConfig::finalized()),
encoding: None,
accounts: None,
min_context_slot: None,
inner_instructions: false,
}),
)
.await
.unwrap();
assert_eq!(
simulation_res.value.err, None,
"Unexpected simulation error"
);
assert!(
simulation_res.value.loaded_accounts_data_size.is_some(),
"Expected loaded_accounts_data_size to be present"
);
assert!(
simulation_res.value.loaded_accounts_data_size.unwrap() > 0,
"Expected loaded_accounts_data_size to be greater than 0"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_transaction_with_ed25519_instruction(test_type: TestType) {
let payer = Keypair::new();
let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
svm_instance
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_instance.latest_blockhash();
let tx = {
let payer_dalek =
ed25519_dalek::Keypair::from_bytes(&payer.to_bytes()).expect("invalid dalek keypair");
let message = b"surfpool ed25519 precompile integration test";
let signature = payer_dalek.sign(message);
let ed25519_ix = new_ed25519_instruction_with_signature(
message,
&signature.to_bytes(),
payer.pubkey().as_array(),
);
let compute_budget_ixs = [
ComputeBudgetInstruction::set_compute_unit_limit(100_000),
ComputeBudgetInstruction::set_compute_unit_price(1),
];
let tx_message = Message::new_with_blockhash(
&[
compute_budget_ixs[0].clone(),
compute_budget_ixs[1].clone(),
ed25519_ix,
],
Some(&payer.pubkey()),
&recent_blockhash,
);
VersionedTransaction::try_new(VersionedMessage::Legacy(tx_message), &[payer])
.expect("Failed to create ed25519 transaction")
};
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let simulation_res = svm_locker.simulate_transaction(tx.clone(), true);
assert!(
simulation_res.is_ok(),
"Expected ed25519 transaction simulation to succeed"
);
let (status_tx, status_rx) = unbounded();
let _ = svm_locker
.process_transaction(&None, tx, status_tx, true, true)
.await
.unwrap();
match status_rx.recv() {
Ok(TransactionStatusEvent::Success(_)) => {
println!("Transaction processed successfully");
}
Ok(TransactionStatusEvent::SimulationFailure((error, _))) => {
panic!("Transaction simulation failed: {:?}", error);
}
Ok(TransactionStatusEvent::ExecutionFailure((error, _))) => {
panic!("Transaction execution failed: {:?}", error);
}
Ok(TransactionStatusEvent::VerificationFailure(error)) => {
panic!("Transaction verification failed: {}", error);
}
Err(e) => {
panic!("Failed to receive transaction status: {:?}", e);
}
}
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_surfnet_estimate_compute_units(test_type: TestType) {
let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let rpc_server = crate::rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc::empty();
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
svm_instance
.airdrop(&payer.pubkey(), lamports_to_send * 2)
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_instance.latest_blockhash();
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message.clone()), &[&payer])
.unwrap();
let tx_bytes = bincode::serialize(&tx).unwrap();
let tx_b64 = base64::engine::general_purpose::STANDARD.encode(&tx_bytes);
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let response_no_tag_initial: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(Some(runloop_context.clone()), tx_b64.clone(), None, None)
.await;
assert!(
response_no_tag_initial.is_ok(),
"RPC call with None tag failed: {:?}",
response_no_tag_initial.err()
);
let rpc_response_value_no_tag = response_no_tag_initial.unwrap().value;
assert!(
rpc_response_value_no_tag
.transaction_profile
.error_message
.is_none(),
"CU estimation with None tag failed"
);
assert!(
rpc_response_value_no_tag
.transaction_profile
.compute_units_consumed
> 0,
"Invalid compute units consumed for None tag"
);
assert!(
rpc_response_value_no_tag
.transaction_profile
.log_messages
.is_some(),
"Log messages should be present for None tag"
);
let tag1 = "test_tag_1".to_string();
println!("\nTesting with tag: {}", tag1);
let response_tagged_1: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(
Some(runloop_context.clone()),
tx_b64.clone(),
Some(tag1.clone()),
None,
)
.await;
assert!(
response_tagged_1.is_ok(),
"RPC call with tag1 failed: {:?}",
response_tagged_1.err()
);
let rpc_response_tagged_1_value = response_tagged_1.unwrap().value;
assert!(
rpc_response_tagged_1_value
.transaction_profile
.error_message
.is_none(),
"CU estimation with tag1 failed"
);
println!("Retrieving profile results for tag: {}", tag1);
let results_vec_tag1 = rpc_server
.get_profile_results_by_tag(Some(runloop_context.clone()), tag1.clone(), None)
.unwrap()
.value
.unwrap_or_default();
assert_eq!(results_vec_tag1.len(), 1, "Expected 1 result for tag1");
assert_eq!(
results_vec_tag1[0]
.transaction_profile
.compute_units_consumed,
rpc_response_tagged_1_value
.transaction_profile
.compute_units_consumed
);
assert_eq!(
results_vec_tag1[0]
.transaction_profile
.error_message
.is_none(),
rpc_response_tagged_1_value
.transaction_profile
.error_message
.is_none()
);
let tag_non_existent = "non_existent_tag".to_string();
println!(
"\nTesting retrieval with non-existent tag: {}",
tag_non_existent
);
let results_non_existent_vec = rpc_server
.get_profile_results_by_tag(
Some(runloop_context.clone()),
tag_non_existent.clone(),
None,
)
.unwrap()
.value
.unwrap_or_default();
assert!(
results_non_existent_vec.is_empty(),
"Expected empty vec for non-existent tag"
);
let tag2 = "test_tag_2".to_string();
println!("\nTesting multiple estimations with tag: {}", tag2);
let response_tagged_2a: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(
Some(runloop_context.clone()),
tx_b64.clone(),
Some(tag2.clone()),
None,
)
.await;
assert!(response_tagged_2a.is_ok(), "First call with tag2 failed");
let cu_2a_profile_result = response_tagged_2a.unwrap().value;
println!(
"CU estimation 1 (tag: {}): consumed = {}, success = {}",
tag2,
cu_2a_profile_result
.transaction_profile
.compute_units_consumed,
cu_2a_profile_result
.transaction_profile
.error_message
.is_none()
);
let response_tagged_2b: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(
Some(runloop_context.clone()),
tx_b64.clone(),
Some(tag2.clone()),
None,
)
.await;
assert!(response_tagged_2b.is_ok(), "Second call with tag2 failed");
let cu_2b_profile_result = response_tagged_2b.unwrap().value;
println!("Retrieving profile results for tag: {}", tag2);
let results_response_tag2 =
rpc_server.get_profile_results_by_tag(Some(runloop_context.clone()), tag2.clone(), None);
assert!(
results_response_tag2.is_ok(),
"get_profile_results for tag2 failed"
);
let results_vec_tag2 = results_response_tag2.unwrap().value.unwrap_or_default();
assert_eq!(results_vec_tag2.len(), 2, "Expected 2 results for tag2");
assert_eq!(
results_vec_tag2[0]
.transaction_profile
.compute_units_consumed,
cu_2a_profile_result
.transaction_profile
.compute_units_consumed
);
assert_eq!(
results_vec_tag2[1]
.transaction_profile
.compute_units_consumed,
cu_2b_profile_result
.transaction_profile
.compute_units_consumed
);
println!(
"\nTesting None tag again to ensure no interference with tag: {}",
tag1
);
let response_no_tag_again: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(Some(runloop_context.clone()), tx_b64.clone(), None, None)
.await;
assert!(
response_no_tag_again.is_ok(),
"RPC call with None tag (again) failed"
);
let _rpc_response_no_tag_again_value = response_no_tag_again.unwrap().value;
println!("Retrieving profile results for tag: {} again", tag1);
let results_response_tag1_again =
rpc_server.get_profile_results_by_tag(Some(runloop_context), tag1.clone(), None);
assert!(
results_response_tag1_again.is_ok(),
"get_profile_results for tag1 (again) failed"
);
let results_vec_tag1_again = results_response_tag1_again
.unwrap()
.value
.unwrap_or_default();
assert_eq!(
results_vec_tag1_again.len(),
1,
"Expected 1 result for tag1 after another None tag call, was {}",
results_vec_tag1_again.len()
);
assert_eq!(
results_vec_tag1_again[0]
.transaction_profile
.compute_units_consumed,
rpc_response_tagged_1_value
.transaction_profile
.compute_units_consumed
);
let (mut svm_for_send, simnet_rx_for_send, _geyser_rx_for_send) = test_type.initialize_svm();
svm_for_send
.airdrop(&payer.pubkey(), lamports_to_send * 2)
.unwrap()
.unwrap();
let latest_blockhash_for_send = svm_for_send.latest_blockhash();
let message_for_send = Message::new_with_blockhash(
&[transfer(&payer.pubkey(), &recipient, lamports_to_send)],
Some(&payer.pubkey()),
&latest_blockhash_for_send,
);
let tx_for_send =
VersionedTransaction::try_new(VersionedMessage::Legacy(message_for_send), &[&payer])
.unwrap();
let _send_result = svm_for_send.send_transaction(tx_for_send, true, true);
let mut found_cu_event = false;
for _ in 0..10 {
if let Ok(event) = simnet_rx_for_send.try_recv() {
if let surfpool_types::SimnetEvent::InfoLog(_, msg) = event {
if msg.starts_with("CU Estimation for tx") {
println!("Found CU estimation event: {}", msg);
found_cu_event = true;
break;
}
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
assert!(found_cu_event, "Did not find CU estimation SimnetEvent");
}
#[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"))]
fn test_enable_and_disable_cheatcodes(test_type: TestType) {
let valid_cheatcode_method = "surfnet_getActiveIdl".to_string();
let enable_cheatcode_method = "surfnet_enableCheatcode".to_string();
let disable_cheatcode_method = "surfnet_disableCheatcode".to_string();
let invalid_cheatcode_method = "surfnet_invalidCheatcode".to_string();
let available_methods = vec![
valid_cheatcode_method.clone(),
enable_cheatcode_method.clone(),
disable_cheatcode_method.clone(),
];
let rpc_server = SurfnetCheatcodesRpc {
registered_methods: Arc::new(std::sync::RwLock::new(available_methods.clone())),
};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let disable_cheatcode_pass_result = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![valid_cheatcode_method.clone()]),
None,
);
assert!(
disable_cheatcode_pass_result.is_ok(),
"Expected surfnet_disableCheatcode to pass"
);
assert!(
runloop_context
.cheatcode_config
.lock()
.unwrap()
.is_cheatcode_disabled(&valid_cheatcode_method),
"Expected cheatcode to be disabled"
);
let disable_enable_cheatcode_lockout_false_fails = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![enable_cheatcode_method.clone()]),
None,
);
let expected_error: jsonrpc_core::Result<()> = Err(SurfpoolError::disable_cheatcode(
"Cannot disable surfnet_enableCheatcode or surfnet_disableCheatcode rpc method when lockout is not enabled".to_string(),
)
.into());
assert!(
disable_enable_cheatcode_lockout_false_fails.is_err(),
"Expected surfnet_disableCheatcode to fail when disabling surfnet_enableCheatcode with lockout == false"
);
assert_eq!(
disable_enable_cheatcode_lockout_false_fails.err().unwrap(),
expected_error.err().unwrap(),
"Expected error did not match the resulting error"
);
let disable_disable_cheatcode_lockout_false_fails = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![disable_cheatcode_method.clone()]),
None,
);
let expected_error_disable_self: jsonrpc_core::Result<()> = Err(SurfpoolError::disable_cheatcode(
"Cannot disable surfnet_enableCheatcode or surfnet_disableCheatcode rpc method when lockout is not enabled".to_string(),
)
.into());
assert!(
disable_disable_cheatcode_lockout_false_fails.is_err(),
"Expected surfnet_disableCheatcode to fail when disabling surfnet_disableCheatcode with lockout == false"
);
assert_eq!(
disable_disable_cheatcode_lockout_false_fails.err().unwrap(),
expected_error_disable_self.err().unwrap(),
"Expected error did not match the resulting error"
);
let disable_cheatcode_fails_on_invalid_cheatcode = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![invalid_cheatcode_method.clone()]),
None,
);
let expected_error_disable_invalid_cheatcode: jsonrpc_core::Result<()> =
Err(SurfpoolError::disable_cheatcode("Invalid cheatcode rpc method".to_string()).into());
assert!(
disable_cheatcode_fails_on_invalid_cheatcode.is_err(),
"Expected surfnet_disableCheatcode to fail on providing invalid cheatcode method"
);
assert_eq!(
expected_error_disable_invalid_cheatcode.err().unwrap(),
disable_cheatcode_fails_on_invalid_cheatcode.err().unwrap(),
"Resulting error does not match the expected error"
);
let disable_all_cheatcodes_fails_with_invalid_config = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::All("not_all".to_string()),
None,
);
let expected_error_disable_all_with_invalid_config: jsonrpc_core::Result<()> =
Err(SurfpoolError::disable_cheatcode(
"Invalid option provided for disabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
)
.into());
assert!(
disable_all_cheatcodes_fails_with_invalid_config.is_err(),
"Expected surfnet_disableCheatcode to fail when config is wrong"
);
assert_eq!(
disable_all_cheatcodes_fails_with_invalid_config
.err()
.unwrap(),
expected_error_disable_all_with_invalid_config
.err()
.unwrap(),
"Expected error did not match the resulting error"
);
let disabled_all_cheatcode_passes_result = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::All("all".to_string()),
None,
);
assert!(
disabled_all_cheatcode_passes_result.is_ok(),
"Expected surfnet_disableCheatcode to pass with valid config"
);
assert_eq!(
runloop_context.cheatcode_config.lock().unwrap().filter,
CheatcodeConfig::filter_all_list(false, available_methods.clone()),
"The disabled cheatcodes list doesn't match the expected one"
);
let disable_fails_if_cheatcode_already_disabled_result = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![valid_cheatcode_method.clone()]),
None,
);
let expected_error_if_cheatcode_already_disabled: jsonrpc_core::Result<()> =
Err(SurfpoolError::disable_cheatcode("Cheatcode already disabled".to_string()).into());
assert!(
disable_fails_if_cheatcode_already_disabled_result.is_err(),
"Expected surfnet_disableCheatcode to fail if cheatcode already disabled"
);
assert_eq!(
disable_fails_if_cheatcode_already_disabled_result
.err()
.unwrap(),
expected_error_if_cheatcode_already_disabled.err().unwrap(),
"The expected error does not match the resulting error"
);
let enable_cheatcode_works_properly_result = rpc_server.enable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![valid_cheatcode_method.clone()]),
);
assert!(
enable_cheatcode_works_properly_result.is_ok(),
"expected surfnet_enableCheatcode to pass"
);
assert!(
!runloop_context
.cheatcode_config
.lock()
.unwrap()
.is_cheatcode_disabled(&valid_cheatcode_method),
"Expected the cheatcode to be enabled after surnet_enableCheatcode rpc method"
);
let enable_cheatcode_fails_on_invalid_cheatcode = rpc_server.enable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::List(vec![invalid_cheatcode_method]),
);
let expected_error_enable_invalid_cheatcode: jsonrpc_core::Result<()> =
Err(SurfpoolError::enable_cheatcode("Invalid cheatcode rpc method".to_string()).into());
assert!(
enable_cheatcode_fails_on_invalid_cheatcode.is_err(),
"Expected surfnet_disableCheatcode to fail on providing invalid cheatcode method"
);
assert_eq!(
enable_cheatcode_fails_on_invalid_cheatcode.err().unwrap(),
expected_error_enable_invalid_cheatcode.err().unwrap(),
"Resulting error does not match the expected error"
);
let enable_all_cheatcodes_fails_with_invalid_config = rpc_server.enable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::All("not_all".to_string()),
);
let expected_error_enable_all_with_invalid_config: jsonrpc_core::Result<()> =
Err(SurfpoolError::enable_cheatcode(
"Invalid option provided for enabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
)
.into());
assert!(
enable_all_cheatcodes_fails_with_invalid_config.is_err(),
"Expected surfnet_disableCheatcode to fail when config is wrong"
);
assert_eq!(
enable_all_cheatcodes_fails_with_invalid_config
.err()
.unwrap(),
expected_error_enable_all_with_invalid_config.err().unwrap(),
"Expected error did not match the resulting error"
);
let enable_all_cheatcodes_pass_result = rpc_server.enable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::All("all".to_string()),
);
assert!(
enable_all_cheatcodes_pass_result.is_ok(),
"Expected surnet_enableCheatcode with correct config to pass"
);
assert_eq!(
runloop_context.cheatcode_config.lock().unwrap().filter,
CheatcodeFilter::List(vec![]),
"Expected all the cheatcodes to be enabled"
);
let disable_all_with_lockout_passes = rpc_server.disable_cheatcode(
Some(runloop_context.clone()),
CheatcodeFilter::All("all".to_string()),
Some(CheatcodeControlConfig {
lockout: Some(true),
}),
);
assert!(
disable_all_with_lockout_passes.is_ok(),
"Expected surfnet_disableCheatcode with lockout == true to pass"
);
assert_eq!(
runloop_context.cheatcode_config.lock().unwrap().filter,
CheatcodeConfig::filter_all_list(true, vec![]),
"Expected all the features to be disabled"
);
for ref cheatcode in [enable_cheatcode_method, disable_cheatcode_method] {
assert!(
runloop_context
.cheatcode_config
.lock()
.unwrap()
.is_cheatcode_disabled(cheatcode),
"Expected {} to be disabled",
cheatcode
);
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_get_transaction_profile(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
svm_instance
.airdrop(&payer.pubkey(), lamports_to_send * 2)
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_instance.latest_blockhash();
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message.clone()), &[&payer])
.unwrap();
let tx_bytes = bincode::serialize(&tx).unwrap();
let tx_b64 = base64::engine::general_purpose::STANDARD.encode(&tx_bytes);
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let tag = "test_get_transaction_profile_tag".to_string();
println!("Testing transaction profiling with tag: {}", tag);
let profile_response: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(
Some(runloop_context.clone()),
tx_b64.clone(),
Some(tag.clone()),
None,
)
.await;
assert!(
profile_response.is_ok(),
"Profile transaction failed: {:?}",
profile_response.err()
);
let profile_result = profile_response.unwrap().value;
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Transaction profiling failed"
);
let UuidOrSignature::Uuid(uuid) = profile_result.key else {
panic!(
"Expected a UUID from the profile result, got: {:?}",
profile_result.key
);
};
println!("Generated UUID: {}", uuid);
println!("Testing retrieval by UUID: {}", uuid);
let uuid_response: JsonRpcResult<RpcResponse<Option<UiKeyedProfileResult>>> = rpc_server
.get_transaction_profile(
Some(runloop_context.clone()),
UuidOrSignature::Uuid(uuid),
None,
);
assert!(
uuid_response.is_ok(),
"Get transaction profile by UUID failed: {:?}",
uuid_response.err()
);
let retrieved_profile = uuid_response.unwrap().value;
assert!(
retrieved_profile.is_some(),
"Profile should be found by UUID"
);
let retrieved = retrieved_profile.unwrap();
assert_eq!(
retrieved.transaction_profile.compute_units_consumed,
profile_result.transaction_profile.compute_units_consumed,
"Retrieved profile should match original profile"
);
assert_eq!(
retrieved.transaction_profile.error_message.is_none(),
profile_result.transaction_profile.error_message.is_none(),
"Retrieved profile success should match original"
);
assert_eq!(
retrieved.key,
UuidOrSignature::Uuid(uuid),
"Retrieved profile should have the same UUID"
);
println!("Processing transaction to get signature");
let (status_tx, status_rx) = crossbeam_unbounded();
svm_locker_for_context
.process_transaction(&None, tx.clone(), status_tx, false, true)
.await
.unwrap();
match status_rx.recv() {
Ok(TransactionStatusEvent::Success(_)) => {
println!("Transaction processed successfully");
}
Ok(TransactionStatusEvent::SimulationFailure((error, _))) => {
panic!("Transaction simulation failed: {:?}", error);
}
Ok(TransactionStatusEvent::ExecutionFailure((error, _))) => {
panic!("Transaction execution failed: {:?}", error);
}
Ok(TransactionStatusEvent::VerificationFailure(error)) => {
panic!("Transaction verification failed: {}", error);
}
Err(e) => {
panic!("Failed to receive transaction status: {:?}", e);
}
}
let signature = tx.signatures[0];
println!("Transaction signature: {}", signature);
println!("Testing retrieval by signature: {}", signature);
let signature_response: JsonRpcResult<RpcResponse<Option<UiKeyedProfileResult>>> = rpc_server
.get_transaction_profile(
Some(runloop_context.clone()),
UuidOrSignature::Signature(signature),
None,
);
assert!(
signature_response.is_ok(),
"Get transaction profile by signature failed: {:?}",
signature_response.err()
);
let retrieved_by_signature = signature_response.unwrap().value;
assert!(
retrieved_by_signature.is_some(),
"Profile should be found by signature"
);
let retrieved_sig = retrieved_by_signature.unwrap();
assert!(
retrieved_sig.transaction_profile.error_message.is_none(),
"Retrieved profile by signature should be successful"
);
assert!(
retrieved_sig.transaction_profile.compute_units_consumed > 0,
"Retrieved profile should have consumed compute units"
);
println!("Testing retrieval with non-existent UUID");
let non_existent_uuid = uuid::Uuid::new_v4();
let non_existent_uuid_response: JsonRpcResult<RpcResponse<Option<UiKeyedProfileResult>>> =
rpc_server.get_transaction_profile(
Some(runloop_context.clone()),
UuidOrSignature::Uuid(non_existent_uuid),
None,
);
assert!(
non_existent_uuid_response.is_ok(),
"Get transaction profile with non-existent UUID should not fail"
);
let non_existent_result = non_existent_uuid_response.unwrap().value;
assert!(
non_existent_result.is_none(),
"Non-existent UUID should return None"
);
println!("Testing retrieval with non-existent signature");
let non_existent_signature = solana_signature::Signature::new_unique();
let non_existent_sig_response: JsonRpcResult<RpcResponse<Option<UiKeyedProfileResult>>> =
rpc_server.get_transaction_profile(
Some(runloop_context.clone()),
UuidOrSignature::Signature(non_existent_signature),
None,
);
assert!(
non_existent_sig_response.is_ok(),
"Get transaction profile with non-existent signature should not fail"
);
let non_existent_sig_result = non_existent_sig_response.unwrap().value;
assert!(
non_existent_sig_result.is_none(),
"Non-existent signature should return None"
);
println!("All get_transaction_profile tests passed successfully!");
}
#[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"))]
fn test_register_and_get_idl_without_slot(test_type: TestType) {
let idl: Idl = serde_json::from_slice(include_bytes!("./assets/idl_v1.json")).unwrap();
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let register_response: JsonRpcResult<RpcResponse<()>> =
rpc_server.register_idl(Some(runloop_context.clone()), idl.clone(), None);
assert!(
register_response.is_ok(),
"Register IDL failed: {:?}",
register_response.err()
);
let get_idl_response: JsonRpcResult<RpcResponse<Option<Idl>>> =
rpc_server.get_idl(Some(runloop_context.clone()), idl.address.to_string(), None);
assert!(
get_idl_response.is_ok(),
"Get IDL failed: {:?}",
get_idl_response.err()
);
let retrieved_idl = get_idl_response.unwrap().value;
assert!(retrieved_idl.is_some(), "IDL should be found");
assert_eq!(
retrieved_idl.unwrap(),
idl,
"Retrieved IDL should match registered IDL"
);
println!("All IDL registration and retrieval tests passed successfully!");
}
#[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"))]
fn test_register_and_get_idl_with_slot(test_type: TestType) {
let idl: Idl = serde_json::from_slice(include_bytes!("./assets/idl_v1.json")).unwrap();
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let register_response: JsonRpcResult<RpcResponse<()>> = rpc_server.register_idl(
Some(runloop_context.clone()),
idl.clone(),
Some(Slot::from(
svm_locker_for_context.get_latest_absolute_slot(),
)),
);
assert!(
register_response.is_ok(),
"Register IDL failed: {:?}",
register_response.err()
);
let get_idl_response: JsonRpcResult<RpcResponse<Option<Idl>>> = rpc_server.get_idl(
Some(runloop_context.clone()),
idl.address.to_string(),
Some(Slot::from(
svm_locker_for_context.get_latest_absolute_slot(),
)),
);
assert!(
get_idl_response.is_ok(),
"Get IDL failed: {:?}",
get_idl_response.err()
);
let retrieved_idl = get_idl_response.unwrap().value;
assert!(retrieved_idl.is_some(), "IDL should be found");
assert_eq!(
retrieved_idl.unwrap(),
idl,
"Retrieved IDL should match registered IDL"
);
println!("All IDL registration and retrieval tests passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_register_and_get_same_idl_with_different_slots(test_type: TestType) {
let idl_v1: Idl = serde_json::from_slice(include_bytes!("./assets/idl_v1.json")).unwrap();
let idl_v2: Idl = serde_json::from_slice(include_bytes!("./assets/idl_v2.json")).unwrap();
let idl_v3: Idl = serde_json::from_slice(include_bytes!("./assets/idl_v3.json")).unwrap();
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance);
let current_slot = svm_locker_for_context.get_latest_absolute_slot();
let slot_1 = current_slot.saturating_add(10); let slot_2 = current_slot.saturating_add(50); let slot_3 = current_slot.saturating_add(100);
println!("Current slot: {}", current_slot);
println!("Slot 1 (IDL v1): {}", slot_1);
println!("Slot 2 (IDL v2): {}", slot_2);
println!("Slot 3 (IDL v3): {}", slot_3);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
println!(" [1] Registering IDL v1 at slot: {}", slot_1);
let register_response: JsonRpcResult<RpcResponse<()>> = rpc_server.register_idl(
Some(runloop_context.clone()),
idl_v1.clone(),
Some(Slot::from(slot_1)),
);
assert!(
register_response.is_ok(),
"Register IDL v1 failed at slot {}: {:?}",
slot_1,
register_response.err()
);
println!(" [2] Registering IDL v2 at slot: {}", slot_2);
let register_response: JsonRpcResult<RpcResponse<()>> = rpc_server.register_idl(
Some(runloop_context.clone()),
idl_v2.clone(),
Some(Slot::from(slot_2)),
);
assert!(
register_response.is_ok(),
"Register IDL v2 failed at slot {}: {:?}",
slot_2,
register_response.err()
);
println!(" [3] Registering IDL v3 at slot: {}", slot_3);
let register_response: JsonRpcResult<RpcResponse<()>> = rpc_server.register_idl(
Some(runloop_context.clone()),
idl_v3.clone(),
Some(Slot::from(slot_3)),
);
assert!(
register_response.is_ok(),
"Register IDL v3 failed at slot {}: {:?}",
slot_3,
register_response.err()
);
let test_cases = vec![
(current_slot + 5, "before any registration", None), (slot_1 + 5, "after v1 registration", Some(&idl_v1)), (slot_2 + 5, "after v2 registration", Some(&idl_v2)), (slot_3 + 5, "after v3 registration", Some(&idl_v3)), ];
for (i, (query_slot, description, expected_idl)) in test_cases.iter().enumerate() {
println!(
" [{}] Querying IDL at slot {} ({})",
i + 4,
query_slot,
description
);
let get_idl_response: JsonRpcResult<RpcResponse<Option<Idl>>> = rpc_server.get_idl(
Some(runloop_context.clone()),
idl_v1.address.to_string(),
Some(Slot::from(*query_slot)),
);
assert!(
get_idl_response.is_ok(),
"Get IDL failed at slot {}: {:?}",
query_slot,
get_idl_response.err()
);
let retrieved_idl = get_idl_response.unwrap().value;
match expected_idl {
None => {
assert!(
retrieved_idl.is_none(),
"IDL should not be available when querying at slot {} (before first registration)",
query_slot
);
println!(
" [{}] Correctly: No IDL available at slot {} (before first registration)",
i + 4,
query_slot
);
}
Some(expected) => {
assert!(
retrieved_idl.is_some(),
"IDL should be available when querying at slot {}",
query_slot
);
let retrieved = retrieved_idl.unwrap();
assert_eq!(
retrieved, **expected,
"Retrieved IDL should match expected IDL at slot {}",
query_slot
);
println!(
" [{}] Correctly: Expected IDL retrieved at slot {}",
i + 4,
query_slot
);
}
}
}
println!("All IDL registration and retrieval tests at different slots passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_basic(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
svm_locker
.with_svm_writer(|svm| svm.airdrop(&payer.pubkey(), lamports_to_send * 2))
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
println!("Testing basic transaction profiling");
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
let UuidOrSignature::Uuid(uuid) = profile_result.key else {
panic!(
"Expected a UUID from the profile result, got: {:?}",
profile_result.key
);
};
println!("Generated UUID: {}", uuid);
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Transaction profiling should succeed"
);
assert!(
profile_result.transaction_profile.compute_units_consumed > 0,
"Transaction should consume compute units"
);
assert_eq!(
profile_result.slot,
svm_locker.get_latest_absolute_slot(),
"Profile slot should match current slot"
);
let stored_profile = svm_locker
.get_profile_result(
UuidOrSignature::Uuid(uuid),
&RpcProfileResultConfig::default(),
)
.unwrap();
assert!(stored_profile.is_some(), "Profile should be stored in SVM");
let stored = stored_profile.unwrap();
assert_eq!(
stored.transaction_profile.compute_units_consumed,
profile_result.transaction_profile.compute_units_consumed,
"Stored profile should match returned profile"
);
println!("Basic transaction profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_multi_instruction_basic(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let lamports_to_send = 1_000_000;
svm_locker
.with_svm_writer(|svm| svm.airdrop(&payer.pubkey(), lamports_to_send * 4))
.unwrap()
.unwrap();
let recipient = Pubkey::new_unique();
let recipient2 = Pubkey::new_unique();
let recipient3 = Pubkey::new_unique();
println!("Sender: {}", payer.pubkey());
println!("Recipient 1: {}", recipient);
println!("Recipient 2: {}", recipient2);
println!("Recipient 3: {}", recipient3);
let transfer_ix = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let transfer_ix2 = transfer(&payer.pubkey(), &recipient2, lamports_to_send);
let transfer_ix3 = transfer(&payer.pubkey(), &recipient3, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message = Message::new_with_blockhash(
&[transfer_ix, transfer_ix2, transfer_ix3],
Some(&payer.pubkey()),
&latest_blockhash,
);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
{
let rpc_profile_config = RpcProfileResultConfig {
encoding: Some(UiAccountEncoding::JsonParsed),
depth: Some(RpcProfileDepth::Transaction),
};
let profile_result = svm_locker
.get_profile_result(key, &rpc_profile_config)
.unwrap()
.expect("Profile result should exist");
let UuidOrSignature::Uuid(uuid) = profile_result.key else {
panic!(
"Expected a UUID from the profile result, got: {:?}",
profile_result.key
);
};
println!("Generated UUID: {}", uuid);
assert!(profile_result.transaction_profile.error_message.is_none(),);
assert_eq!(
profile_result.transaction_profile.compute_units_consumed,
450
);
assert_eq!(
profile_result.instruction_profiles, None,
"Instruction profiles should be None for Transaction depth config"
);
let account_states = profile_result.transaction_profile.account_states;
let _ = profile_result
.readonly_account_states
.get(&system_program::id())
.expect("System program should be present in readonly account states");
let UiAccountProfileState::Readonly = account_states
.get(&system_program::id())
.expect("System program state should be present")
else {
panic!("Expected system program state to be Readonly");
};
{
let UiAccountProfileState::Writable(sender_account_change) = account_states
.get(&payer.pubkey())
.expect("Payer account state should be present")
else {
panic!("Expected account state to be Writable");
};
match sender_account_change {
UiAccountChange::Update(before, after) => {
assert_eq!(
after.lamports,
before.lamports - (lamports_to_send * 3) - 5000,
"Payer account should be original balance minus three transfers and fees"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
{
let UiAccountProfileState::Writable(recipient_1_change) = account_states
.get(&recipient)
.expect("Recipient 1 account state should be present")
else {
panic!("Expected recipient 1 account state to be Writable");
};
match recipient_1_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, lamports_to_send,
"Recipient 1 account should have received the transfer amount"
);
}
other => {
panic!("Expected account state to be an Create, got: {:?}", other);
}
}
}
{
let UiAccountProfileState::Writable(recipient_2_change) = account_states
.get(&recipient2)
.expect("Recipient 2 account state should be present")
else {
panic!("Expected recipient 2 account state to be Writable");
};
match recipient_2_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, lamports_to_send,
"Recipient 2 account should have received the transfer amount"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
{
let UiAccountProfileState::Writable(recipient_3_change) = account_states
.get(&recipient3)
.expect("Recipient 3 account state should be present")
else {
panic!("Expected recipient 3 account state to be Writable");
};
match recipient_3_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, lamports_to_send,
"Recipient 3 account should have received the transfer amount"
);
}
other => {
panic!("Expected account state to be Create, got: {:?}", other);
}
}
}
}
{
let rpc_profile_config = RpcProfileResultConfig {
encoding: Some(UiAccountEncoding::JsonParsed),
depth: Some(RpcProfileDepth::Instruction),
};
let profile_result = svm_locker
.get_profile_result(key, &rpc_profile_config)
.unwrap()
.expect("Profile result should exist");
println!(
"Profile result with Instruction depth: {}",
serde_json::to_string_pretty(&profile_result).unwrap()
);
let instruction_profiles = profile_result
.instruction_profiles
.expect("Instruction profiles should be present for Instruction depth config");
{
let ix_profile = instruction_profiles
.get(0)
.expect("Instruction profile should exist");
assert_eq!(
ix_profile.compute_units_consumed, 150,
"Instruction should consume 150 CU"
);
assert!(ix_profile.error_message.is_none());
let account_states = &ix_profile.account_states;
{
let UiAccountProfileState::Writable(sender_account_change) = account_states
.get(&payer.pubkey())
.expect("Payer account state should be present")
else {
panic!("Expected account state to be Writable");
};
match sender_account_change {
UiAccountChange::Update(before, after) => {
assert_eq!(
after.lamports,
before.lamports - lamports_to_send - 5000,
"Payer account should be original balance minus transfer amount"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
{
let UiAccountProfileState::Writable(recipient_1_change) = account_states
.get(&recipient)
.expect("Recipient 1 account state should be present")
else {
panic!("Expected recipient 1 account state to be Writable");
};
match recipient_1_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, lamports_to_send,
"Recipient 1 account should have received the transfer amount"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
assert!(
account_states.get(&recipient2).is_none(),
"Recipient 2 should not be affected by first instruction"
);
assert!(
account_states.get(&recipient3).is_none(),
"Recipient 3 should not be affected by first instruction"
);
}
{
let ix_profile = instruction_profiles
.get(1)
.expect("Instruction profile should exist");
assert_eq!(
ix_profile.compute_units_consumed, 150,
"Instruction should consume 150 CU"
);
assert!(ix_profile.error_message.is_none());
let account_states = &ix_profile.account_states;
{
let UiAccountProfileState::Writable(sender_account_change) = account_states
.get(&payer.pubkey())
.expect("Payer account state should be present")
else {
panic!("Expected account state to be Writable");
};
match sender_account_change {
UiAccountChange::Update(before, after) => {
assert_eq!(after.lamports, before.lamports - lamports_to_send);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
println!(
"Recipient 1 account state: {:?}",
account_states.get(&recipient)
);
assert!(
account_states.get(&recipient).is_none(),
"Recipient 1 should not be affected by second instruction"
);
{
let UiAccountProfileState::Writable(recipient_2_change) = account_states
.get(&recipient2)
.expect("Recipient 2 account state should be present")
else {
panic!("Expected recipient 2 account state to be Writable");
};
match recipient_2_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, lamports_to_send,
"Recipient 2 account should have received the transfer amount"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
assert!(
account_states.get(&recipient3).is_none(),
"Recipient 3 should not be affected by first instruction"
);
}
{
let ix_profile = instruction_profiles
.get(2)
.expect("Instruction profile should exist");
assert_eq!(
ix_profile.compute_units_consumed, 150,
"Instruction should consume 150 CU"
);
assert!(ix_profile.error_message.is_none());
let account_states = &ix_profile.account_states;
{
let UiAccountProfileState::Writable(sender_account_change) = account_states
.get(&payer.pubkey())
.expect("Payer account state should be present")
else {
panic!("Expected account state to be Writable");
};
match sender_account_change {
UiAccountChange::Update(before, after) => {
assert_eq!(after.lamports, before.lamports - lamports_to_send,);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
assert!(
account_states.get(&recipient).is_none(),
"Recipient 1 should not be affected by second instruction"
);
assert!(
account_states.get(&recipient2).is_none(),
"Recipient 2 should not be affected by first instruction"
);
{
let UiAccountProfileState::Writable(recipient_3_change) = account_states
.get(&recipient3)
.expect("Recipient 3 account state should be present")
else {
panic!("Expected recipient 3 account state to be Writable");
};
match recipient_3_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, lamports_to_send,
"Recipient 3 account should have received the transfer amount"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
}
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_with_tag(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
svm_locker
.with_svm_writer(|svm| svm.airdrop(&payer.pubkey(), lamports_to_send * 3))
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let tag = "test_profile_transaction_tag".to_string();
println!("Testing transaction profiling with tag: {}", tag);
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), Some(tag.clone()))
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Transaction profiling should succeed"
);
let tagged_results = svm_locker
.get_profile_results_by_tag(tag.clone(), &RpcProfileResultConfig::default())
.unwrap();
assert!(tagged_results.is_some(), "Tagged results should be found");
let tagged_profiles = tagged_results.unwrap();
assert_eq!(
tagged_profiles.len(),
1,
"Should have exactly one profile for this tag"
);
let tagged_profile = &tagged_profiles[0];
assert_eq!(
tagged_profile.key,
UuidOrSignature::Uuid(profile_uuid),
"Tagged profile should have the same UUID"
);
println!("Testing multiple profiles with the same tag");
let recipient2 = Pubkey::new_unique();
let instruction2 = transfer(&payer.pubkey(), &recipient2, lamports_to_send);
let message2 =
Message::new_with_blockhash(&[instruction2], Some(&payer.pubkey()), &latest_blockhash);
let transaction2 =
VersionedTransaction::try_new(VersionedMessage::Legacy(message2), &[&payer]).unwrap();
let uuid2 = svm_locker
.profile_transaction(&None, transaction2.clone(), Some(tag.clone()))
.await
.unwrap()
.inner;
let tagged_results_updated = svm_locker
.get_profile_results_by_tag(tag.clone(), &RpcProfileResultConfig::default())
.unwrap();
assert!(
tagged_results_updated.is_some(),
"Tagged results should still be found after adding second profile"
);
let tagged_profiles_updated = tagged_results_updated.unwrap();
assert_eq!(
tagged_profiles_updated.len(),
2,
"Should have exactly two profiles for this tag"
);
let uuids: Vec<Uuid> = tagged_profiles_updated
.iter()
.filter_map(|profile| {
if let UuidOrSignature::Uuid(uuid) = profile.key {
Some(uuid)
} else {
None
}
})
.collect();
assert!(
uuids.contains(&profile_uuid),
"First UUID should be in tagged results"
);
assert!(
uuids.contains(&uuid2),
"Second UUID should be in tagged results"
);
let non_existent_tag = "non_existent_tag".to_string();
let non_existent_results = svm_locker
.get_profile_results_by_tag(non_existent_tag, &RpcProfileResultConfig::default())
.unwrap();
assert!(
non_existent_results.is_none(),
"Non-existent tag should return None"
);
let stored_profile1 = svm_locker
.get_profile_result(
UuidOrSignature::Uuid(profile_uuid),
&RpcProfileResultConfig::default(),
)
.unwrap();
assert!(
stored_profile1.is_some(),
"First profile should be retrievable by UUID"
);
let stored_profile2 = svm_locker
.get_profile_result(
UuidOrSignature::Uuid(uuid2),
&RpcProfileResultConfig::default(),
)
.unwrap();
assert!(
stored_profile2.is_some(),
"Second profile should be retrievable by UUID"
);
println!("Tag-based transaction profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_token_transfer(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let mint = Keypair::new();
let lamports_to_send = 2 * LAMPORTS_PER_SOL;
println!("Sender: {}", payer.pubkey());
println!("Recipient: {}", recipient);
println!("Mint: {}", mint.pubkey());
svm_locker
.airdrop(&payer.pubkey(), lamports_to_send)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
println!("Recent blockhash after airdrop: {}", recent_blockhash);
let mint_rent = 1461600;
let create_account_ix = system_instruction::create_account(
&payer.pubkey(),
&mint.pubkey(),
mint_rent,
82,
&spl_token_2022_interface::id(),
);
let initialize_mint_ix = spl_token_2022_interface::instruction::initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
2, )
.unwrap();
let source_ata = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&payer.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
println!("Source ATA: {}", source_ata);
let _dest_ata = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&recipient,
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_source_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let _create_dest_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&payer.pubkey(),
&recipient,
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let mint_amount = 100_00; let _mint_to_ix = spl_token_2022_interface::instruction::mint_to(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&source_ata,
&payer.pubkey(),
&[&payer.pubkey()],
mint_amount,
)
.unwrap();
let setup_message = Message::new_with_blockhash(
&[
create_account_ix,
initialize_mint_ix,
create_source_ata_ix,
],
Some(&payer.pubkey()),
&recent_blockhash,
);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_message), &[&payer, &mint])
.unwrap();
{
let profile_result = svm_locker
.profile_transaction(&None, setup_tx.clone(), None)
.await
.unwrap();
let rpc_profile_config = RpcProfileResultConfig {
encoding: Some(UiAccountEncoding::JsonParsed),
depth: Some(RpcProfileDepth::Instruction),
};
let key = UuidOrSignature::Uuid(profile_result.inner);
let ui_profile_result = svm_locker
.get_profile_result(key, &rpc_profile_config)
.unwrap()
.expect("Profile result should exist");
assert!(
ui_profile_result
.transaction_profile
.error_message
.is_none(),
"Setup transaction should succeed, found error: {}",
ui_profile_result.transaction_profile.error_message.unwrap()
);
{
let ix_profile = ui_profile_result
.instruction_profiles
.as_ref()
.unwrap()
.get(0)
.expect("instruction profile should exist");
assert!(
ix_profile.error_message.is_none(),
"Profile should succeed, found error: {}",
ix_profile.error_message.as_ref().unwrap()
);
assert_eq!(ix_profile.compute_units_consumed, 150);
assert!(ix_profile.error_message.is_none());
let account_states = &ix_profile.account_states;
let UiAccountProfileState::Writable(sender_account_change) = account_states
.get(&payer.pubkey())
.expect("Payer account state should be present")
else {
panic!("Expected account state to be Writable");
};
match sender_account_change {
UiAccountChange::Update(before, after) => {
assert_eq!(
after.lamports,
before.lamports - mint_rent - (2 * 5000), "Payer account should be original balance minus rent"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
let UiAccountProfileState::Writable(mint_account_change) = account_states
.get(&mint.pubkey())
.expect("Mint account state should be present")
else {
panic!("Expected mint account state to be Writable");
};
match mint_account_change {
UiAccountChange::Create(mint_account) => {
assert_eq!(
mint_account.lamports, mint_rent,
"Mint account should have the correct rent amount"
);
assert_eq!(
mint_account.owner,
spl_token_2022_interface::id().to_string(),
"Mint account should be owned by the SPL Token program"
);
assert_eq!(
mint_account.data,
UiAccountData::Binary(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".into(),
UiAccountEncoding::Base64
),
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
{
let ix_profile = ui_profile_result
.instruction_profiles
.as_ref()
.unwrap()
.get(1)
.expect("instruction profile should exist");
assert!(
ix_profile.error_message.is_none(),
"Profile should succeed, found error: {}",
ix_profile.error_message.as_ref().unwrap()
);
assert_eq!(ix_profile.compute_units_consumed, 1230);
assert!(ix_profile.error_message.is_none());
let account_states = &ix_profile.account_states;
assert!(account_states.get(&payer.pubkey()).is_none());
let UiAccountProfileState::Writable(mint_account_change) = account_states
.get(&mint.pubkey())
.expect("Mint account state should be present")
else {
panic!("Expected mint account state to be Writable");
};
match mint_account_change {
UiAccountChange::Update(_before, after) => {
assert_eq!(
after.lamports, mint_rent,
"Mint account should have the correct rent amount"
);
assert_eq!(
after.owner,
spl_token_2022_interface::id().to_string(),
"Mint account should be owned by the SPL Token program"
);
assert_eq!(
after.data,
UiAccountData::Json(ParsedAccount {
program: "spl-token-2022".to_string(),
parsed: json!({
"info": {
"decimals": 2,
"freezeAuthority": payer.pubkey().to_string(),
"mintAuthority": payer.pubkey().to_string(),
"isInitialized": true,
"supply": "0",
},
"type": "mint"
}),
space: 82,
}),
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
}
{
let ix_profile = ui_profile_result
.instruction_profiles
.as_ref()
.unwrap()
.get(2)
.expect("instruction profile should exist");
assert!(
ix_profile.error_message.is_none(),
"Profile should succeed, found error: {}",
ix_profile.error_message.as_ref().unwrap()
);
assert!(ix_profile.error_message.is_none());
let account_states = &ix_profile.account_states;
let UiAccountProfileState::Writable(sender_account_change) = account_states
.get(&payer.pubkey())
.expect("Payer account state should be present")
else {
panic!("Expected account state to be Writable");
};
match sender_account_change {
UiAccountChange::Update(before, after) => {
assert_eq!(
after.lamports,
before.lamports - 2074080,
"Payer account should be original balance minus rent"
);
}
other => {
panic!("Expected account state to be an Update, got: {:?}", other);
}
}
let UiAccountProfileState::Writable(mint_account_change) = account_states
.get(&mint.pubkey())
.expect("Mint account state should be present")
else {
panic!("Expected mint account state to be Writable");
};
match mint_account_change {
UiAccountChange::Unchanged(mint_account) => {
assert!(mint_account.is_some())
}
other => {
panic!("Expected account state to be Unchanged, got: {:?}", other);
}
}
let UiAccountProfileState::Writable(source_ata_change) = account_states
.get(&source_ata)
.expect("account state should be present")
else {
panic!("Expected account state to be Writable");
};
match source_ata_change {
UiAccountChange::Create(new) => {
assert_eq!(
new.lamports, 2074080,
"Source ATA should have the correct lamports after creation"
);
assert_eq!(
new.owner,
spl_token_2022_interface::id().to_string(),
"Source ATA should be owned by the SPL Token program"
);
let UiAccountData::Binary(new_data, UiAccountEncoding::Base64) = &new.data
else {
panic!("Expected account data to be Base64 encoded");
};
let mut mint_bs64 =
base64::engine::general_purpose::STANDARD.encode(&mint.pubkey().to_bytes());
mint_bs64.truncate(42);
assert!(new_data.starts_with(mint_bs64.as_str()));
assert!(
new_data.ends_with(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcAAAA="
)
);
}
other => {
panic!("Expected account state to be Create, got: {:?}", other);
}
}
}
assert_eq!(
ui_profile_result.transaction_profile.compute_units_consumed,
ui_profile_result
.instruction_profiles
.as_ref()
.unwrap()
.iter()
.map(|ix| ix.compute_units_consumed)
.sum::<u64>(),
);
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_insufficient_funds(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let insufficient_funds = LAMPORTS_PER_SOL;
let large_transfer = LAMPORTS_PER_SOL * 10;
svm_locker
.airdrop(&payer.pubkey(), insufficient_funds)
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, large_transfer);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let ui_profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
assert!(
ui_profile_result
.transaction_profile
.error_message
.is_some(),
"Transaction should fail due to insufficient funds"
);
let error_msg = ui_profile_result
.transaction_profile
.error_message
.as_ref()
.unwrap();
assert!(
error_msg.eq("Error processing Instruction 0: custom program error: 0x1"),
"Error message should indicate insufficient funds, got: {}",
error_msg
);
assert!(
ui_profile_result.transaction_profile.compute_units_consumed > 0,
"Failed transaction should still consume compute units"
);
println!("Insufficient funds profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_multi_instruction_failure(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let payer2 = Keypair::new();
let recipient1 = Pubkey::new_unique();
let lamports_to_send = LAMPORTS_PER_SOL;
svm_locker
.airdrop(&payer.pubkey(), lamports_to_send * 3)
.unwrap()
.unwrap();
let valid_instruction = transfer(&payer.pubkey(), &recipient1, lamports_to_send);
let invalid_instruction = transfer(&payer2.pubkey(), &recipient1, lamports_to_send * 2);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message = Message::new_with_blockhash(
&[valid_instruction, invalid_instruction],
Some(&payer.pubkey()),
&latest_blockhash,
);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer, &payer2])
.unwrap();
let uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
assert!(
profile_result.transaction_profile.error_message.is_some(),
"Multi-instruction transaction should fail"
);
assert!(
profile_result.instruction_profiles.is_some(),
"Instruction profiles should be generated for multi-instruction transaction"
);
let instruction_profiles = profile_result.instruction_profiles.as_ref().unwrap();
assert_eq!(instruction_profiles.len(), 2,);
let first_instruction_profile = &instruction_profiles[0];
assert!(
first_instruction_profile.error_message.is_none(),
"First instruction should succeed"
);
assert!(
first_instruction_profile.compute_units_consumed > 0,
"First instruction should consume compute units"
);
let second_profile = &instruction_profiles[1];
assert!(
second_profile.error_message.is_some(),
"Second instruction should fail due to insufficient funds"
);
println!("Multi-instruction failure profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_with_encoding(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = LAMPORTS_PER_SOL;
svm_locker
.with_svm_writer(|svm| svm.airdrop(&payer.pubkey(), lamports_to_send * 2))
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Transaction profiling should succeed with base64 encoding"
);
println!("Encoding profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_with_tag_and_retrieval(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = LAMPORTS_PER_SOL;
svm_locker
.with_svm_writer(|svm| svm.airdrop(&payer.pubkey(), lamports_to_send * 3))
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let tag = "test_tag_retrieval".to_string();
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), Some(tag.clone()))
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Transaction profiling should succeed"
);
let retrieved_by_uuid = svm_locker
.get_profile_result(
UuidOrSignature::Uuid(profile_uuid),
&RpcProfileResultConfig::default(),
)
.unwrap();
assert!(
retrieved_by_uuid.is_some(),
"Profile should be retrievable by UUID"
);
let retrieved = retrieved_by_uuid.unwrap();
assert_eq!(
retrieved.transaction_profile.compute_units_consumed,
profile_result.transaction_profile.compute_units_consumed,
"Retrieved profile should match original profile"
);
let retrieved_by_tag = svm_locker
.get_profile_results_by_tag(tag.clone(), &RpcProfileResultConfig::default())
.unwrap();
assert!(
retrieved_by_tag.is_some(),
"Profile should be retrievable by tag"
);
let tagged_profiles = retrieved_by_tag.unwrap();
assert_eq!(
tagged_profiles.len(),
1,
"Should have exactly one profile for this tag"
);
let tagged_profile = &tagged_profiles[0];
assert_eq!(
tagged_profile.key,
UuidOrSignature::Uuid(profile_uuid),
"Tagged profile should have the same UUID"
);
let non_existent_tag = "non_existent_tag".to_string();
let non_existent_result = svm_locker
.get_profile_results_by_tag(non_existent_tag, &RpcProfileResultConfig::default())
.unwrap();
assert!(
non_existent_result.is_none(),
"Non-existent tag should return None"
);
println!("Tag and retrieval profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_empty_instruction(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let lamports_to_send = LAMPORTS_PER_SOL;
svm_locker
.airdrop(&payer.pubkey(), lamports_to_send)
.unwrap()
.unwrap();
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message = Message::new_with_blockhash(
&[], Some(&payer.pubkey()),
&latest_blockhash,
);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
println!("profile result: {:#?}", profile_result);
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Empty transaction profiling should succeed"
);
assert_eq!(profile_result.transaction_profile.compute_units_consumed, 0);
assert!(
profile_result.instruction_profiles.is_none(),
"Empty transaction should not have instruction profiles"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_versioned_message(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = LAMPORTS_PER_SOL;
svm_locker
.airdrop(&payer.pubkey(), 2 * lamports_to_send)
.unwrap()
.unwrap();
svm_locker.confirm_current_block(&None).await.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let v0_message = v0::Message::try_compile(
&payer.pubkey(),
&[instruction],
&[], latest_blockhash,
)
.expect("Failed to compile v0 message");
let transaction =
VersionedTransaction::try_new(VersionedMessage::V0(v0_message), &[&payer]).unwrap();
let profile_uuid = svm_locker
.profile_transaction(&None, transaction.clone(), None)
.await
.unwrap()
.inner;
let key = UuidOrSignature::Uuid(profile_uuid);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist");
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Versioned transaction profiling should succeed, found: {:?}",
profile_result.transaction_profile.error_message.unwrap()
);
assert!(
profile_result.transaction_profile.compute_units_consumed > 0,
"Versioned transaction should consume compute units"
);
println!("Versioned message profiling test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_get_local_signatures_without_limit(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance.clone());
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let payer = Keypair::new();
let recipient = Keypair::new();
let lamports_to_send = 1_000_000;
svm_locker_for_context
.airdrop(&payer.pubkey(), lamports_to_send * 2)
.unwrap()
.unwrap();
svm_locker_for_context
.confirm_current_block(&None)
.await
.unwrap();
let create_account_instruction = system_instruction::create_account(
&payer.pubkey(),
&recipient.pubkey(),
lamports_to_send / 2,
0,
&solana_sdk_ids::system_program::id(),
);
let create_account_tx = Transaction::new_signed_with_payer(
&[create_account_instruction],
Some(&payer.pubkey()),
&[&payer, &recipient],
svm_locker_for_context.with_svm_reader(|svm| svm.latest_blockhash()),
);
let (create_status_tx, _create_status_rx) = crossbeam_channel::bounded(1);
svm_locker_for_context
.process_transaction(
&None,
create_account_tx.into(),
create_status_tx,
false,
true,
)
.await
.unwrap();
svm_locker_for_context
.confirm_current_block(&None)
.await
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient.pubkey(), lamports_to_send);
let latest_blockhash = svm_locker_for_context.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message.clone()), &[&payer])
.unwrap();
let (status_tx, _status_rx) = crossbeam_channel::bounded(1);
svm_locker_for_context
.process_transaction(&None, tx.clone(), status_tx, false, true)
.await
.unwrap();
svm_locker_for_context
.confirm_current_block(&None)
.await
.unwrap();
let get_local_signatures_response: JsonRpcResult<RpcResponse<Vec<RpcLogsResponse>>> =
rpc_server
.get_local_signatures(Some(runloop_context.clone()), None)
.await;
assert!(
get_local_signatures_response.is_ok(),
"Get local signatures failed: {:?}",
get_local_signatures_response.err()
);
let local_signatures = get_local_signatures_response.unwrap().value;
assert!(local_signatures.len() > 0);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_get_local_signatures_with_limit(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker_for_context = SurfnetSvmLocker::new(svm_instance.clone());
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker_for_context.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let payer = Keypair::new();
let _recipient = Keypair::new();
let lamports_to_send = 10_000_000;
svm_locker_for_context
.airdrop(&payer.pubkey(), lamports_to_send * 10)
.unwrap()
.unwrap();
svm_locker_for_context
.confirm_current_block(&None)
.await
.unwrap();
let initial_signatures_response: JsonRpcResult<RpcResponse<Vec<RpcLogsResponse>>> = rpc_server
.get_local_signatures(Some(runloop_context.clone()), None)
.await;
let initial_count = initial_signatures_response.unwrap().value.len();
let num_transactions = 10;
let mut transaction_signatures = Vec::new();
for _i in 0..num_transactions {
let unique_recipient = Keypair::new();
let instruction = transfer(
&payer.pubkey(),
&unique_recipient.pubkey(),
lamports_to_send / num_transactions as u64,
);
let latest_blockhash = svm_locker_for_context.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(message.clone()), &[&payer])
.unwrap();
let (status_tx, _status_rx) = crossbeam_channel::bounded(1);
svm_locker_for_context
.process_transaction(&None, tx.clone(), status_tx, false, true)
.await
.unwrap();
transaction_signatures.push(tx.signatures[0]);
svm_locker_for_context
.confirm_current_block(&None)
.await
.unwrap();
}
let test_limits = vec![1, 3, 5, 10, 15];
for limit in test_limits {
let get_local_signatures_response: JsonRpcResult<RpcResponse<Vec<RpcLogsResponse>>> =
rpc_server
.get_local_signatures(Some(runloop_context.clone()), Some(limit))
.await;
assert!(
get_local_signatures_response.is_ok(),
"Get local signatures with limit {} failed: {:?}",
limit,
get_local_signatures_response.err()
);
let local_signatures = get_local_signatures_response.unwrap().value;
assert!(
local_signatures.len() <= limit as usize,
"Expected at most {} signatures, but got {}",
limit,
local_signatures.len()
);
let total_expected_signatures = initial_count + num_transactions;
let expected_count = std::cmp::min(limit as usize, total_expected_signatures);
assert!(
local_signatures.len() == expected_count,
"Expected {} signatures with limit {}, but got {} (initial: {}, new: {})",
expected_count,
limit,
local_signatures.len(),
initial_count,
num_transactions
);
}
let get_local_signatures_response: JsonRpcResult<RpcResponse<Vec<RpcLogsResponse>>> =
rpc_server
.get_local_signatures(Some(runloop_context.clone()), Some(0))
.await;
assert!(
get_local_signatures_response.is_ok(),
"Get local signatures with limit 0 failed: {:?}",
get_local_signatures_response.err()
);
let local_signatures = get_local_signatures_response.unwrap().value;
assert!(
local_signatures.is_empty(),
"Expected empty list with limit 0, but got {} signatures",
local_signatures.len()
);
println!("All local signatures tests passed successfully!");
}
fn boot_simnet(
block_production_mode: BlockProductionMode,
slot_time: Option<u64>,
test_type: TestType,
) -> (
SurfnetSvmLocker,
crossbeam_channel::Sender<SimnetCommand>,
crossbeam_channel::Receiver<SimnetEvent>,
) {
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: slot_time.unwrap_or(DEFAULT_SLOT_TIME_MS),
block_production_mode,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
gossip_port: 0,
tpu_port: 0,
tpu_quic_port: 0,
},
subgraph: SubgraphConfig::default(),
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
let svm_locker_cc: SurfnetSvmLocker = svm_locker.clone();
let simnet_commands_tx_cc = simnet_commands_tx.clone();
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
svm_locker_cc,
config,
simnet_commands_tx_cc,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
loop {
if let Ok(SimnetEvent::Ready(_)) =
simnet_events_rx.recv_timeout(Duration::from_millis(1000))
{
break;
}
}
(svm_locker, simnet_commands_tx, simnet_events_rx)
}
#[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"))]
fn test_time_travel_resume_paused_clock(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_locker, simnet_cmd_tx, _) =
boot_simnet(BlockProductionMode::Clock, Some(100), test_type);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let initial_slot = svm_locker.get_latest_absolute_slot();
std::thread::sleep(Duration::from_millis(500));
let new_slot = svm_locker.get_latest_absolute_slot();
assert!(
new_slot > initial_slot,
"Slot should change when clock is not paused"
);
let _ = rpc_server.pause_clock(Some(runloop_context.clone()));
std::thread::sleep(Duration::from_millis(100));
let slot_after_pause = svm_locker.get_latest_absolute_slot();
std::thread::sleep(Duration::from_millis(500));
let slot_after_pause_and_500ms = svm_locker.get_latest_absolute_slot();
assert!(
slot_after_pause == slot_after_pause_and_500ms,
"Slot should change when clock is not paused"
);
std::thread::sleep(Duration::from_millis(2000));
let slot_after_pause_and_2500ms = svm_locker.get_latest_absolute_slot();
assert!(
slot_after_pause == slot_after_pause_and_2500ms,
"Slot should change when clock is not paused"
);
let resume_response: JsonRpcResult<EpochInfo> =
rpc_server.resume_clock(Some(runloop_context.clone()));
let slot_after_resume = svm_locker.get_latest_absolute_slot();
assert!(
resume_response.is_ok(),
"Resume clock failed: {:?}",
resume_response.err()
);
std::thread::sleep(Duration::from_millis(500));
let slot_after_resume_and_500ms = svm_locker.get_latest_absolute_slot();
assert!(
slot_after_resume < slot_after_resume_and_500ms,
"Slot should change when clock is not paused"
);
println!("Resume clock test passed successfully!");
}
#[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"))]
fn test_time_travel_absolute_timestamp(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let slot_time = 100;
let (svm_locker, simnet_cmd_tx, simnet_events_rx) = boot_simnet(
BlockProductionMode::Clock,
Some(slot_time.clone()),
test_type,
);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx.clone(),
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let clock = Clock {
slot: 1,
epoch_start_timestamp: 1,
epoch: 1,
leader_schedule_epoch: 100,
unix_timestamp: 100,
};
simnet_cmd_tx
.send(SimnetCommand::UpdateInternalClock(None, clock))
.unwrap();
let Ok(SimnetEvent::SystemClockUpdated(_clock_updated)) =
simnet_events_rx.recv_timeout(Duration::from_millis(5000))
else {
panic!("failed to update internal clock")
};
let initial_epoch_info = svm_locker.get_epoch_info();
println!("Initial epoch info: {:?}", initial_epoch_info);
let seven_days = 7 * 24 * 60 * 60 * 1000;
let target_timestamp = svm_locker.0.blocking_read().updated_at + seven_days;
let time_travel_response: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteTimestamp(target_timestamp)),
);
assert!(
time_travel_response.is_ok(),
"Time travel to absolute timestamp failed: {:?}",
time_travel_response.err()
);
let new_epoch_info = time_travel_response.unwrap();
println!("Response: {:?}", new_epoch_info);
assert_ne!(
new_epoch_info.epoch, initial_epoch_info.epoch,
"Epoch should change after time travel"
);
assert_ne!(
new_epoch_info.absolute_slot, initial_epoch_info.absolute_slot,
"Slot should change after time travel"
);
let current_epoch_info = svm_locker.get_epoch_info();
println!("Updated epoch info: {:?}", current_epoch_info);
assert_eq!(current_epoch_info.epoch, new_epoch_info.epoch);
assert_eq!(
current_epoch_info.absolute_slot,
new_epoch_info.absolute_slot
);
println!("Time travel to absolute timestamp test passed successfully!");
}
#[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"))]
fn test_time_travel_absolute_slot(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_locker, simnet_cmd_tx, simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx.clone(),
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let clock = Clock {
slot: 1,
epoch_start_timestamp: 1,
epoch: 1,
leader_schedule_epoch: 100,
unix_timestamp: 100,
};
simnet_cmd_tx
.send(SimnetCommand::UpdateInternalClock(None, clock))
.unwrap();
let Ok(SimnetEvent::SystemClockUpdated(_clock_updated)) =
simnet_events_rx.recv_timeout(Duration::from_millis(5000))
else {
panic!("failed to update internal clock")
};
let initial_epoch_info = svm_locker.get_epoch_info();
let target_slot = initial_epoch_info.absolute_slot + 1000000;
let time_travel_response: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteSlot(target_slot)),
);
assert!(
time_travel_response.is_ok(),
"Time travel to absolute slot failed: {:?}",
time_travel_response.err()
);
let new_epoch_info = time_travel_response.unwrap();
assert_eq!(
new_epoch_info.absolute_slot, target_slot,
"Slot should match target slot"
);
assert!(
new_epoch_info.epoch > initial_epoch_info.epoch,
"Epoch should change after time travel"
);
assert!(
new_epoch_info.absolute_slot > initial_epoch_info.absolute_slot,
"Epoch should change after time travel"
);
let current_epoch_info = svm_locker.get_epoch_info();
assert_eq!(current_epoch_info.absolute_slot, target_slot);
assert_eq!(current_epoch_info.epoch, new_epoch_info.epoch);
println!("Time travel to absolute slot test passed successfully!");
}
#[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"))]
fn test_time_travel_absolute_epoch(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_locker, simnet_cmd_tx, simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx.clone(),
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let clock = Clock {
slot: 1,
epoch_start_timestamp: 1,
epoch: 1,
leader_schedule_epoch: 100,
unix_timestamp: 100,
};
simnet_cmd_tx
.send(SimnetCommand::UpdateInternalClock(None, clock))
.unwrap();
let Ok(SimnetEvent::SystemClockUpdated(_clock_updated)) =
simnet_events_rx.recv_timeout(Duration::from_millis(5000))
else {
panic!("failed to update internal clock")
};
let initial_epoch_info = svm_locker.get_epoch_info();
let target_epoch = initial_epoch_info.epoch + 100;
let time_travel_response: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteEpoch(target_epoch)),
);
assert!(
time_travel_response.is_ok(),
"Time travel to absolute epoch failed: {:?}",
time_travel_response.err()
);
let new_epoch_info = time_travel_response.unwrap();
assert_eq!(
new_epoch_info.epoch, target_epoch,
"Epoch should match target epoch"
);
assert_ne!(
new_epoch_info.epoch, initial_epoch_info.epoch,
"Epoch should change after time travel"
);
assert_ne!(
new_epoch_info.absolute_slot, initial_epoch_info.absolute_slot,
"Slot should change after time travel"
);
let current_epoch_info = svm_locker.get_epoch_info();
assert_eq!(current_epoch_info.epoch, target_epoch);
assert_eq!(
current_epoch_info.absolute_slot,
new_epoch_info.absolute_slot
);
println!("Time travel to absolute epoch test passed successfully!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ix_profiling_with_alt_tx(test_type: TestType) {
let (svm_locker, _simnet_cmd_tx, _simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let p1 = Keypair::new();
let p2 = Keypair::new();
svm_locker
.airdrop(&p1.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
svm_locker
.airdrop(&p2.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let mint = Keypair::new();
let mint_rent = 1461600;
let create_mint_ix = system_instruction::create_account(
&p1.pubkey(),
&mint.pubkey(),
mint_rent,
82,
&spl_token_2022_interface::id(),
);
let initialize_mint_ix = spl_token_2022_interface::instruction::initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&p1.pubkey(),
Some(&p1.pubkey()),
2,
)
.unwrap();
let at1 = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&p1.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let at2 = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&p2.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_at1_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&p1.pubkey(),
&p1.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_at2_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&p1.pubkey(),
&p2.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let mint_amount = 100_00;
let mint_to_ix = spl_token_2022_interface::instruction::mint_to(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&at1,
&p1.pubkey(),
&[&p1.pubkey()],
mint_amount,
)
.unwrap();
let setup_instructions = vec![
create_mint_ix,
initialize_mint_ix,
create_at1_ix,
create_at2_ix,
mint_to_ix,
];
let setup_message =
Message::new_with_blockhash(&setup_instructions, Some(&p1.pubkey()), &recent_blockhash);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_message), &[&p1, &mint])
.unwrap();
let status_tx = crossbeam_unbounded::<TransactionStatusEvent>().0;
svm_locker
.process_transaction(&None, setup_tx, status_tx, false, true)
.await
.unwrap();
let alt_key = Pubkey::new_unique();
let address_lookup_table_account = AddressLookupTableAccount {
key: alt_key,
addresses: vec![at1, at2, spl_token_2022_interface::id(), mint.pubkey()],
};
println!(
"addresses present on the ALT: {:?}",
address_lookup_table_account.addresses
);
let alt_account_data = AddressLookupTable {
meta: LookupTableMeta {
authority: Some(p1.pubkey()),
..Default::default()
},
addresses: address_lookup_table_account.addresses.clone().into(),
};
svm_locker.with_svm_writer(|svm| {
let alt_data = alt_account_data.serialize_for_tests().unwrap();
let alt_account = solana_account::Account {
lamports: 1000000,
data: alt_data,
owner: solana_address_lookup_table_interface::program::id(),
executable: false,
rent_epoch: 0,
};
svm.set_account(&alt_key, alt_account).unwrap();
});
let transfer_amount = 50_00;
let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
&spl_token_2022_interface::id(),
&at1,
&mint.pubkey(),
&at2,
&p1.pubkey(),
&[&p1.pubkey()],
transfer_amount,
2,
)
.unwrap();
let alt_message = v0::Message::try_compile(
&p1.pubkey(),
&[transfer_ix],
&[address_lookup_table_account.clone()],
recent_blockhash,
)
.expect("Failed to compile ALT message");
let alt_tx = VersionedTransaction::try_new(VersionedMessage::V0(alt_message.clone()), &[&p1])
.expect("Failed to create ALT transaction");
let binding = svm_locker
.profile_transaction(&None, alt_tx.clone(), None)
.await
.unwrap();
let profile_result_uuid = binding.inner();
let profile_result = svm_locker
.get_profile_result(
UuidOrSignature::Uuid(*profile_result_uuid),
&RpcProfileResultConfig::default(),
)
.unwrap()
.unwrap();
let ix_profiles = profile_result
.instruction_profiles
.as_ref()
.expect("instruction profiles should exist");
assert_eq!(
ix_profiles.len(),
1,
"ALT transfer should have one instruction"
);
let ix_profile = ix_profiles.get(0).unwrap();
assert!(
ix_profile.error_message.is_none(),
"Profile should succeed, found error: {:?}",
ix_profile.error_message.as_ref().unwrap()
);
assert!(
ix_profile.account_states.get(&alt_key).is_none(),
"ALT account should not be in instruction account states"
);
assert!(
profile_result
.readonly_account_states
.get(&alt_key)
.is_none(),
"ALT account should not be in readonly account states"
);
let table = alt_message
.address_table_lookups
.first()
.expect("ALT lookups should exist");
let expected_loaded_writable: Vec<Pubkey> = table
.writable_indexes
.iter()
.map(|&i| address_lookup_table_account.addresses[i as usize])
.collect();
let expected_loaded_readonly: Vec<Pubkey> = table
.readonly_indexes
.iter()
.map(|&i| address_lookup_table_account.addresses[i as usize])
.collect();
for pk in &expected_loaded_writable {
match ix_profile
.account_states
.get(pk)
.expect("loaded writable address must be present")
{
UiAccountProfileState::Writable(_) => {}
other => panic!(
"expected Writable for loaded writable address {}, got {:?}",
pk, other
),
}
}
for pk in &expected_loaded_readonly {
match ix_profile
.account_states
.get(pk)
.expect("loaded readonly address must be present")
{
UiAccountProfileState::Readonly => {}
other => panic!(
"expected Readonly for loaded readonly address {}, got {:?}",
pk, other
),
}
}
let account_states = ix_profile.account_states.clone();
let UiAccountProfileState::Readonly = account_states
.get(&spl_token_2022_interface::id())
.expect("token-2022 program should be present")
else {
panic!("expected token-2022 program to be Readonly");
};
let UiAccountProfileState::Readonly = account_states
.get(&mint.pubkey())
.expect("mint should be present")
else {
panic!("expected mint to be Readonly");
};
let UiAccountProfileState::Writable(src_change) = account_states
.get(&at1)
.expect("source ATA should be present")
else {
panic!("expected source ATA to be Writable");
};
match src_change {
UiAccountChange::Update(before, after) => {
assert_ne!(
before.data, after.data,
"token data should change on source"
);
}
_ => panic!("expected Update for source ATA"),
}
let UiAccountProfileState::Writable(dst_change) = account_states
.get(&at2)
.expect("destination ATA should be present")
else {
panic!("expected destination ATA to be Writable");
};
match dst_change {
UiAccountChange::Update(before, after) => {
assert_ne!(
before.data, after.data,
"token data should change on destination"
);
}
_ => panic!("expected Update for destination ATA"),
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn it_should_delete_accounts_with_no_lamports(test_type: TestType) {
let (svm_locker, _simnet_cmd_tx, _simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let p1 = Keypair::new();
let p2 = Keypair::new();
svm_locker
.airdrop(&p1.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message = Message::new_with_blockhash(
&[system_instruction::transfer(
&p1.pubkey(),
&p2.pubkey(),
LAMPORTS_PER_SOL - 5000,
)],
Some(&p1.pubkey()),
&recent_blockhash,
);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&p1]).unwrap();
let (status_tx, rx) = unbounded();
let _ = svm_locker
.process_transaction(&None, tx, status_tx, true, false)
.await
.unwrap();
#[allow(clippy::never_loop)]
loop {
match rx.recv() {
Ok(status) => {
println!("Transaction status: {:?}", status);
break;
}
Err(_) => panic!("status channel closed unexpectedly"),
}
}
assert!(
svm_locker.get_account_local(&p1.pubkey()).inner.is_none(),
"Account should be deleted"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_compute_budget_profiling(test_type: TestType) {
let (svm_locker, _simnet_cmd_tx, _simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let p1 = Keypair::new();
let p2 = Keypair::new();
svm_locker
.airdrop(&p1.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message = Message::new_with_blockhash(
&[
ComputeBudgetInstruction::set_compute_unit_limit(1_000_000),
ComputeBudgetInstruction::set_compute_unit_price(1),
system_instruction::transfer(&p1.pubkey(), &p2.pubkey(), LAMPORTS_PER_SOL),
],
Some(&p1.pubkey()),
&recent_blockhash,
);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&p1]).unwrap();
let uuid = svm_locker
.profile_transaction(&None, tx, None)
.await
.unwrap()
.inner;
let profile_result = svm_locker
.get_profile_result(
UuidOrSignature::Uuid(uuid),
&RpcProfileResultConfig::default(),
)
.unwrap()
.unwrap();
let ix_profile = profile_result.instruction_profiles.unwrap();
assert_eq!(ix_profile.len(), 3, "Should have 3 instruction profiles");
let ix = &ix_profile[0];
assert!(
ix.error_message.is_none(),
"Expected no error for instruction, found {}",
ix.error_message.as_ref().unwrap()
);
assert_eq!(ix.compute_units_consumed, 150);
let ix = &ix_profile[1];
assert!(
ix.error_message.is_none(),
"Expected no error for instruction, found {}",
ix.error_message.as_ref().unwrap()
);
assert_eq!(ix.compute_units_consumed, 150);
let ix = &ix_profile[1];
assert!(
ix.error_message.is_none(),
"Expected no error for instruction, found {}",
ix.error_message.as_ref().unwrap()
);
assert_eq!(ix.compute_units_consumed, 150);
}
#[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"))]
fn test_reset_account(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let p1 = Keypair::new();
println!("P1 pubkey: {}", p1.pubkey());
svm_locker
.airdrop(&p1.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap(); println!("Airdropped SOL to p1");
println!(
"Account before reset: {:?}",
svm_locker.get_account_local(&p1.pubkey()).inner
);
svm_locker.reset_account(p1.pubkey(), false).unwrap();
println!("Reset account");
println!(
"Account deleted: {:?}",
svm_locker.get_account_local(&p1.pubkey()).inner
);
assert!(
svm_locker.get_account_local(&p1.pubkey()).inner.is_none(),
"Account should be deleted"
);
}
#[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"))]
fn test_reset_account_cascade(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let owner = Pubkey::new_unique();
let owned = Pubkey::new_unique();
let owner_account = Account {
lamports: 10 * LAMPORTS_PER_SOL,
data: vec![0x01, 0x02],
owner: solana_sdk_ids::system_program::id(),
executable: false,
rent_epoch: 0,
};
let owned_account = Account {
lamports: 5 * LAMPORTS_PER_SOL,
data: vec![0x03, 0x04],
owner, executable: false,
rent_epoch: 0,
};
svm_locker
.with_svm_writer(|svm_writer| {
svm_writer.set_account(&owner, owner_account).unwrap();
svm_writer.set_account(&owned, owned_account).unwrap();
Ok::<(), SurfpoolError>(())
})
.unwrap();
assert!(!svm_locker.get_account_local(&owner).inner.is_none());
assert!(!svm_locker.get_account_local(&owned).inner.is_none());
svm_locker.reset_account(owner, true).unwrap();
assert!(svm_locker.get_account_local(&owner).inner.is_none());
assert!(svm_locker.get_account_local(&owned).inner.is_none());
svm_locker.reset_account(owned, false).unwrap();
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_reset_streamed_account(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let p1 = Keypair::new();
println!("P1 pubkey: {}", p1.pubkey());
svm_locker
.airdrop(&p1.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap(); println!("Airdropped SOL to p1");
let _ = svm_locker.confirm_current_block(&None).await;
assert!(!svm_locker.get_account_local(&p1.pubkey()).inner.is_none());
svm_locker.stream_account(p1.pubkey(), false).unwrap();
let _ = svm_locker.confirm_current_block(&None).await;
assert!(
svm_locker.get_account_local(&p1.pubkey()).inner.is_none(),
"Streamed account should be deleted"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_reset_streamed_account_cascade(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let owner = Pubkey::new_unique();
let owned = Pubkey::new_unique();
let owner_account = Account {
lamports: 10 * LAMPORTS_PER_SOL,
data: vec![0x01, 0x02],
owner: solana_sdk_ids::system_program::id(),
executable: false,
rent_epoch: 0,
};
let owned_account = Account {
lamports: 5 * LAMPORTS_PER_SOL,
data: vec![0x03, 0x04],
owner, executable: false,
rent_epoch: 0,
};
svm_locker
.with_svm_writer(|svm_writer| {
svm_writer.set_account(&owner, owner_account).unwrap();
svm_writer.set_account(&owned, owned_account).unwrap();
Ok::<(), SurfpoolError>(())
})
.unwrap();
assert!(!svm_locker.get_account_local(&owner).inner.is_none());
assert!(!svm_locker.get_account_local(&owned).inner.is_none());
let _ = svm_locker.confirm_current_block(&None).await;
assert!(!svm_locker.get_account_local(&owner).inner.is_none());
assert!(!svm_locker.get_account_local(&owned).inner.is_none());
svm_locker.stream_account(owner, true).unwrap();
let _ = svm_locker.confirm_current_block(&None).await;
assert!(svm_locker.get_account_local(&owner).inner.is_none());
assert!(svm_locker.get_account_local(&owned).inner.is_none());
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_stream_accounts_multiple(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let p1 = Keypair::new();
let p2 = Keypair::new();
let p3 = Keypair::new();
for kp in [&p1, &p2, &p3] {
svm_locker
.airdrop(&kp.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
}
let _ = svm_locker.confirm_current_block(&None).await;
assert!(!svm_locker.get_account_local(&p1.pubkey()).inner.is_none());
assert!(!svm_locker.get_account_local(&p2.pubkey()).inner.is_none());
assert!(!svm_locker.get_account_local(&p3.pubkey()).inner.is_none());
for kp in [&p1, &p2, &p3] {
svm_locker.stream_account(kp.pubkey(), false).unwrap();
}
let _ = svm_locker.confirm_current_block(&None).await;
assert!(
svm_locker.get_account_local(&p1.pubkey()).inner.is_none(),
"First streamed account should be deleted"
);
assert!(
svm_locker.get_account_local(&p2.pubkey()).inner.is_none(),
"Second streamed account should be deleted"
);
assert!(
svm_locker.get_account_local(&p3.pubkey()).inner.is_none(),
"Third streamed account should be deleted"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_stream_accounts_with_cascade(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let owner1 = Pubkey::new_unique();
let owned1 = Pubkey::new_unique();
let owner2 = Pubkey::new_unique();
let owned2 = Pubkey::new_unique();
svm_locker
.with_svm_writer(|svm_writer| {
svm_writer
.set_account(
&owner1,
Account {
lamports: 10 * LAMPORTS_PER_SOL,
data: vec![0x01],
owner: solana_sdk_ids::system_program::id(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();
svm_writer
.set_account(
&owned1,
Account {
lamports: 5 * LAMPORTS_PER_SOL,
data: vec![0x02],
owner: owner1,
executable: false,
rent_epoch: 0,
},
)
.unwrap();
svm_writer
.set_account(
&owner2,
Account {
lamports: 10 * LAMPORTS_PER_SOL,
data: vec![0x03],
owner: solana_sdk_ids::system_program::id(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();
svm_writer
.set_account(
&owned2,
Account {
lamports: 5 * LAMPORTS_PER_SOL,
data: vec![0x04],
owner: owner2,
executable: false,
rent_epoch: 0,
},
)
.unwrap();
Ok::<(), SurfpoolError>(())
})
.unwrap();
assert!(!svm_locker.get_account_local(&owner1).inner.is_none());
assert!(!svm_locker.get_account_local(&owned1).inner.is_none());
assert!(!svm_locker.get_account_local(&owner2).inner.is_none());
assert!(!svm_locker.get_account_local(&owned2).inner.is_none());
let _ = svm_locker.confirm_current_block(&None).await;
svm_locker.stream_account(owner1, true).unwrap();
svm_locker.stream_account(owner2, true).unwrap();
let _ = svm_locker.confirm_current_block(&None).await;
assert!(
svm_locker.get_account_local(&owner1).inner.is_none(),
"First owner should be deleted"
);
assert!(
svm_locker.get_account_local(&owned1).inner.is_none(),
"First owned account should be deleted"
);
assert!(
svm_locker.get_account_local(&owner2).inner.is_none(),
"Second owner should be deleted"
);
assert!(
svm_locker.get_account_local(&owned2).inner.is_none(),
"Second owned account should be deleted"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_stream_accounts_empty(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let p1 = Keypair::new();
svm_locker
.airdrop(&p1.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let _ = svm_locker.confirm_current_block(&None).await;
assert!(!svm_locker.get_account_local(&p1.pubkey()).inner.is_none());
let _ = svm_locker.confirm_current_block(&None).await;
assert!(
!svm_locker.get_account_local(&p1.pubkey()).inner.is_none(),
"Existing account should not be affected when no accounts are streamed"
);
}
#[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"))]
fn test_reset_network(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let owner = Pubkey::new_unique();
let owned = Pubkey::new_unique();
let owner_account = Account {
lamports: 10 * LAMPORTS_PER_SOL,
data: vec![0x01, 0x02],
owner: solana_sdk_ids::system_program::id(),
executable: false,
rent_epoch: 0,
};
let owned_account = Account {
lamports: 5 * LAMPORTS_PER_SOL,
data: vec![0x03, 0x04],
owner, executable: false,
rent_epoch: 0,
};
svm_locker
.with_svm_writer(|svm_writer| {
svm_writer.set_account(&owner, owner_account).unwrap();
svm_writer.set_account(&owned, owned_account).unwrap();
Ok::<(), SurfpoolError>(())
})
.unwrap();
assert!(!svm_locker.get_account_local(&owner).inner.is_none());
assert!(!svm_locker.get_account_local(&owned).inner.is_none());
hiro_system_kit::nestable_block_on(svm_locker.reset_network(&None)).unwrap();
assert!(svm_locker.get_account_local(&owner).inner.is_none());
assert!(svm_locker.get_account_local(&owned).inner.is_none());
svm_locker.reset_account(owned, false).unwrap();
}
#[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"))]
fn test_reset_network_keeps_latest_blockhash_valid(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
hiro_system_kit::nestable_block_on(svm_locker.reset_network(&None)).unwrap();
let epoch_schedule =
svm_locker.with_svm_reader(|svm_reader| svm_reader.inner.get_sysvar::<EpochSchedule>());
let (genesis_slot, latest_slot) = svm_locker.with_svm_reader(|svm_reader| {
(
svm_reader.genesis_slot,
svm_reader.latest_epoch_info.absolute_slot,
)
});
assert_eq!(genesis_slot, FINALIZATION_SLOT_THRESHOLD);
assert_eq!(latest_slot, FINALIZATION_SLOT_THRESHOLD);
assert_eq!(epoch_schedule, EpochSchedule::without_warmup());
let latest_blockhash = svm_locker.with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
let is_recent = svm_locker
.with_svm_reader(|svm_reader| svm_reader.check_blockhash_is_recent(&latest_blockhash));
assert!(
is_recent,
"latest_blockhash returned after reset should be present in RecentBlockhashes sysvar",
);
}
#[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"))]
fn test_reset_network_time_travel_timestamp(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_locker, simnet_cmd_tx, simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let seven_days = 7 * 24 * 60 * 60 * 1000;
let target_timestamp = svm_locker.0.blocking_read().updated_at + seven_days;
let time_travel_response: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteTimestamp(target_timestamp)),
);
assert!(
time_travel_response.is_ok(),
"First time travel should succeed"
);
let reset_response: JsonRpcResult<RpcResponse<()>> =
hiro_system_kit::nestable_block_on(rpc_server.reset_network(Some(runloop_context.clone())));
assert!(reset_response.is_ok(), "Reset network should succeed");
let time_travel_response2: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteTimestamp(target_timestamp)),
);
assert!(
time_travel_response2.is_ok(),
"Second time travel should succeed after reset. Error: {:?}",
time_travel_response2.err()
);
}
#[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"))]
fn test_reset_network_time_travel_slot(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_locker, simnet_cmd_tx, simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let initial_reset: JsonRpcResult<RpcResponse<()>> =
hiro_system_kit::nestable_block_on(rpc_server.reset_network(Some(runloop_context.clone())));
assert!(initial_reset.is_ok(), "Initial reset should succeed");
let target_slot = 1000;
let time_travel_response: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteSlot(target_slot)),
);
assert!(
time_travel_response.is_ok(),
"First time travel should succeed"
);
let reset_response: JsonRpcResult<RpcResponse<()>> =
hiro_system_kit::nestable_block_on(rpc_server.reset_network(Some(runloop_context.clone())));
assert!(reset_response.is_ok(), "Reset network should succeed");
let time_travel_response2: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteSlot(target_slot)),
);
assert!(
time_travel_response2.is_ok(),
"Second time travel should succeed after reset. Error: {:?}",
time_travel_response2.err()
);
}
#[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"))]
fn test_reset_network_time_travel_epoch(test_type: TestType) {
let rpc_server = SurfnetCheatcodesRpc::empty();
let (svm_locker, simnet_cmd_tx, simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
let initial_reset: JsonRpcResult<RpcResponse<()>> =
hiro_system_kit::nestable_block_on(rpc_server.reset_network(Some(runloop_context.clone())));
assert!(initial_reset.is_ok(), "Initial reset should succeed");
let target_epoch = 5;
let time_travel_response: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteEpoch(target_epoch)),
);
assert!(
time_travel_response.is_ok(),
"First time travel should succeed"
);
let reset_response: JsonRpcResult<RpcResponse<()>> =
hiro_system_kit::nestable_block_on(rpc_server.reset_network(Some(runloop_context.clone())));
assert!(reset_response.is_ok(), "Reset network should succeed");
let time_travel_response2: JsonRpcResult<EpochInfo> = rpc_server.time_travel(
Some(runloop_context.clone()),
Some(TimeTravelConfig::AbsoluteEpoch(target_epoch)),
);
assert!(
time_travel_response2.is_ok(),
"Second time travel should succeed after reset. Error: {:?}",
time_travel_response2.err()
);
}
fn start_surfnet(
airdrop_addresses: Vec<Pubkey>,
datasource_rpc_url: Option<String>,
test_type: TestType,
) -> Result<(String, SurfnetSvmLocker), String> {
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let offline_mode = datasource_rpc_url.is_none();
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
airdrop_addresses,
airdrop_token_amount: LAMPORTS_PER_SOL,
offline_mode,
remote_rpc_url: datasource_rpc_url,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
let svm_locker_cc: SurfnetSvmLocker = svm_locker.clone();
let _handle = std::thread::spawn(move || {
let future = start_local_surfnet_runloop(
svm_locker_cc,
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
let mut ready = false;
let mut connected = offline_mode;
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::Ready(_)) => {
ready = true;
}
Ok(SimnetEvent::Connected(_)) => {
connected = true;
}
_ => (),
}
if ready && connected {
break;
}
}
Ok((format!("http://{}:{}", bind_host, bind_port), svm_locker))
}
#[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_closed_accounts(test_type: TestType) {
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
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(),
},
};
let (datasource_surfnet_url, _datasource_svm_locker) =
start_surfnet(vec![pubkey], None, test_type).expect("Failed to start datasource surfnet");
println!("Datasource surfnet started at {}", datasource_surfnet_url);
let (surfnet_url, surfnet_svm_locker) =
start_surfnet(vec![], Some(datasource_surfnet_url), another_test_type)
.expect("Failed to start surfnet");
println!("Surfnet started at {}", surfnet_url);
let rpc_client = RpcClient::new(surfnet_url);
{
let account = rpc_client
.get_account(&pubkey)
.await
.expect("Failed to get account from surfnet");
assert_eq!(
account.lamports, LAMPORTS_PER_SOL,
"Account lamports should match airdrop amount"
);
println!("Account fetched successfully from datasource surfnet");
}
{
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_resetAccount",
},
json!([pubkey.to_string()]),
)
.await
.expect("Failed to reset account");
assert!(
surfnet_svm_locker
.get_account_local(&pubkey)
.inner
.is_none(),
"Account should be deleted locally after reset_account"
);
let account = rpc_client
.get_account(&pubkey)
.await
.expect("Failed to get account from surfnet after reset_account");
assert_eq!(
account.lamports, LAMPORTS_PER_SOL,
"Account should be re-fetched from remote after reset_account"
);
println!("Account re-fetched successfully from datasource surfnet after reset_account");
}
{
let recent_blockhash = rpc_client
.get_latest_blockhash()
.await
.expect("Failed to get latest blockhash");
let recipient = Pubkey::new_unique();
let close_ix = system_instruction::transfer(&pubkey, &recipient, LAMPORTS_PER_SOL - 5000);
println!(
"Sending {} from {} to {}",
LAMPORTS_PER_SOL - 5000,
pubkey,
recipient
);
let message = Message::new_with_blockhash(&[close_ix], Some(&pubkey), &recent_blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&keypair])
.expect("Failed to create transaction");
let _sig = rpc_client
.send_and_confirm_transaction(&tx)
.await
.expect("Failed to send and confirm transaction");
let tx = rpc_client
.get_transaction(
&_sig,
solana_transaction_status::UiTransactionEncoding::JsonParsed,
)
.await
.expect("Failed to get transaction");
println!("Transaction details: {:?}", tx);
let _ = rpc_client
.get_signature_status_with_commitment(&_sig, CommitmentConfig::finalized())
.await
.expect("Failed to get transaction status")
.expect("Transaction status not found")
.unwrap();
assert!(
surfnet_svm_locker
.get_account_local(&pubkey)
.inner
.is_none(),
"Account should be deleted locally after being closed"
);
println!("Fetching account after being closed...");
let account_result = rpc_client.get_account(&pubkey).await;
assert!(
account_result.is_err(),
"Account should not be re-fetched from remote after being closed; found {:?}",
account_result.unwrap()
);
println!("Account successfully closed and not re-fetched from datasource surfnet");
}
}
#[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_offline_account_including_owned_accounts(test_type: TestType) {
let owner = Pubkey::new_unique();
let owned = 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(),
},
};
let (datasource_surfnet_url, datasource_svm_locker) =
start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet");
datasource_svm_locker
.with_svm_writer(|svm_writer| {
svm_writer
.set_account(
&owner,
Account {
lamports: LAMPORTS_PER_SOL,
data: vec![1, 2, 3],
owner: system_program::id(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();
svm_writer
.set_account(
&owned,
Account {
lamports: LAMPORTS_PER_SOL / 2,
data: vec![4, 5, 6],
owner,
executable: false,
rent_epoch: 0,
},
)
.unwrap();
Ok::<(), SurfpoolError>(())
})
.expect("Failed to seed datasource accounts");
let (surfnet_url, surfnet_svm_locker) =
start_surfnet(vec![], Some(datasource_surfnet_url), another_test_type)
.expect("Failed to start surfnet");
let rpc_client = RpcClient::new(surfnet_url);
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_offlineAccount",
},
serde_json::json!([owner.to_string(), { "includeOwnedAccounts": true }]),
)
.await
.expect("Failed to set account offline");
assert!(
surfnet_svm_locker.is_account_offline(&owner),
"Owner should be recorded as offline"
);
assert!(
surfnet_svm_locker
.get_offline_account_owners()
.contains(&owner),
"Owner should be recorded as a offline account owner"
);
let owner_result = rpc_client.get_account(&owner).await;
assert!(
owner_result.is_err(),
"Offline owner account should not be fetched from remote"
);
assert!(
surfnet_svm_locker.get_account_local(&owner).inner.is_none(),
"Offline owner account should remain absent locally"
);
let owned_result = rpc_client.get_account(&owned).await;
assert!(
owned_result.is_err(),
"Owned account should not be fetched from remote once marked offline"
);
assert!(
surfnet_svm_locker.get_account_local(&owned).inner.is_none(),
"Offline owned account should remain absent locally"
);
}
#[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_offline_account_without_owned_accounts(test_type: TestType) {
let owner = Pubkey::new_unique();
let owned = 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(),
},
};
let (datasource_surfnet_url, datasource_svm_locker) =
start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet");
datasource_svm_locker
.with_svm_writer(|svm_writer| {
svm_writer
.set_account(
&owner,
Account {
lamports: LAMPORTS_PER_SOL,
data: vec![1, 2, 3],
owner: system_program::id(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();
svm_writer
.set_account(
&owned,
Account {
lamports: LAMPORTS_PER_SOL / 2,
data: vec![4, 5, 6],
owner,
executable: false,
rent_epoch: 0,
},
)
.unwrap();
Ok::<(), SurfpoolError>(())
})
.expect("Failed to seed datasource accounts");
let (surfnet_url, surfnet_svm_locker) =
start_surfnet(vec![], Some(datasource_surfnet_url), another_test_type)
.expect("Failed to start surfnet");
let rpc_client = RpcClient::new(surfnet_url);
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_offlineAccount",
},
serde_json::json!([owner.to_string()]),
)
.await
.expect("Failed to set account offline");
assert!(
surfnet_svm_locker.is_account_offline(&owner),
"Owner should be recorded as offline"
);
assert!(
!surfnet_svm_locker
.get_offline_account_owners()
.contains(&owner),
"Owner should NOT be in offline account owners (includeOwnedAccounts=false)"
);
let owner_result = rpc_client.get_account(&owner).await;
assert!(
owner_result.is_err(),
"Offline owner account should not be fetched from remote"
);
let owned_result = rpc_client.get_account(&owned).await;
assert!(
owned_result.is_ok(),
"Owned account should still be fetchable when owner blocking is not active"
);
}
#[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_reset_account_after_offline_restores(test_type: TestType) {
let account = 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(),
},
};
let (datasource_surfnet_url, datasource_svm_locker) =
start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet");
datasource_svm_locker
.with_svm_writer(|svm_writer| {
svm_writer
.set_account(
&account,
Account {
lamports: LAMPORTS_PER_SOL,
data: vec![1, 2, 3],
owner: system_program::id(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();
Ok::<(), SurfpoolError>(())
})
.expect("Failed to seed datasource accounts");
let (surfnet_url, surfnet_svm_locker) =
start_surfnet(vec![], Some(datasource_surfnet_url), another_test_type)
.expect("Failed to start surfnet");
let rpc_client = RpcClient::new(surfnet_url);
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_offlineAccount",
},
serde_json::json!([account.to_string()]),
)
.await
.expect("Failed to set account offline");
assert!(
surfnet_svm_locker.is_account_offline(&account),
"Account should be offline"
);
let result = rpc_client.get_account(&account).await;
assert!(
result.is_err(),
"Offline account should not be fetched from remote"
);
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_resetAccount",
},
serde_json::json!([account.to_string()]),
)
.await
.expect("Failed to reset account");
assert!(
!surfnet_svm_locker.is_account_offline(&account),
"Account should be online after reset"
);
let result = rpc_client.get_account(&account).await;
assert!(
result.is_ok(),
"Account should be fetchable from remote after reset"
);
}
#[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_reset_network_clears_offline_accounts(test_type: TestType) {
let account = 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(),
},
};
let (datasource_surfnet_url, datasource_svm_locker) =
start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet");
datasource_svm_locker
.with_svm_writer(|svm_writer| {
svm_writer
.set_account(
&account,
Account {
lamports: LAMPORTS_PER_SOL,
data: vec![1, 2, 3],
owner: system_program::id(),
executable: false,
rent_epoch: 0,
},
)
.unwrap();
Ok::<(), SurfpoolError>(())
})
.expect("Failed to seed datasource accounts");
let (surfnet_url, surfnet_svm_locker) =
start_surfnet(vec![], Some(datasource_surfnet_url), another_test_type)
.expect("Failed to start surfnet");
let rpc_client = RpcClient::new(surfnet_url);
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_offlineAccount",
},
serde_json::json!([account.to_string()]),
)
.await
.expect("Failed to set account offline");
assert!(
surfnet_svm_locker.is_account_offline(&account),
"Account should be offline"
);
let _: serde_json::Value = rpc_client
.send(
solana_client::rpc_request::RpcRequest::Custom {
method: "surfnet_resetNetwork",
},
serde_json::json!([]),
)
.await
.expect("Failed to reset network");
assert!(
!surfnet_svm_locker.is_account_offline(&account),
"Account should be online after network reset"
);
let result = rpc_client.get_account(&account).await;
assert!(
result.is_ok(),
"Account should be fetchable from remote after network reset"
);
}
#[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(),
},
};
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);
datasource_svm_locker
.write_program(program_pubkey, None, 0, &[1, 2, 3], &None)
.await
.expect("Failed to write program account");
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);
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(),
},
};
let (datasource_url, datasource_svm_locker) =
start_surfnet(vec![], None, test_type).expect("Failed to start datasource surfnet");
datasource_svm_locker
.write_program(program_pubkey, None, 0, &[1, 2, 3], &None)
.await
.expect("Failed to write program account");
datasource_svm_locker
.airdrop(&plain_pubkey, LAMPORTS_PER_SOL)
.expect("Failed to airdrop to plain account")
.unwrap();
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);
let accounts = rpc_client
.get_multiple_accounts(&[program_pubkey, plain_pubkey])
.await
.expect("Failed to get multiple accounts");
assert_eq!(accounts.len(), 2);
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)"
);
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"
);
}
#[test_case(SignatureSubscriptionType::processed() ; "processed commitment")]
#[test_case(SignatureSubscriptionType::received() ; "received commitment")]
#[test_case(SignatureSubscriptionType::confirmed() ; "confirmed commitment")]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_signature_subscribe(subscription_type: SignatureSubscriptionType) {
use crossbeam_channel::unbounded;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, lamports_to_send);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let signature = tx.signatures[0];
let notification_rx =
svm_locker.subscribe_for_signature_updates(&signature, subscription_type.clone());
let (status_tx, _status_rx) = unbounded();
let result = svm_locker
.process_transaction(&None, VersionedTransaction::from(tx), status_tx, true, true)
.await;
assert!(
result.is_ok(),
"Transaction should be processed successfully"
);
match subscription_type {
SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed) => {
svm_locker.confirm_current_block(&None).await.unwrap();
}
_ => {}
}
let notification = notification_rx.recv_timeout(Duration::from_secs(5));
assert!(
notification.is_ok(),
"Should receive {} notification",
subscription_type
);
let (slot, error_opt) = notification.unwrap();
assert!(
error_opt.is_none(),
"Transaction should succeed without error"
);
println!(
"✓ Received {} signature notification at slot {}",
subscription_type, slot
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_signature_subscribe_failed_transaction(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_system_interface::instruction as system_instruction;
use crate::surfnet::SignatureSubscriptionType;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix =
system_instruction::transfer(&payer.pubkey(), &recipient, LAMPORTS_PER_SOL * 2); let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let signature = tx.signatures[0];
let subscription_type = SignatureSubscriptionType::processed();
let notification_rx = svm_locker.subscribe_for_signature_updates(&signature, subscription_type);
let (status_tx, _status_rx) = unbounded();
let _ = svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await;
let notification = notification_rx.recv_timeout(Duration::from_secs(5));
assert!(
notification.is_ok(),
"Should receive notification for failed transaction"
);
let (slot, error_opt) = notification.unwrap();
assert!(error_opt.is_some(), "Failed transaction should have error");
println!(
"✓ Received signature notification for failed transaction at slot {} with error: {:?}",
slot, error_opt
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_signature_subscribe_multiple_subscribers(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_system_interface::instruction as system_instruction;
use crate::surfnet::SignatureSubscriptionType;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let signature = tx.signatures[0];
let notification_rx1 = svm_locker
.subscribe_for_signature_updates(&signature, SignatureSubscriptionType::processed());
let notification_rx2 = svm_locker
.subscribe_for_signature_updates(&signature, SignatureSubscriptionType::processed());
let notification_rx3 = svm_locker
.subscribe_for_signature_updates(&signature, SignatureSubscriptionType::confirmed());
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
assert!(
notification_rx1
.recv_timeout(Duration::from_secs(5))
.is_ok(),
"Subscriber 1 should receive notification"
);
assert!(
notification_rx2
.recv_timeout(Duration::from_secs(5))
.is_ok(),
"Subscriber 2 should receive notification"
);
svm_locker.confirm_current_block(&None).await.unwrap();
assert!(
notification_rx3
.recv_timeout(Duration::from_secs(5))
.is_ok(),
"Confirmed subscriber should receive notification"
);
println!("✓ Multiple subscribers all received notifications correctly");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_signature_subscribe_before_transaction_exists(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_system_interface::instruction as system_instruction;
use crate::surfnet::SignatureSubscriptionType;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let signature = tx.signatures[0];
let subscription_type = SignatureSubscriptionType::processed();
let notification_rx = svm_locker.subscribe_for_signature_updates(&signature, subscription_type);
tokio::time::sleep(Duration::from_millis(100)).await;
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let notification = notification_rx.recv_timeout(Duration::from_secs(5));
assert!(
notification.is_ok(),
"Should receive notification even when subscribed before transaction"
);
let (slot, error_opt) = notification.unwrap();
assert!(error_opt.is_none(), "Transaction should succeed");
println!(
"✓ Subscription before transaction works correctly at slot {}",
slot
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_account_subscribe_balance_change(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let account_rx =
svm_locker.subscribe_for_account_updates(&payer.pubkey(), Some(UiAccountEncoding::Base58));
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let account_update = account_rx.recv_timeout(Duration::from_secs(5));
assert!(account_update.is_ok(), "Should receive account update");
let updated_account = account_update.unwrap();
assert_eq!(
updated_account.lamports,
LAMPORTS_PER_SOL - 1_000_000 - 5000, "Account balance should be updated"
);
println!(
"✓ Received account update notification with new balance {}",
updated_account.lamports
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_account_subscribe_multiple_changes(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_account_decoder::UiAccountEncoding;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), 10 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let account_rx =
svm_locker.subscribe_for_account_updates(&payer.pubkey(), Some(UiAccountEncoding::Base58));
for i in 0..3 {
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let account_update = account_rx.recv_timeout(Duration::from_secs(5));
assert!(
account_update.is_ok(),
"Should receive account update for transaction {}",
i + 1
);
println!(
"✓ Received account update notification for transaction {}",
i + 1
);
if i < 2 {
svm_locker.confirm_current_block(&None).await.unwrap();
}
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_account_subscribe_multiple_subscribers(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_account_decoder::UiAccountEncoding;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let sender = Keypair::new();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
svm_locker
.airdrop(&sender.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let account_rx1 =
svm_locker.subscribe_for_account_updates(&payer.pubkey(), Some(UiAccountEncoding::Base64));
let account_rx2 =
svm_locker.subscribe_for_account_updates(&payer.pubkey(), Some(UiAccountEncoding::Base58));
let account_rx3 = svm_locker.subscribe_for_account_updates(&payer.pubkey(), None);
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&sender.pubkey(), &payer.pubkey(), 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&sender.pubkey()),
&[&sender],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
assert!(
account_rx1.recv_timeout(Duration::from_secs(5)).is_ok(),
"Subscriber 1 should receive notification"
);
assert!(
account_rx2.recv_timeout(Duration::from_secs(5)).is_ok(),
"Subscriber 2 should receive notification"
);
assert!(
account_rx3.recv_timeout(Duration::from_secs(5)).is_ok(),
"Subscriber 3 should receive notification"
);
println!("✓ All 3 subscribers received notifications for account change");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_account_subscribe_new_account_creation(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_account_decoder::UiAccountEncoding;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let new_account = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let account_rx =
svm_locker.subscribe_for_account_updates(&new_account, Some(UiAccountEncoding::Base64));
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &new_account, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let account_update = account_rx.recv_timeout(Duration::from_secs(5));
assert!(
account_update.is_ok(),
"Should receive notification when account is created"
);
let created_account = account_update.unwrap();
assert_eq!(
created_account.lamports, 1_000_000,
"New account should have correct balance"
);
println!(
"✓ Received account update notification for new account creation with balance {}",
created_account.lamports
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_account_subscribe_account_closure(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_account_decoder::UiAccountEncoding;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let account_to_close = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&account_to_close.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let account_rx = svm_locker
.subscribe_for_account_updates(&account_to_close.pubkey(), Some(UiAccountEncoding::Base64));
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let close_ix = system_instruction::transfer(
&account_to_close.pubkey(),
&recipient,
LAMPORTS_PER_SOL - 5_000,
);
let tx = Transaction::new_signed_with_payer(
&[close_ix],
Some(&account_to_close.pubkey()),
&[&account_to_close],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
true,
false,
)
.await
.unwrap();
let account_update = account_rx.recv_timeout(Duration::from_secs(5));
assert!(
account_update.is_ok(),
"Should receive notification when account is closed"
);
println!("✓ Received notification for account closure");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_slot_subscribe_basic(test_type: TestType) {
use surfpool_types::types::BlockProductionMode;
let (svm_locker, _simnet_commands_tx, _simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(100), test_type);
let slot_rx = svm_locker.subscribe_for_slot_updates();
let slot_info_1 = slot_rx.recv_timeout(Duration::from_secs(2));
assert!(slot_info_1.is_ok(), "Should receive slot update");
let first_slot = slot_info_1.unwrap();
println!("✓ Received first slot update: {}", first_slot.slot);
let slot_info_2 = slot_rx.recv_timeout(Duration::from_secs(2));
assert!(slot_info_2.is_ok(), "Should receive slot update");
let second_slot = slot_info_2.unwrap();
println!("✓ Received second slot update: {}", second_slot.slot);
assert!(
second_slot.slot > first_slot.slot,
"Second slot should be greater than first slot"
);
println!("✓ Slot updates are progressing correctly");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_slot_subscribe_manual_advancement(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let slot_rx = svm_locker.subscribe_for_slot_updates();
let initial_slot = svm_locker.get_latest_absolute_slot();
svm_locker.confirm_current_block(&None).await.unwrap();
let slot_update = slot_rx.recv_timeout(Duration::from_secs(5));
assert!(
slot_update.is_ok(),
"Should receive slot update after block confirmation"
);
let slot_info = slot_update.unwrap();
assert!(
slot_info.slot > initial_slot,
"Updated slot should be greater than initial slot"
);
println!(
"✓ Received slot notification after manual block confirmation: slot {} -> {}",
initial_slot, slot_info.slot
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_slot_subscribe_multiple_subscribers(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let slot_rx1 = svm_locker.subscribe_for_slot_updates();
let slot_rx2 = svm_locker.subscribe_for_slot_updates();
let slot_rx3 = svm_locker.subscribe_for_slot_updates();
svm_locker.confirm_current_block(&None).await.unwrap();
assert!(
slot_rx1.recv_timeout(Duration::from_secs(5)).is_ok(),
"Subscriber 1 should receive slot update"
);
assert!(
slot_rx2.recv_timeout(Duration::from_secs(5)).is_ok(),
"Subscriber 2 should receive slot update"
);
assert!(
slot_rx3.recv_timeout(Duration::from_secs(5)).is_ok(),
"Subscriber 3 should receive slot update"
);
println!("✓ All 3 subscribers received slot update notifications");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_slot_subscribe_multiple_slot_changes(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let slot_rx = svm_locker.subscribe_for_slot_updates();
for i in 0..3 {
svm_locker.confirm_current_block(&None).await.unwrap();
let slot_update = slot_rx.recv_timeout(Duration::from_secs(5));
assert!(
slot_update.is_ok(),
"Should receive slot update for advancement {}",
i + 1
);
let slot_info = slot_update.unwrap();
println!(
"✓ Received slot notification #{}: slot {}",
i + 1,
slot_info.slot
);
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_all_transactions(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let logs_rx = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Processed, &RpcTransactionLogsFilter::All);
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let signature = tx.signatures[0];
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let logs_update = logs_rx.recv_timeout(Duration::from_secs(5));
assert!(logs_update.is_ok(), "Should receive logs update");
let (_slot, logs_response) = logs_update.unwrap();
assert_eq!(
logs_response.signature,
signature.to_string(),
"Signature should match"
);
assert!(
logs_response.err.is_none(),
"Transaction should succeed without error, got error: {}",
logs_response.err.unwrap()
);
assert!(
!logs_response.logs.is_empty(),
"Should have at least one log message"
);
println!("✓ Received logs update for transaction: {}", signature);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_mentions_account(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_commitment_config::CommitmentLevel;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let system_program = solana_sdk_ids::system_program::id();
let logs_rx = svm_locker.subscribe_for_logs_updates(
&CommitmentLevel::Processed,
&RpcTransactionLogsFilter::Mentions(vec![system_program.to_string()]),
);
let token_program = spl_token_interface::id();
let logs_rx_2 = svm_locker.subscribe_for_logs_updates(
&CommitmentLevel::Processed,
&RpcTransactionLogsFilter::Mentions(vec![token_program.to_string()]),
);
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let logs_notification = logs_rx.recv_timeout(Duration::from_secs(5));
assert!(
logs_notification.is_ok(),
"Should receive logs for transaction mentioning system program"
);
let (_slot, logs_response) = logs_notification.unwrap();
assert!(
!logs_response.logs.is_empty(),
"Should have logs from system program"
);
println!("✓ Received logs notification for transaction mentioning system program");
let logs_notification_2 = logs_rx_2.recv_timeout(Duration::from_secs(3));
assert!(
logs_notification_2.is_err(),
"Should NOT receive logs for transaction not mentioning token program"
);
println!("✓ Did not receive logs notification for transaction not mentioning token program");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_confirmed_commitment(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_commitment_config::CommitmentLevel;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let logs_rx = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Confirmed, &RpcTransactionLogsFilter::All);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
svm_locker.confirm_current_block(&None).await.unwrap();
let logs_notification = logs_rx.recv_timeout(Duration::from_secs(5));
assert!(
logs_notification.is_ok(),
"Should receive confirmed logs notification"
);
let (slot, logs_response) = logs_notification.unwrap();
assert!(
!logs_response.logs.is_empty(),
"Confirmed logs should not be empty"
);
println!("✓ Received confirmed logs notification at slot {}", slot);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_finalized_commitment(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_commitment_config::CommitmentLevel;
use solana_system_interface::instruction as system_instruction;
use crate::surfnet::FINALIZATION_SLOT_THRESHOLD;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let logs_rx = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Finalized, &RpcTransactionLogsFilter::All);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
svm_locker.confirm_current_block(&None).await.unwrap();
for _ in 0..FINALIZATION_SLOT_THRESHOLD {
svm_locker.confirm_current_block(&None).await.unwrap();
}
let logs_notification = logs_rx.recv_timeout(Duration::from_secs(5));
assert!(
logs_notification.is_ok(),
"Should receive finalized logs notification"
);
let (slot, logs_response) = logs_notification.unwrap();
assert!(
!logs_response.logs.is_empty(),
"Finalized logs should not be empty"
);
println!("✓ Received finalized logs notification at slot {}", slot);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_failed_transaction(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_commitment_config::CommitmentLevel;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let logs_rx = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Processed, &RpcTransactionLogsFilter::All);
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix =
system_instruction::transfer(&payer.pubkey(), &recipient, LAMPORTS_PER_SOL * 2);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
let _ = svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await;
let logs_notification = logs_rx.recv_timeout(Duration::from_secs(5));
assert!(
logs_notification.is_ok(),
"Should receive logs for failed transaction"
);
let (_slot, logs_response) = logs_notification.unwrap();
assert!(
logs_response.err.is_some(),
"Failed transaction should have error in logs"
);
assert!(
!logs_response.logs.is_empty(),
"Failed transaction should still have logs"
);
println!(
"✓ Received logs notification for failed transaction with error: {:?}",
logs_response.err
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_multiple_subscribers(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_commitment_config::CommitmentLevel;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let logs_rx1 = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Processed, &RpcTransactionLogsFilter::All);
let logs_rx2 = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Processed, &RpcTransactionLogsFilter::All);
let logs_rx3 = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Confirmed, &RpcTransactionLogsFilter::All);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
assert!(
logs_rx1.recv_timeout(Duration::from_secs(5)).is_ok(),
"Processed subscriber 1 should receive logs"
);
assert!(
logs_rx2.recv_timeout(Duration::from_secs(5)).is_ok(),
"Processed subscriber 2 should receive logs"
);
svm_locker.confirm_current_block(&None).await.unwrap();
assert!(
logs_rx3.recv_timeout(Duration::from_secs(5)).is_ok(),
"Confirmed subscriber should receive logs"
);
println!("✓ All subscribers received logs notifications at their respective commitment levels");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_logs_subscribe_logs_content(test_type: TestType) {
use crossbeam_channel::unbounded;
use solana_client::rpc_config::RpcTransactionLogsFilter;
use solana_commitment_config::CommitmentLevel;
use solana_system_interface::instruction as system_instruction;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let logs_rx = svm_locker
.subscribe_for_logs_updates(&CommitmentLevel::Processed, &RpcTransactionLogsFilter::All);
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let signature = tx.signatures[0];
let (status_tx, _status_rx) = unbounded();
svm_locker
.process_transaction(
&None,
VersionedTransaction::from(tx),
status_tx,
false,
false,
)
.await
.unwrap();
let logs_notification = logs_rx.recv_timeout(Duration::from_secs(5));
assert!(logs_notification.is_ok(), "Should receive logs");
let (_slot, logs_response) = logs_notification.unwrap();
assert_eq!(logs_response.signature, signature.to_string());
assert!(logs_response.err.is_none());
assert!(!logs_response.logs.is_empty());
let has_program_log = logs_response.logs.iter().any(|log| log.contains("Program"));
assert!(
has_program_log,
"Logs should contain program execution messages"
);
println!("✓ Logs notification contains valid content:");
for (i, log) in logs_response.logs.iter().enumerate() {
println!(" Log {}: {}", i + 1, log);
}
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_token2022_full_lifecycle(test_type: TestType) {
use solana_system_interface::instruction as system_instruction;
use spl_token_2022_interface::instruction::{
burn, close_account, initialize_mint2, mint_to, transfer_checked,
};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Keypair::new();
let mint = Keypair::new();
let decimals = 6u8;
svm_locker
.airdrop(&payer.pubkey(), 10 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
svm_locker
.airdrop(&recipient.pubkey(), 1 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let mint_rent =
svm_locker.with_svm_reader(|svm| svm.inner.minimum_balance_for_rent_exemption(82));
let create_mint_ix = system_instruction::create_account(
&payer.pubkey(),
&mint.pubkey(),
mint_rent,
82,
&spl_token_2022_interface::id(),
);
let init_mint_ix = initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer.pubkey(), Some(&payer.pubkey()), decimals,
)
.unwrap();
let payer_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&payer.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let recipient_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_payer_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_recipient_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&payer.pubkey(),
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let mint_amount = 1_000_000u64;
let mint_to_ix = mint_to(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer_ata,
&payer.pubkey(),
&[&payer.pubkey()],
mint_amount,
)
.unwrap();
let setup_msg = Message::new_with_blockhash(
&[
create_mint_ix,
init_mint_ix,
create_payer_ata_ix,
create_recipient_ata_ix,
mint_to_ix,
],
Some(&payer.pubkey()),
&recent_blockhash,
);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&payer, &mint])
.unwrap();
let setup_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(setup_tx, false, false));
assert!(
setup_result.is_ok(),
"Setup transaction failed: {:?}",
setup_result.err()
);
let transfer_amount = 500_000u64;
let transfer_ix = transfer_checked(
&spl_token_2022_interface::id(),
&payer_ata,
&mint.pubkey(),
&recipient_ata,
&payer.pubkey(),
&[&payer.pubkey()],
transfer_amount,
decimals,
)
.unwrap();
let transfer_msg =
Message::new_with_blockhash(&[transfer_ix], Some(&payer.pubkey()), &recent_blockhash);
let transfer_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(transfer_msg), &[&payer]).unwrap();
let transfer_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(transfer_tx, false, false));
assert!(
transfer_result.is_ok(),
"Transfer transaction failed: {:?}",
transfer_result.err()
);
let burn_amount = 200_000u64;
let burn_ix = burn(
&spl_token_2022_interface::id(),
&recipient_ata,
&mint.pubkey(),
&recipient.pubkey(),
&[&recipient.pubkey()],
burn_amount,
)
.unwrap();
let burn_msg =
Message::new_with_blockhash(&[burn_ix], Some(&recipient.pubkey()), &recent_blockhash);
let burn_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(burn_msg), &[&recipient]).unwrap();
let burn_result = svm_locker.with_svm_writer(|svm| svm.send_transaction(burn_tx, false, false));
assert!(
burn_result.is_ok(),
"Burn transaction failed: {:?}",
burn_result.err()
);
let remaining = transfer_amount - burn_amount;
let transfer_back_ix = transfer_checked(
&spl_token_2022_interface::id(),
&recipient_ata,
&mint.pubkey(),
&payer_ata,
&recipient.pubkey(),
&[&recipient.pubkey()],
remaining,
decimals,
)
.unwrap();
let close_ix = close_account(
&spl_token_2022_interface::id(),
&recipient_ata,
&recipient.pubkey(), &recipient.pubkey(), &[&recipient.pubkey()],
)
.unwrap();
let close_msg = Message::new_with_blockhash(
&[transfer_back_ix, close_ix],
Some(&recipient.pubkey()),
&recent_blockhash,
);
let close_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(close_msg), &[&recipient]).unwrap();
let close_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(close_tx, false, false));
assert!(
close_result.is_ok(),
"Close transaction failed: {:?}",
close_result.err()
);
let recipient_account = svm_locker
.get_account_local(&recipient_ata)
.inner
.map_account()
.ok();
assert!(
recipient_account.is_none() || recipient_account.as_ref().map(|a| a.lamports) == Some(0),
"Recipient ATA should be closed"
);
let expected_balance = mint_amount - burn_amount;
let _ = expected_balance;
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_token2022_error_cases(test_type: TestType) {
use solana_system_interface::instruction as system_instruction;
use spl_token_2022_interface::instruction::{
burn, close_account, initialize_mint2, mint_to, transfer_checked,
};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Keypair::new();
let mint = Keypair::new();
let decimals = 6u8;
svm_locker
.airdrop(&payer.pubkey(), 10 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
svm_locker
.airdrop(&recipient.pubkey(), 1 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let mint_rent =
svm_locker.with_svm_reader(|svm| svm.inner.minimum_balance_for_rent_exemption(82));
let create_mint_ix = system_instruction::create_account(
&payer.pubkey(),
&mint.pubkey(),
mint_rent,
82,
&spl_token_2022_interface::id(),
);
let init_mint_ix = initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
decimals,
)
.unwrap();
let payer_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&payer.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let recipient_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_payer_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&payer.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_recipient_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&payer.pubkey(),
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let mint_amount = 1_000_000u64;
let mint_to_ix = mint_to(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer_ata,
&payer.pubkey(),
&[&payer.pubkey()],
mint_amount,
)
.unwrap();
let setup_msg = Message::new_with_blockhash(
&[
create_mint_ix,
init_mint_ix,
create_payer_ata_ix,
create_recipient_ata_ix,
mint_to_ix,
],
Some(&payer.pubkey()),
&recent_blockhash,
);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&payer, &mint])
.unwrap();
let setup_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(setup_tx, false, false));
assert!(
setup_result.is_ok(),
"Setup failed: {:?}",
setup_result.err()
);
let excessive_transfer = mint_amount + 1;
let bad_transfer_ix = transfer_checked(
&spl_token_2022_interface::id(),
&payer_ata,
&mint.pubkey(),
&recipient_ata,
&payer.pubkey(),
&[&payer.pubkey()],
excessive_transfer,
decimals,
)
.unwrap();
let bad_transfer_msg =
Message::new_with_blockhash(&[bad_transfer_ix], Some(&payer.pubkey()), &recent_blockhash);
let bad_transfer_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(bad_transfer_msg), &[&payer])
.unwrap();
let transfer_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(bad_transfer_tx, false, false));
assert!(
transfer_result.is_err(),
"Transfer exceeding balance should fail"
);
let excessive_burn = mint_amount + 1;
let bad_burn_ix = burn(
&spl_token_2022_interface::id(),
&payer_ata,
&mint.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
excessive_burn,
)
.unwrap();
let bad_burn_msg =
Message::new_with_blockhash(&[bad_burn_ix], Some(&payer.pubkey()), &recent_blockhash);
let bad_burn_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(bad_burn_msg), &[&payer]).unwrap();
let burn_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(bad_burn_tx, false, false));
assert!(burn_result.is_err(), "Burn exceeding balance should fail");
let bad_close_ix = close_account(
&spl_token_2022_interface::id(),
&payer_ata,
&payer.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
)
.unwrap();
let bad_close_msg =
Message::new_with_blockhash(&[bad_close_ix], Some(&payer.pubkey()), &recent_blockhash);
let bad_close_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(bad_close_msg), &[&payer]).unwrap();
let close_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(bad_close_tx, false, false));
assert!(
close_result.is_err(),
"Close account with balance should fail"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_token2022_delegate_operations(test_type: TestType) {
use solana_system_interface::instruction as system_instruction;
use spl_token_2022_interface::instruction::{
approve, initialize_mint2, mint_to, revoke, transfer_checked,
};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let owner = Keypair::new();
let delegate = Keypair::new();
let recipient = Keypair::new();
let mint = Keypair::new();
let decimals = 6u8;
svm_locker
.airdrop(&owner.pubkey(), 10 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
svm_locker
.airdrop(&delegate.pubkey(), 1 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let mint_rent =
svm_locker.with_svm_reader(|svm| svm.inner.minimum_balance_for_rent_exemption(82));
let create_mint_ix = system_instruction::create_account(
&owner.pubkey(),
&mint.pubkey(),
mint_rent,
82,
&spl_token_2022_interface::id(),
);
let init_mint_ix = initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&owner.pubkey(),
None, decimals,
)
.unwrap();
let owner_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&owner.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let recipient_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_owner_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&owner.pubkey(),
&owner.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_recipient_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&owner.pubkey(),
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let mint_amount = 1_000_000u64;
let mint_to_ix = mint_to(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&owner_ata,
&owner.pubkey(),
&[&owner.pubkey()],
mint_amount,
)
.unwrap();
let setup_msg = Message::new_with_blockhash(
&[
create_mint_ix,
init_mint_ix,
create_owner_ata_ix,
create_recipient_ata_ix,
mint_to_ix,
],
Some(&owner.pubkey()),
&recent_blockhash,
);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&owner, &mint])
.unwrap();
let setup_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(setup_tx, false, false));
assert!(
setup_result.is_ok(),
"Setup failed: {:?}",
setup_result.err()
);
let delegate_amount = 500_000u64;
let approve_ix = approve(
&spl_token_2022_interface::id(),
&owner_ata,
&delegate.pubkey(),
&owner.pubkey(),
&[&owner.pubkey()],
delegate_amount,
)
.unwrap();
let approve_msg =
Message::new_with_blockhash(&[approve_ix], Some(&owner.pubkey()), &recent_blockhash);
let approve_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(approve_msg), &[&owner]).unwrap();
let approve_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(approve_tx, false, false));
assert!(
approve_result.is_ok(),
"Approve failed: {:?}",
approve_result.err()
);
let transfer_amount = 200_000u64;
let delegated_transfer_ix = transfer_checked(
&spl_token_2022_interface::id(),
&owner_ata,
&mint.pubkey(),
&recipient_ata,
&delegate.pubkey(), &[&delegate.pubkey()],
transfer_amount,
decimals,
)
.unwrap();
let delegated_transfer_msg = Message::new_with_blockhash(
&[delegated_transfer_ix],
Some(&delegate.pubkey()),
&recent_blockhash,
);
let delegated_transfer_tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(delegated_transfer_msg),
&[&delegate],
)
.unwrap();
let delegated_transfer_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(delegated_transfer_tx, false, false));
assert!(
delegated_transfer_result.is_ok(),
"Delegated transfer failed: {:?}",
delegated_transfer_result.err()
);
let revoke_ix = revoke(
&spl_token_2022_interface::id(),
&owner_ata,
&owner.pubkey(),
&[&owner.pubkey()],
)
.unwrap();
let revoke_msg =
Message::new_with_blockhash(&[revoke_ix], Some(&owner.pubkey()), &recent_blockhash);
let revoke_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(revoke_msg), &[&owner]).unwrap();
let revoke_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(revoke_tx, false, false));
assert!(
revoke_result.is_ok(),
"Revoke failed: {:?}",
revoke_result.err()
);
let post_revoke_transfer_ix = transfer_checked(
&spl_token_2022_interface::id(),
&owner_ata,
&mint.pubkey(),
&recipient_ata,
&delegate.pubkey(),
&[&delegate.pubkey()],
100_000u64,
decimals,
)
.unwrap();
let post_revoke_msg = Message::new_with_blockhash(
&[post_revoke_transfer_ix],
Some(&delegate.pubkey()),
&recent_blockhash,
);
let post_revoke_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(post_revoke_msg), &[&delegate])
.unwrap();
let post_revoke_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(post_revoke_tx, false, false));
assert!(
post_revoke_result.is_err(),
"Transfer after revoke should fail"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_token2022_freeze_thaw(test_type: TestType) {
use solana_system_interface::instruction as system_instruction;
use spl_token_2022_interface::instruction::{
freeze_account, initialize_mint2, mint_to, thaw_account, transfer_checked,
};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let owner = Keypair::new();
let recipient = Keypair::new();
let mint = Keypair::new();
let decimals = 6u8;
svm_locker
.airdrop(&owner.pubkey(), 10 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let mint_rent =
svm_locker.with_svm_reader(|svm| svm.inner.minimum_balance_for_rent_exemption(82));
let create_mint_ix = system_instruction::create_account(
&owner.pubkey(),
&mint.pubkey(),
mint_rent,
82,
&spl_token_2022_interface::id(),
);
let init_mint_ix = initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&owner.pubkey(),
Some(&owner.pubkey()), decimals,
)
.unwrap();
let owner_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&owner.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let recipient_ata =
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_owner_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&owner.pubkey(),
&owner.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let create_recipient_ata_ix =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&owner.pubkey(),
&recipient.pubkey(),
&mint.pubkey(),
&spl_token_2022_interface::id(),
);
let mint_amount = 1_000_000u64;
let mint_to_ix = mint_to(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&owner_ata,
&owner.pubkey(),
&[&owner.pubkey()],
mint_amount,
)
.unwrap();
let setup_msg = Message::new_with_blockhash(
&[
create_mint_ix,
init_mint_ix,
create_owner_ata_ix,
create_recipient_ata_ix,
mint_to_ix,
],
Some(&owner.pubkey()),
&recent_blockhash,
);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&owner, &mint])
.unwrap();
let setup_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(setup_tx, false, false));
assert!(
setup_result.is_ok(),
"Setup failed: {:?}",
setup_result.err()
);
let freeze_ix = freeze_account(
&spl_token_2022_interface::id(),
&owner_ata,
&mint.pubkey(),
&owner.pubkey(), &[&owner.pubkey()],
)
.unwrap();
let freeze_msg =
Message::new_with_blockhash(&[freeze_ix], Some(&owner.pubkey()), &recent_blockhash);
let freeze_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(freeze_msg), &[&owner]).unwrap();
let freeze_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(freeze_tx, false, false));
assert!(
freeze_result.is_ok(),
"Freeze failed: {:?}",
freeze_result.err()
);
let transfer_amount = 100_000u64;
let frozen_transfer_ix = transfer_checked(
&spl_token_2022_interface::id(),
&owner_ata,
&mint.pubkey(),
&recipient_ata,
&owner.pubkey(),
&[&owner.pubkey()],
transfer_amount,
decimals,
)
.unwrap();
let frozen_transfer_msg = Message::new_with_blockhash(
&[frozen_transfer_ix],
Some(&owner.pubkey()),
&recent_blockhash,
);
let frozen_transfer_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(frozen_transfer_msg), &[&owner])
.unwrap();
let frozen_transfer_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(frozen_transfer_tx, false, false));
assert!(
frozen_transfer_result.is_err(),
"Transfer from frozen account should fail"
);
let fresh_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let thaw_ix = thaw_account(
&spl_token_2022_interface::id(),
&owner_ata,
&mint.pubkey(),
&owner.pubkey(), &[&owner.pubkey()],
)
.unwrap();
let thaw_msg = Message::new_with_blockhash(&[thaw_ix], Some(&owner.pubkey()), &fresh_blockhash);
let thaw_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(thaw_msg), &[&owner]).unwrap();
let thaw_result = svm_locker.with_svm_writer(|svm| svm.send_transaction(thaw_tx, false, false));
assert!(thaw_result.is_ok(), "Thaw failed: {:?}", thaw_result.err());
println!("✓ Step 3: Account thawed");
let post_thaw_amount = 50_000u64; let post_thaw_transfer_ix = transfer_checked(
&spl_token_2022_interface::id(),
&owner_ata,
&mint.pubkey(),
&recipient_ata,
&owner.pubkey(),
&[&owner.pubkey()],
post_thaw_amount,
decimals,
)
.unwrap();
let post_thaw_msg = Message::new_with_blockhash(
&[post_thaw_transfer_ix],
Some(&owner.pubkey()),
&fresh_blockhash,
);
let post_thaw_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(post_thaw_msg), &[&owner]).unwrap();
let post_thaw_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(post_thaw_tx, false, false));
assert!(
post_thaw_result.is_ok(),
"Transfer after thaw failed: {:?}",
post_thaw_result.err()
);
println!(
"✓ Step 4: Transfer after thaw succeeded ({} tokens)",
post_thaw_amount
);
println!("✓ All freeze/thaw operations work correctly!");
}
use std::sync::Once;
static INIT_LOGGER: Once = Once::new();
fn setup() {
INIT_LOGGER.call_once(|| {
env_logger::builder().is_test(true).try_init().unwrap();
});
}
#[test]
fn test_nonce_accounts() {
setup();
use solana_system_interface::instruction::create_nonce_account;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let nonce_account = Keypair::new();
println!("Payer Pubkey: {}", payer.pubkey());
println!("Nonce Account Pubkey: {}", nonce_account.pubkey());
println!("Nonce authority: {}", payer.pubkey());
svm_locker
.airdrop(&payer.pubkey(), 5 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let nonce_rent = svm_locker.with_svm_reader(|svm_reader| {
svm_reader
.inner
.minimum_balance_for_rent_exemption(solana_nonce::state::State::size())
});
let create_nonce_ix = create_nonce_account(
&payer.pubkey(),
&nonce_account.pubkey(),
&payer.pubkey(), nonce_rent,
);
let recent_blockhash = svm_locker.latest_absolute_blockhash();
let create_nonce_msg =
Message::new_with_blockhash(&create_nonce_ix, Some(&payer.pubkey()), &recent_blockhash);
let create_nonce_tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(create_nonce_msg),
&[&payer, &nonce_account],
)
.unwrap();
let create_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(create_nonce_tx, false, false));
assert!(
create_result.is_ok(),
"Create nonce account failed: {:?}",
create_result.err()
);
let nonce_account_data = svm_locker
.get_account_local(&nonce_account.pubkey())
.inner
.map_account()
.expect("Failed to fetch nonce account");
let state: solana_nonce::versions::Versions = bincode::deserialize(&nonce_account_data.data)
.expect("Failed to deserialize nonce account state");
let state = state.state();
let nonce_hash = match state {
solana_nonce::state::State::Initialized(nonce_data) => {
println!(
"✓ Nonce account initialized with nonce: {:?}",
nonce_data.durable_nonce
);
nonce_data.blockhash()
}
_ => panic!("Nonce account is not initialized"),
};
let to_pubkey = Pubkey::new_unique();
let transfer_ix =
solana_system_interface::instruction::transfer(&payer.pubkey(), &to_pubkey, 1_000_000);
let mut nonce_msg = Message::new_with_nonce(
vec![transfer_ix],
Some(&payer.pubkey()),
&nonce_account.pubkey(),
&payer.pubkey(),
);
nonce_msg.recent_blockhash = nonce_hash;
let nonce_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(nonce_msg), &[&payer]).unwrap();
let nonce_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(nonce_tx, false, false));
assert!(
nonce_result.is_ok(),
"Transaction using nonce failed: {:?}",
nonce_result.err()
);
let to_account_data = svm_locker
.get_account_local(&to_pubkey)
.inner
.map_account()
.expect("Failed to fetch recipient account");
assert_eq!(
to_account_data.lamports, 1_000_000,
"Recipient account did not receive correct amount"
);
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_profile_transaction_does_not_mutate_state(test_type: TestType) {
let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let rpc_server = crate::rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc::empty();
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
let initial_payer_balance = lamports_to_send * 10;
svm_instance
.airdrop(&payer.pubkey(), initial_payer_balance)
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_instance.latest_blockhash();
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message.clone()), &[&payer])
.unwrap();
let tx_bytes = bincode::serialize(&tx).unwrap();
let tx_b64 = base64::engine::general_purpose::STANDARD.encode(&tx_bytes);
let initial_transactions_processed = svm_instance.transactions_processed;
let initial_payer_account = svm_instance.get_account(&payer.pubkey()).unwrap();
let initial_recipient_account = svm_instance.get_account(&recipient).ok().flatten();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let (simnet_cmd_tx, _simnet_cmd_rx) = crossbeam_unbounded::<SimnetCommand>();
let (plugin_commands_tx, _plugin_commands_rx) = crossbeam_channel::unbounded::<PluginCommand>();
let runloop_context = RunloopContext {
id: None,
svm_locker: svm_locker.clone(),
simnet_commands_tx: simnet_cmd_tx,
remote_rpc_client: None,
rpc_config: RpcConfig::default(),
cheatcode_config: CheatcodeConfig::new(),
plugin_commands_tx,
};
for i in 0..3 {
let response: JsonRpcResult<RpcResponse<UiKeyedProfileResult>> = rpc_server
.profile_transaction(
Some(runloop_context.clone()),
tx_b64.clone(),
Some(format!("test_isolation_{}", i)),
None,
)
.await;
assert!(
response.is_ok(),
"Profile transaction {} failed: {:?}",
i,
response.err()
);
let profile_result = response.unwrap().value;
assert!(
profile_result.transaction_profile.error_message.is_none(),
"Profile {} had unexpected error: {:?}",
i,
profile_result.transaction_profile.error_message
);
assert!(
profile_result.transaction_profile.compute_units_consumed > 0,
"Profile {} should show compute units consumed",
i
);
}
let final_transactions_processed = svm_locker.with_svm_reader(|svm| svm.transactions_processed);
let final_payer_account = svm_locker
.with_svm_reader(|svm| svm.get_account(&payer.pubkey()))
.unwrap();
let final_recipient_account = svm_locker
.with_svm_reader(|svm| svm.get_account(&recipient))
.ok()
.flatten();
assert_eq!(
initial_transactions_processed, final_transactions_processed,
"transactions_processed should not change from profiling"
);
assert_eq!(
initial_payer_account.as_ref().map(|a| a.lamports),
final_payer_account.as_ref().map(|a| a.lamports),
"Payer balance should not change from profiling"
);
assert_eq!(
initial_recipient_account.as_ref().map(|a| a.lamports),
final_recipient_account.as_ref().map(|a| a.lamports),
"Recipient balance should not change from profiling"
);
let execution_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(tx.clone(), false, false));
assert!(
execution_result.is_ok(),
"Actual transaction execution should succeed: {:?}",
execution_result.err()
);
let post_execution_transactions = svm_locker.with_svm_reader(|svm| svm.transactions_processed);
let post_execution_recipient = svm_locker
.with_svm_reader(|svm| svm.get_account(&recipient))
.unwrap();
assert_eq!(
post_execution_transactions,
initial_transactions_processed + 1,
"Transaction count should increase after actual execution"
);
assert!(
post_execution_recipient.is_some(),
"Recipient should exist after actual execution"
);
assert_eq!(
post_execution_recipient.unwrap().lamports,
lamports_to_send,
"Recipient should have received funds after actual execution"
);
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_instruction_profiling_does_not_mutate_state(test_type: TestType) {
let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
assert!(
svm_instance.instruction_profiling_enabled,
"Instruction profiling should be enabled by default"
);
let payer = Keypair::new();
let payer_without_funds = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = LAMPORTS_PER_SOL;
svm_instance
.airdrop(&payer.pubkey(), lamports_to_send * 3)
.unwrap()
.unwrap();
let initial_payer_balance = svm_instance
.get_account(&payer.pubkey())
.unwrap()
.map(|a| a.lamports)
.unwrap_or(0);
let initial_recipient_account = svm_instance.get_account(&recipient).ok().flatten();
let initial_transactions_processed = svm_instance.transactions_processed;
let valid_instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let invalid_instruction = transfer(&payer_without_funds.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_instance.latest_blockhash();
let message = Message::new_with_blockhash(
&[valid_instruction, invalid_instruction],
Some(&payer.pubkey()),
&latest_blockhash,
);
let transaction = VersionedTransaction::try_new(
VersionedMessage::Legacy(message),
&[&payer, &payer_without_funds],
)
.unwrap();
let signature = transaction.signatures[0];
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let (status_tx, _status_rx) = crossbeam_unbounded::<TransactionStatusEvent>();
let process_result = svm_locker
.process_transaction(
&None, transaction.clone(),
status_tx,
true, false, )
.await;
assert!(
process_result.is_err() || process_result.is_ok(),
"process_transaction should complete (success or failure)"
);
let key = UuidOrSignature::Signature(signature);
let profile_result = svm_locker
.get_profile_result(key, &RpcProfileResultConfig::default())
.unwrap()
.expect("Profile result should exist for executed transaction");
assert!(
profile_result.transaction_profile.error_message.is_some(),
"Transaction should fail due to second instruction's insufficient funds"
);
assert!(
profile_result.instruction_profiles.is_some(),
"Instruction profiles should be generated when instruction profiling is enabled"
);
let instruction_profiles = profile_result.instruction_profiles.as_ref().unwrap();
assert_eq!(
instruction_profiles.len(),
2,
"Should have profiles for both instructions"
);
let first_ix_profile = &instruction_profiles[0];
assert!(
first_ix_profile.error_message.is_none(),
"First instruction profile should succeed: {:?}",
first_ix_profile.error_message
);
assert!(
first_ix_profile.compute_units_consumed > 0,
"First instruction should have consumed compute units"
);
let second_ix_profile = &instruction_profiles[1];
assert!(
second_ix_profile.error_message.is_some(),
"Second instruction should fail due to insufficient funds"
);
let final_payer_balance = svm_locker
.with_svm_reader(|svm| svm.get_account(&payer.pubkey()))
.unwrap()
.map(|a| a.lamports)
.unwrap_or(0);
let final_recipient_account = svm_locker
.with_svm_reader(|svm| svm.get_account(&recipient))
.ok()
.flatten();
let final_transactions_processed = svm_locker.with_svm_reader(|svm| svm.transactions_processed);
assert_eq!(
initial_recipient_account.as_ref().map(|a| a.lamports),
final_recipient_account.as_ref().map(|a| a.lamports),
"Recipient should not have received funds - instruction profiling must not leak state"
);
let balance_decrease = initial_payer_balance.saturating_sub(final_payer_balance);
assert!(
balance_decrease < lamports_to_send,
"Payer should only lose transaction fee, not the transfer amount. Lost: {} lamports",
balance_decrease
);
assert_eq!(
final_transactions_processed,
initial_transactions_processed + 1,
"Transaction count should increment after processing (even for failed tx)"
);
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_program_subscribe_basic(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let program_id = Pubkey::new_unique();
let account_pubkey = Pubkey::new_unique();
let program_rx = svm_locker.subscribe_for_program_updates(
&program_id,
Some(UiAccountEncoding::Base64),
None,
);
let account = Account {
lamports: 1_000_000,
data: vec![1, 2, 3, 4],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&account_pubkey, account).unwrap();
});
let update = program_rx.recv_timeout(Duration::from_secs(5));
assert!(update.is_ok(), "Should receive program account update");
let keyed_account = update.unwrap();
assert_eq!(
keyed_account.pubkey,
account_pubkey.to_string(),
"Notification should contain the correct account pubkey"
);
assert_eq!(
keyed_account.account.lamports, 1_000_000,
"Notification should contain the correct lamports"
);
println!("✓ programSubscribe basic flow works correctly");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_program_subscribe_data_size_filter(test_type: TestType) {
use solana_client::rpc_filter::RpcFilterType;
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let program_id = Pubkey::new_unique();
let filters = vec![RpcFilterType::DataSize(8)];
let program_rx = svm_locker.subscribe_for_program_updates(
&program_id,
Some(UiAccountEncoding::Base64),
Some(filters),
);
let small_account_pubkey = Pubkey::new_unique();
let small_account = Account {
lamports: 1_000_000,
data: vec![1, 2, 3, 4],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&small_account_pubkey, small_account)
.unwrap();
});
let no_update = program_rx.recv_timeout(Duration::from_millis(200));
assert!(
no_update.is_err(),
"Should NOT receive notification for account that doesn't match dataSize filter"
);
let matching_account_pubkey = Pubkey::new_unique();
let matching_account = Account {
lamports: 2_000_000,
data: vec![1, 2, 3, 4, 5, 6, 7, 8],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&matching_account_pubkey, matching_account)
.unwrap();
});
let update = program_rx.recv_timeout(Duration::from_secs(5));
assert!(
update.is_ok(),
"Should receive notification for account matching dataSize filter"
);
let keyed_account = update.unwrap();
assert_eq!(
keyed_account.pubkey,
matching_account_pubkey.to_string(),
"Notification should contain the correct matching account pubkey"
);
println!("✓ programSubscribe with dataSize filter works correctly");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_program_subscribe_memcmp_filter(test_type: TestType) {
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let program_id = Pubkey::new_unique();
let filters = vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
0,
vec![0xAA, 0xBB],
))];
let program_rx = svm_locker.subscribe_for_program_updates(
&program_id,
Some(UiAccountEncoding::Base64),
Some(filters),
);
let non_matching_pubkey = Pubkey::new_unique();
let non_matching_account = Account {
lamports: 1_000_000,
data: vec![0x00, 0x00, 0xCC, 0xDD],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&non_matching_pubkey, non_matching_account)
.unwrap();
});
let no_update = program_rx.recv_timeout(Duration::from_millis(200));
assert!(
no_update.is_err(),
"Should NOT receive notification for account that doesn't match memcmp filter"
);
let matching_pubkey = Pubkey::new_unique();
let matching_account = Account {
lamports: 2_000_000,
data: vec![0xAA, 0xBB, 0xCC, 0xDD],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&matching_pubkey, matching_account).unwrap();
});
let update = program_rx.recv_timeout(Duration::from_secs(5));
assert!(
update.is_ok(),
"Should receive notification for account matching memcmp filter"
);
let keyed_account = update.unwrap();
assert_eq!(
keyed_account.pubkey,
matching_pubkey.to_string(),
"Notification should contain the correct matching account pubkey"
);
println!("✓ programSubscribe with memcmp filter works correctly");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_program_unsubscribe(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let program_id = Pubkey::new_unique();
let program_rx = svm_locker.subscribe_for_program_updates(
&program_id,
Some(UiAccountEncoding::Base64),
None,
);
let account_pubkey = Pubkey::new_unique();
let account = Account {
lamports: 1_000_000,
data: vec![1, 2, 3, 4],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&account_pubkey, account).unwrap();
});
let update = program_rx.recv_timeout(Duration::from_secs(5));
assert!(update.is_ok(), "Should receive first notification");
drop(program_rx);
let account_pubkey2 = Pubkey::new_unique();
let account2 = Account {
lamports: 2_000_000,
data: vec![5, 6, 7, 8],
owner: program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&account_pubkey2, account2).unwrap();
});
println!("✓ programUnsubscribe works correctly (no panic after receiver dropped)");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_ws_program_subscribe_wrong_owner(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let subscribed_program_id = Pubkey::new_unique();
let other_program_id = Pubkey::new_unique();
let program_rx = svm_locker.subscribe_for_program_updates(
&subscribed_program_id,
Some(UiAccountEncoding::Base64),
None,
);
let account_pubkey = Pubkey::new_unique();
let account = Account {
lamports: 1_000_000,
data: vec![1, 2, 3, 4],
owner: other_program_id,
executable: false,
rent_epoch: 0,
};
svm_locker.with_svm_writer(|svm| {
svm.set_account(&account_pubkey, account).unwrap();
});
let no_update = program_rx.recv_timeout(Duration::from_millis(200));
assert!(
no_update.is_err(),
"Should NOT receive notification for account owned by a different program"
);
println!("✓ programSubscribe correctly ignores accounts owned by other programs");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_token2022_metadata_realloc(test_type: TestType) {
use solana_system_interface::instruction as system_instruction;
use spl_token_2022_interface::{
extension::{ExtensionType, metadata_pointer},
instruction::initialize_mint2,
pod::PodMint,
};
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let mint = Keypair::new();
let decimals = 6u8;
svm_locker
.airdrop(&payer.pubkey(), 10 * LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let mint_space =
ExtensionType::try_calculate_account_len::<PodMint>(&[ExtensionType::MetadataPointer])
.unwrap();
let mint_rent =
svm_locker.with_svm_reader(|svm| svm.inner.minimum_balance_for_rent_exemption(mint_space));
let create_account_ix = system_instruction::create_account(
&payer.pubkey(),
&mint.pubkey(),
mint_rent,
mint_space as u64,
&spl_token_2022_interface::id(),
);
let init_metadata_pointer_ix = metadata_pointer::instruction::initialize(
&spl_token_2022_interface::id(),
&mint.pubkey(),
Some(payer.pubkey()),
Some(mint.pubkey()),
)
.unwrap();
let init_mint_ix = initialize_mint2(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer.pubkey(),
None,
decimals,
)
.unwrap();
let setup_msg = Message::new_with_blockhash(
&[create_account_ix, init_metadata_pointer_ix, init_mint_ix],
Some(&payer.pubkey()),
&recent_blockhash,
);
let setup_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&payer, &mint])
.unwrap();
let setup_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(setup_tx, false, false));
assert!(
setup_result.is_ok(),
"Setup transaction (create mint with MetadataPointer) \
should succeed: {:?}",
setup_result,
);
println!("✓ Setup transaction succeeded");
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let increase_mint_rent_ix =
system_instruction::transfer(&payer.pubkey(), &mint.pubkey(), LAMPORTS_PER_SOL);
let metadata_ix_data = spl_token_metadata_interface::instruction::initialize(
&spl_token_2022_interface::id(),
&mint.pubkey(),
&payer.pubkey(),
&mint.pubkey(),
&payer.pubkey(),
"Test Token".to_string(),
"TST".to_string(),
"https://example.com/metadata.json".to_string(),
);
let msg = Message::new_with_blockhash(
&[increase_mint_rent_ix, metadata_ix_data],
Some(&payer.pubkey()),
&recent_blockhash,
);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let result = svm_locker.with_svm_writer(|svm| svm.send_transaction(tx, false, false));
assert!(
result.is_ok(),
"CPI metadata initialization should succeed (regression test for #530): {:?}",
result,
);
println!("✓ Regression test #530: Token-2022 metadata CPI realloc works correctly");
}
async fn test_duplicate_transaction_rejected(test_type: TestType) {
let (svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm();
let svm_locker = SurfnetSvmLocker::new(svm_instance);
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let lamports_to_send = 1_000_000;
svm_locker
.with_svm_writer(|svm| svm.airdrop(&payer.pubkey(), lamports_to_send * 10))
.unwrap()
.unwrap();
let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
let latest_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let message =
Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &latest_blockhash);
let transaction =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let (status_tx, status_rx) = crossbeam_unbounded();
svm_locker
.process_transaction(&None, transaction.clone(), status_tx, false, true)
.await
.unwrap();
match status_rx.recv() {
Ok(TransactionStatusEvent::Success(_)) => {
println!("First transaction processed successfully");
}
other => {
panic!("Expected first transaction to succeed, got: {:?}", other);
}
}
let (status_tx2, status_rx2) = crossbeam_unbounded();
let result = svm_locker
.process_transaction(&None, transaction.clone(), status_tx2, false, true)
.await;
assert!(result.is_err(), "Duplicate transaction should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("already been processed"),
"Error should mention 'already been processed', got: {err_msg}"
);
match status_rx2.recv() {
Ok(TransactionStatusEvent::VerificationFailure(msg)) => {
assert!(
msg.contains("already been processed"),
"VerificationFailure should mention 'already been processed', got: {msg}"
);
println!("Duplicate correctly rejected with VerificationFailure");
}
other => {
panic!(
"Expected VerificationFailure on status channel, got: {:?}",
other
);
}
}
let sig = transaction.signatures[0].to_string();
let stored = svm_locker.with_svm_reader(|svm| svm.transactions.get(&sig));
assert!(
matches!(
stored,
Ok(Some(crate::types::SurfnetTransactionStatus::Processed(_)))
),
"Original transaction should still be stored as Processed"
);
println!("Duplicate transaction rejection test passed!");
}
#[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"))]
#[tokio::test(flavor = "multi_thread")]
async fn test_account_loaded_twice_rejected(test_type: TestType) {
let (svm_locker, _simnet_cmd_tx, _simnet_events_rx) =
boot_simnet(BlockProductionMode::Clock, Some(400), test_type);
let payer = Keypair::new();
svm_locker
.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap()
.unwrap();
let recent_blockhash = svm_locker.with_svm_reader(|svm| svm.latest_blockhash());
let alt_key = Pubkey::new_unique();
let system_program_id = system_program::id();
let alt_account_data = AddressLookupTable {
meta: LookupTableMeta {
authority: Some(payer.pubkey()),
..Default::default()
},
addresses: vec![system_program_id].into(),
};
svm_locker.with_svm_writer(|svm| {
let alt_data = alt_account_data.serialize_for_tests().unwrap();
let alt_account = Account {
lamports: 1_000_000,
data: alt_data,
owner: solana_address_lookup_table_interface::program::id(),
executable: false,
rent_epoch: 0,
};
svm.set_account(&alt_key, alt_account).unwrap();
});
let recipient = Pubkey::new_unique();
let v0_message = v0::Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![payer.pubkey(), recipient, system_program_id],
recent_blockhash,
instructions: vec![solana_message::compiled_instruction::CompiledInstruction {
program_id_index: 2,
accounts: vec![0, 1],
data: {
let mut data = vec![2, 0, 0, 0];
data.extend_from_slice(&100u64.to_le_bytes());
data
},
}],
address_table_lookups: vec![MessageAddressTableLookup {
account_key: alt_key,
writable_indexes: vec![],
readonly_indexes: vec![0], }],
};
let tx = VersionedTransaction::try_new(VersionedMessage::V0(v0_message), &[&payer])
.expect("Failed to create transaction");
let (status_tx, _status_rx) = crossbeam_channel::unbounded();
let result = svm_locker
.process_transaction(&None, tx, status_tx, false, true)
.await;
assert!(
result.is_err(),
"Transaction with duplicate account should be rejected, got: {:?}",
result
);
let err_string = result.unwrap_err().to_string();
assert!(
err_string.contains("Account loaded twice"),
"Expected AccountLoadedTwice error, got: {}",
err_string
);
println!("AccountLoadedTwice rejection test passed!");
}
#[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")]
#[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"))]
#[tokio::test]
async fn test_send_transaction_skip_sig_verify_processes_and_updates_state(test_type: TestType) {
let payer = Keypair::new();
let recipient = Keypair::new().pubkey();
let transfer_amount = LAMPORTS_PER_SOL / 2;
let airdrop_amount = LAMPORTS_PER_SOL * 2;
let bind_host = "127.0.0.1";
let bind_port = get_free_port().unwrap();
let ws_port = get_free_port().unwrap();
let config = SurfpoolConfig {
simnets: vec![SimnetConfig {
slot_time: 1,
airdrop_addresses: vec![payer.pubkey()],
airdrop_token_amount: airdrop_amount,
..SimnetConfig::default()
}],
rpc: RpcConfig {
bind_host: bind_host.to_string(),
bind_port,
ws_port,
..Default::default()
},
..SurfpoolConfig::default()
};
let (surfnet_svm, simnet_events_rx, geyser_events_rx) = test_type.initialize_svm();
let (simnet_commands_tx, simnet_commands_rx) = unbounded();
let svm_locker = SurfnetSvmLocker::new(surfnet_svm);
let _handle = hiro_system_kit::thread_named("test").spawn(move || {
let future = start_local_surfnet_runloop(
svm_locker,
config,
simnet_commands_tx,
simnet_commands_rx,
geyser_events_rx,
);
if let Err(e) = hiro_system_kit::nestable_block_on(future) {
panic!("{e:?}");
}
});
wait_for_ready_and_connected(&simnet_events_rx);
let minimal_client =
http::connect::<MinimalClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let full_client =
http::connect::<FullClient>(format!("http://{bind_host}:{bind_port}").as_str())
.await
.expect("Failed to connect to Surfpool");
let payer_balance = minimal_client
.get_balance(payer.pubkey().to_string(), None)
.await
.expect("Failed to get payer balance");
assert_eq!(
payer_balance.value, airdrop_amount,
"Payer should have airdrop amount"
);
let recipient_balance = minimal_client
.get_balance(recipient.to_string(), None)
.await
.expect("Failed to get recipient balance");
assert_eq!(
recipient_balance.value, 0,
"Recipient should start with zero balance"
);
let recent_blockhash = full_client
.get_latest_blockhash(None)
.await
.map(|r| {
Hash::from_str(r.value.blockhash.as_str()).expect("Failed to deserialize blockhash")
})
.expect("Failed to get blockhash");
let msg = VersionedMessage::Legacy(Message::new_with_blockhash(
&[system_instruction::transfer(
&payer.pubkey(),
&recipient,
transfer_amount,
)],
Some(&payer.pubkey()),
&recent_blockhash,
));
let tx = VersionedTransaction {
signatures: vec![solana_signature::Signature::new_unique()],
message: msg,
};
let encoded = bincode::serialize(&tx).expect("Failed to serialize tx");
let data = bs58::encode(encoded).into_string();
let send_config = SurfpoolRpcSendTransactionConfig {
base: RpcSendTransactionConfig {
skip_preflight: true,
..Default::default()
},
skip_sig_verify: Some(true),
};
let _sig = full_client
.send_transaction(data, Some(send_config))
.await
.expect("send_transaction with skip_sig_verify should succeed");
let _ = task::spawn_blocking(move || {
loop {
match simnet_events_rx.recv() {
Ok(SimnetEvent::TransactionProcessed(..)) => break,
_ => (),
}
}
})
.await;
let final_recipient_balance = minimal_client
.get_balance(recipient.to_string(), None)
.await
.expect("Failed to get final recipient balance");
assert_eq!(
final_recipient_balance.value, transfer_amount,
"Recipient should have received the transfer amount"
);
let final_payer_balance = minimal_client
.get_balance(payer.pubkey().to_string(), None)
.await
.expect("Failed to get final payer balance");
assert!(
final_payer_balance.value < airdrop_amount - transfer_amount,
"Payer balance ({}) should be less than airdrop minus transfer ({}) due to fees",
final_payer_balance.value,
airdrop_amount - transfer_amount
);
assert!(
final_payer_balance.value > airdrop_amount - transfer_amount - LAMPORTS_PER_SOL / 10,
"Payer balance ({}) should not have decreased by more than transfer + reasonable fees",
final_payer_balance.value,
);
}