1use std::str::FromStr;
2
3use itertools::Itertools;
4use jsonrpc_core::{BoxFuture, Error, Result};
5use jsonrpc_derive::rpc;
6use litesvm::types::TransactionMetadata;
7use solana_account_decoder::UiAccount;
8use solana_client::{
9 rpc_config::{
10 RpcAccountInfoConfig, RpcBlockConfig, RpcBlocksConfigWrapper, RpcContextConfig,
11 RpcEncodingConfigWrapper, RpcEpochConfig, RpcRequestAirdropConfig,
12 RpcSendTransactionConfig, RpcSignatureStatusConfig, RpcSignaturesForAddressConfig,
13 RpcSimulateTransactionConfig, RpcTransactionConfig,
14 },
15 rpc_custom_error::RpcCustomError,
16 rpc_response::{
17 RpcBlockhash, RpcConfirmedTransactionStatusWithSignature, RpcContactInfo,
18 RpcInflationReward, RpcPerfSample, RpcPrioritizationFee, RpcResponseContext,
19 RpcSimulateTransactionResult,
20 },
21};
22use solana_clock::{Slot, UnixTimestamp};
23use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
24use solana_compute_budget_interface::ComputeBudgetInstruction;
25use solana_message::{VersionedMessage, compiled_instruction::CompiledInstruction};
26use solana_pubkey::Pubkey;
27use solana_rpc_client_api::response::Response as RpcResponse;
28use solana_sdk_ids::compute_budget;
29use solana_signature::Signature;
30use solana_system_interface::program as system_program;
31use solana_transaction::versioned::VersionedTransaction;
32use solana_transaction_error::TransactionError;
33use solana_transaction_status::{
34 EncodedConfirmedTransactionWithStatusMeta, TransactionBinaryEncoding,
35 TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, UiTransactionEncoding,
36};
37use surfpool_types::{SimnetCommand, TransactionStatusEvent};
38
39use super::{
40 RunloopContext, State, SurfnetRpcContext,
41 utils::{decode_and_deserialize, transform_tx_metadata_to_ui_accounts, verify_pubkey},
42};
43use crate::{
44 SURFPOOL_IDENTITY_PUBKEY,
45 error::{SurfpoolError, SurfpoolResult},
46 rpc::utils::{adjust_default_transaction_config, get_default_transaction_config},
47 surfnet::{
48 FINALIZATION_SLOT_THRESHOLD, GetAccountResult, GetTransactionResult,
49 locker::SvmAccessContext, svm::MAX_RECENT_BLOCKHASHES_STANDARD,
50 },
51 types::{SurfnetTransactionStatus, surfpool_tx_metadata_to_litesvm_tx_metadata},
52};
53
54const MAX_PRIORITIZATION_FEE_BLOCKS_CACHE: usize = 150;
55
56#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct SurfpoolRpcSendTransactionConfig {
59 #[serde(flatten)]
60 pub base: RpcSendTransactionConfig,
61 pub skip_sig_verify: Option<bool>,
63}
64
65#[rpc]
66pub trait Full {
67 type Metadata;
68
69 #[rpc(meta, name = "getInflationReward")]
129 fn get_inflation_reward(
130 &self,
131 meta: Self::Metadata,
132 address_strs: Vec<String>,
133 config: Option<RpcEpochConfig>,
134 ) -> BoxFuture<Result<Vec<Option<RpcInflationReward>>>>;
135
136 #[rpc(meta, name = "getClusterNodes")]
181 fn get_cluster_nodes(&self, meta: Self::Metadata) -> Result<Vec<RpcContactInfo>>;
182
183 #[rpc(meta, name = "getRecentPerformanceSamples")]
226 fn get_recent_performance_samples(
227 &self,
228 meta: Self::Metadata,
229 limit: Option<usize>,
230 ) -> Result<Vec<RpcPerfSample>>;
231
232 #[rpc(meta, name = "getSignatureStatuses")]
317 fn get_signature_statuses(
318 &self,
319 meta: Self::Metadata,
320 signature_strs: Vec<String>,
321 config: Option<RpcSignatureStatusConfig>,
322 ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>>;
323
324 #[rpc(meta, name = "getMaxRetransmitSlot")]
365 fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot>;
366
367 #[rpc(meta, name = "getMaxShredInsertSlot")]
408 fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot>;
409
410 #[rpc(meta, name = "requestAirdrop")]
458 fn request_airdrop(
459 &self,
460 meta: Self::Metadata,
461 pubkey_str: String,
462 lamports: u64,
463 config: Option<RpcRequestAirdropConfig>,
464 ) -> Result<String>;
465
466 #[rpc(meta, name = "sendTransaction")]
519 fn send_transaction(
520 &self,
521 meta: Self::Metadata,
522 data: String,
523 config: Option<SurfpoolRpcSendTransactionConfig>,
524 ) -> Result<String>;
525
526 #[rpc(meta, name = "simulateTransaction")]
594 fn simulate_transaction(
595 &self,
596 meta: Self::Metadata,
597 data: String,
598 config: Option<RpcSimulateTransactionConfig>,
599 ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>>;
600
601 #[rpc(meta, name = "minimumLedgerSlot")]
641 fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>>;
642
643 #[rpc(meta, name = "getBlock")]
700 fn get_block(
701 &self,
702 meta: Self::Metadata,
703 slot: Slot,
704 config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
705 ) -> BoxFuture<Result<Option<UiConfirmedBlock>>>;
706
707 #[rpc(meta, name = "getBlockTime")]
749 fn get_block_time(
750 &self,
751 meta: Self::Metadata,
752 slot: Slot,
753 ) -> BoxFuture<Result<Option<UnixTimestamp>>>;
754
755 #[rpc(meta, name = "getBlocks")]
801 fn get_blocks(
802 &self,
803 meta: Self::Metadata,
804 start_slot: Slot,
805 wrapper: Option<RpcBlocksConfigWrapper>,
806 config: Option<RpcContextConfig>,
807 ) -> BoxFuture<Result<Vec<Slot>>>;
808
809 #[rpc(meta, name = "getBlocksWithLimit")]
854 fn get_blocks_with_limit(
855 &self,
856 meta: Self::Metadata,
857 start_slot: Slot,
858 limit: usize,
859 config: Option<RpcContextConfig>,
860 ) -> BoxFuture<Result<Vec<Slot>>>;
861
862 #[rpc(meta, name = "getTransaction")]
933 fn get_transaction(
934 &self,
935 meta: Self::Metadata,
936 signature_str: String,
937 config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
938 ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>>;
939
940 #[rpc(meta, name = "getSignaturesForAddress")]
1017 fn get_signatures_for_address(
1018 &self,
1019 meta: Self::Metadata,
1020 address: String,
1021 config: Option<RpcSignaturesForAddressConfig>,
1022 ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>>;
1023
1024 #[rpc(meta, name = "getFirstAvailableBlock")]
1065 fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot>;
1066
1067 #[rpc(meta, name = "getLatestBlockhash")]
1121 fn get_latest_blockhash(
1122 &self,
1123 meta: Self::Metadata,
1124 config: Option<RpcContextConfig>,
1125 ) -> Result<RpcResponse<RpcBlockhash>>;
1126
1127 #[rpc(meta, name = "isBlockhashValid")]
1180 fn is_blockhash_valid(
1181 &self,
1182 meta: Self::Metadata,
1183 blockhash: String,
1184 config: Option<RpcContextConfig>,
1185 ) -> Result<RpcResponse<bool>>;
1186
1187 #[rpc(meta, name = "getFeeForMessage")]
1242 fn get_fee_for_message(
1243 &self,
1244 meta: Self::Metadata,
1245 data: String,
1246 config: Option<RpcContextConfig>,
1247 ) -> Result<RpcResponse<Option<u64>>>;
1248
1249 #[rpc(meta, name = "getStakeMinimumDelegation")]
1296 fn get_stake_minimum_delegation(
1297 &self,
1298 meta: Self::Metadata,
1299 config: Option<RpcContextConfig>,
1300 ) -> Result<RpcResponse<u64>>;
1301
1302 #[rpc(meta, name = "getRecentPrioritizationFees")]
1351 fn get_recent_prioritization_fees(
1352 &self,
1353 meta: Self::Metadata,
1354 pubkey_strs: Option<Vec<String>>,
1355 ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>>;
1356}
1357
1358#[derive(Clone)]
1359pub struct SurfpoolFullRpc;
1360impl Full for SurfpoolFullRpc {
1361 type Metadata = Option<RunloopContext>;
1362
1363 fn get_inflation_reward(
1364 &self,
1365 meta: Self::Metadata,
1366 address_strs: Vec<String>,
1367 config: Option<RpcEpochConfig>,
1368 ) -> BoxFuture<Result<Vec<Option<RpcInflationReward>>>> {
1369 Box::pin(async move {
1370 let svm_locker = meta.get_svm_locker()?;
1371
1372 let current_epoch = svm_locker.get_epoch_info().epoch;
1373 if let Some(epoch) = config.as_ref().and_then(|config| config.epoch) {
1374 if epoch > current_epoch {
1375 return Err(Error::invalid_params(
1376 "Invalid epoch. Epoch is larger that current epoch",
1377 ));
1378 }
1379 };
1380
1381 let current_slot = svm_locker.get_epoch_info().absolute_slot;
1382 if let Some(slot) = config.as_ref().and_then(|config| config.min_context_slot) {
1383 if slot > current_slot {
1384 return Err(Error::invalid_params(
1385 "Minimum context slot has not been reached",
1386 ));
1387 }
1388 };
1389
1390 let pubkeys = address_strs
1391 .iter()
1392 .map(|addr| verify_pubkey(addr))
1393 .collect::<std::result::Result<Vec<Pubkey>, SurfpoolError>>()?;
1394
1395 meta.with_svm_reader(|svm_reader| {
1396 pubkeys
1397 .iter()
1398 .map(|_| {
1399 Some(RpcInflationReward {
1400 amount: 0,
1401 commission: None,
1402 effective_slot: svm_reader.get_latest_absolute_slot(),
1403 epoch: svm_reader.latest_epoch_info().epoch,
1404 post_balance: 0,
1405 })
1406 })
1407 .collect()
1408 })
1409 .map_err(Into::into)
1410 })
1411 }
1412
1413 fn get_cluster_nodes(&self, meta: Self::Metadata) -> Result<Vec<RpcContactInfo>> {
1414 let (gossip, tpu, tpu_quic, rpc, pubsub) = if let Some(ctx) = meta {
1415 let config = ctx.rpc_config;
1416 let to_socket = |port: u16| -> Option<std::net::SocketAddr> {
1417 format!("{}:{}", config.bind_host, port).parse().ok()
1418 };
1419 (
1420 to_socket(config.gossip_port),
1421 to_socket(config.tpu_port),
1422 to_socket(config.tpu_quic_port),
1423 to_socket(config.bind_port),
1424 to_socket(config.ws_port),
1425 )
1426 } else {
1427 (None, None, None, None, None)
1428 };
1429
1430 Ok(vec![RpcContactInfo {
1431 pubkey: SURFPOOL_IDENTITY_PUBKEY.to_string(),
1432 gossip,
1433 tvu: None,
1434 tpu,
1435 tpu_quic,
1436 tpu_forwards: None,
1437 tpu_forwards_quic: None,
1438 tpu_vote: None,
1439 serve_repair: None,
1440 rpc,
1441 pubsub,
1442 version: None,
1443 feature_set: None,
1444 shred_version: None,
1445 }])
1446 }
1447
1448 fn get_recent_performance_samples(
1449 &self,
1450 meta: Self::Metadata,
1451 limit: Option<usize>,
1452 ) -> Result<Vec<RpcPerfSample>> {
1453 let limit = limit.unwrap_or(720);
1454 if limit > 720 {
1455 return Err(Error::invalid_params("Invalid limit; max 720"));
1456 }
1457
1458 meta.with_svm_reader(|svm_reader| {
1459 svm_reader
1460 .perf_samples
1461 .iter()
1462 .take(limit)
1463 .cloned()
1464 .collect::<Vec<_>>()
1465 })
1466 .map_err(Into::into)
1467 }
1468
1469 fn get_signature_statuses(
1470 &self,
1471 meta: Self::Metadata,
1472 signature_strs: Vec<String>,
1473 _config: Option<RpcSignatureStatusConfig>,
1474 ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>> {
1475 let signatures = match signature_strs
1476 .iter()
1477 .map(|s| {
1478 Signature::from_str(s)
1479 .map_err(|e| SurfpoolError::invalid_signature(s, e.to_string()))
1480 })
1481 .collect::<std::result::Result<Vec<Signature>, SurfpoolError>>()
1482 {
1483 Ok(sigs) => sigs,
1484 Err(e) => return e.into(),
1485 };
1486
1487 let SurfnetRpcContext {
1488 svm_locker,
1489 remote_ctx,
1490 } = match meta.get_rpc_context(()) {
1491 Ok(res) => res,
1492 Err(e) => return e.into(),
1493 };
1494 let remote_client = remote_ctx.map(|(r, _)| r);
1495
1496 Box::pin(async move {
1497 let context_slot = svm_locker.get_latest_absolute_slot();
1500
1501 let mut responses = Vec::with_capacity(signatures.len());
1502 for signature in signatures.into_iter() {
1503 let res = svm_locker
1504 .get_transaction(&remote_client, &signature, get_default_transaction_config())
1505 .await?;
1506
1507 let mut status = res.map_some_transaction_status();
1508 if let Some(confirmation_status) =
1509 status.as_ref().and_then(|s| s.confirmation_status.as_ref())
1510 {
1511 if confirmation_status.eq(&TransactionConfirmationStatus::Processed) {
1512 status = None;
1516 }
1517 }
1518 responses.push(status);
1519 }
1520 Ok(RpcResponse {
1521 context: RpcResponseContext::new(context_slot),
1522 value: responses,
1523 })
1524 })
1525 }
1526
1527 fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1528 meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1529 .map_err(Into::into)
1530 }
1531
1532 fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1533 meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1534 .map_err(Into::into)
1535 }
1536
1537 fn request_airdrop(
1538 &self,
1539 meta: Self::Metadata,
1540 pubkey_str: String,
1541 lamports: u64,
1542 _config: Option<RpcRequestAirdropConfig>,
1543 ) -> Result<String> {
1544 let pubkey = verify_pubkey(&pubkey_str)?;
1545 let Some(ctx) = meta else {
1546 return Err(SurfpoolError::missing_context().into());
1547 };
1548 let svm_locker = ctx.svm_locker;
1549 let res = svm_locker
1550 .airdrop(&pubkey, lamports)?
1551 .map_err(|err| Error::invalid_params(format!("failed to send transaction: {err:?}")))?;
1552 let _ = ctx
1553 .simnet_commands_tx
1554 .try_send(SimnetCommand::AirdropProcessed);
1555
1556 Ok(res.signature.to_string())
1557 }
1558
1559 fn send_transaction(
1560 &self,
1561 meta: Self::Metadata,
1562 data: String,
1563 config: Option<SurfpoolRpcSendTransactionConfig>,
1564 ) -> Result<String> {
1565 let config = config.unwrap_or_default();
1566 let tx_encoding = config
1567 .base
1568 .encoding
1569 .unwrap_or(UiTransactionEncoding::Base58);
1570 let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
1571 Error::invalid_params(format!(
1572 "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
1573 ))
1574 })?;
1575 let (_, unsanitized_tx) =
1576 decode_and_deserialize::<VersionedTransaction>(data, binary_encoding)?;
1577 let signatures = unsanitized_tx.signatures.clone();
1578 let signature = signatures[0];
1579 let tx_message = unsanitized_tx.message.clone();
1581 let Some(ctx) = meta else {
1582 return Err(RpcCustomError::NodeUnhealthy {
1583 num_slots_behind: None,
1584 }
1585 .into());
1586 };
1587
1588 let (status_update_tx, status_update_rx) = crossbeam_channel::bounded(1);
1589 ctx.simnet_commands_tx
1590 .send(SimnetCommand::ProcessTransaction(
1591 ctx.id,
1592 unsanitized_tx,
1593 status_update_tx,
1594 config.base.skip_preflight,
1595 config.skip_sig_verify,
1596 ))
1597 .map_err(|_| RpcCustomError::NodeUnhealthy {
1598 num_slots_behind: None,
1599 })?;
1600
1601 match status_update_rx.recv() {
1602 Ok(TransactionStatusEvent::SimulationFailure((error, metadata))) => {
1603 return Err(Error {
1604 data: Some(
1605 serde_json::to_value(get_simulate_transaction_result(
1606 surfpool_tx_metadata_to_litesvm_tx_metadata(&metadata),
1607 None,
1608 Some(error.clone()),
1609 None,
1610 false,
1611 &tx_message,
1612 None, None,
1614 ))
1615 .map_err(|e| {
1616 Error::invalid_params(format!(
1617 "Failed to serialize simulation result: {e}"
1618 ))
1619 })?,
1620 ),
1621 message: format!(
1622 "Transaction simulation failed: {}{}",
1623 error,
1624 if metadata.logs.is_empty() {
1625 String::new()
1626 } else {
1627 format!(
1628 ": {} log messages:\n{}",
1629 metadata.logs.len(),
1630 metadata.logs.iter().map(|l| l.to_string()).join("\n")
1631 )
1632 }
1633 ),
1634 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1635 });
1636 }
1637 Ok(TransactionStatusEvent::ExecutionFailure(_)) => {}
1638 Ok(TransactionStatusEvent::VerificationFailure(signature)) => {
1639 return Err(Error {
1640 data: None,
1641 message: format!("Transaction verification failed for transaction {signature}"),
1642 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1643 });
1644 }
1645 Err(e) => {
1646 return Err(Error {
1647 data: None,
1648 message: format!("Failed to process transaction: {e}"),
1649 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1650 });
1651 }
1652 Ok(TransactionStatusEvent::Success(_)) => {}
1653 }
1654 Ok(signature.to_string())
1655 }
1656
1657 fn simulate_transaction(
1658 &self,
1659 meta: Self::Metadata,
1660 data: String,
1661 config: Option<RpcSimulateTransactionConfig>,
1662 ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>> {
1663 let config = config.unwrap_or_default();
1664
1665 if config.sig_verify && config.replace_recent_blockhash {
1666 return SurfpoolError::sig_verify_replace_recent_blockhash_collision().into();
1667 }
1668
1669 let tx_encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
1670 let binary_encoding = match tx_encoding.into_binary_encoding() {
1671 Some(binary_encoding) => binary_encoding,
1672 None => {
1673 return Box::pin(async move {
1674 Err(Error::invalid_params(format!(
1675 "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
1676 )))
1677 });
1678 }
1679 };
1680 let (_, mut unsanitized_tx) =
1681 match decode_and_deserialize::<VersionedTransaction>(data, binary_encoding) {
1682 Ok(res) => res,
1683 Err(e) => return Box::pin(async move { Err(e) }),
1684 };
1685
1686 let SurfnetRpcContext {
1687 svm_locker,
1688 remote_ctx,
1689 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1690 Ok(res) => res,
1691 Err(e) => return e.into(),
1692 };
1693
1694 Box::pin(async move {
1695 let loaded_addresses = svm_locker
1696 .get_loaded_addresses(&remote_ctx, &unsanitized_tx.message)
1697 .await?;
1698 let transaction_pubkeys = svm_locker.get_pubkeys_from_message(
1699 &unsanitized_tx.message,
1700 loaded_addresses.as_ref().map(|l| l.all_loaded_addresses()),
1701 );
1702
1703 let SvmAccessContext {
1704 slot,
1705 inner: account_updates,
1706 latest_blockhash,
1707 latest_epoch_info,
1708 } = svm_locker
1709 .get_multiple_accounts(&remote_ctx, &transaction_pubkeys, None)
1710 .await?;
1711
1712 let mut seen_accounts = std::collections::HashSet::new();
1713 let mut loaded_accounts_data_size: u64 = 0;
1714
1715 let mut track_accounts_data_size =
1716 |account_update: &GetAccountResult| match account_update {
1717 GetAccountResult::FoundAccount(pubkey, account, _) => {
1718 if seen_accounts.insert(*pubkey) {
1719 loaded_accounts_data_size += account.data.len() as u64;
1720 }
1721 }
1722 GetAccountResult::FoundProgramAccount(
1724 (pubkey, account),
1725 (pd_pubkey, pd_account),
1726 ) => {
1727 if seen_accounts.insert(*pubkey) {
1728 loaded_accounts_data_size += account.data.len() as u64;
1729 }
1730 if let Some(pd) = pd_account {
1731 if seen_accounts.insert(*pd_pubkey) {
1732 loaded_accounts_data_size += pd.data.len() as u64;
1733 }
1734 }
1735 }
1736 GetAccountResult::FoundTokenAccount(
1737 (pubkey, account),
1738 (td_pubkey, td_account),
1739 ) => {
1740 if seen_accounts.insert(*pubkey) {
1741 loaded_accounts_data_size += account.data.len() as u64;
1742 }
1743 if let Some(td) = td_account {
1744 let td_key_in_tx_pubkeys =
1745 transaction_pubkeys.iter().find(|k| **k == *td_pubkey);
1746 if td_key_in_tx_pubkeys.is_some() && seen_accounts.insert(*td_pubkey) {
1748 loaded_accounts_data_size += td.data.len() as u64;
1749 }
1750 }
1751 }
1752 GetAccountResult::None(_) => {}
1753 };
1754
1755 for res in account_updates.iter() {
1756 track_accounts_data_size(res);
1757 }
1758
1759 svm_locker.write_multiple_account_updates(&account_updates);
1760
1761 let loaded_addresses_data = loaded_addresses.as_ref().map(|la| la.loaded_addresses());
1763
1764 if let Some(alt_pubkeys) = loaded_addresses.map(|l| l.alt_addresses()) {
1765 let alt_updates = svm_locker
1766 .get_multiple_accounts(&remote_ctx, &alt_pubkeys, None)
1767 .await?
1768 .inner;
1769 for res in alt_updates.iter() {
1770 track_accounts_data_size(res);
1771 }
1772 svm_locker.write_multiple_account_updates(&alt_updates);
1773 }
1774
1775 let replacement_blockhash = if config.replace_recent_blockhash {
1776 match &mut unsanitized_tx.message {
1777 VersionedMessage::Legacy(message) => {
1778 message.recent_blockhash = latest_blockhash
1779 }
1780 VersionedMessage::V0(message) => message.recent_blockhash = latest_blockhash,
1781 }
1782 Some(RpcBlockhash {
1783 blockhash: latest_blockhash.to_string(),
1784 last_valid_block_height: latest_epoch_info.block_height,
1785 })
1786 } else {
1787 None
1788 };
1789
1790 let tx_message = unsanitized_tx.message.clone();
1792
1793 let value = match svm_locker.simulate_transaction(unsanitized_tx, config.sig_verify) {
1794 Ok(tx_info) => {
1795 let mut accounts = None;
1796 if let Some(observed_accounts) = config.accounts {
1797 let mut ui_accounts = vec![];
1798 for observed_pubkey in observed_accounts.addresses.iter() {
1799 let mut ui_account = None;
1800 for (updated_pubkey, account) in tx_info.post_accounts.iter() {
1801 if observed_pubkey.eq(&updated_pubkey.to_string()) {
1802 ui_account = Some(
1803 svm_locker
1804 .account_to_rpc_keyed_account(
1805 updated_pubkey,
1806 account,
1807 &RpcAccountInfoConfig::default(),
1808 None,
1809 )
1810 .account,
1811 );
1812 }
1813 }
1814 ui_accounts.push(ui_account);
1815 }
1816 accounts = Some(ui_accounts);
1817 }
1818 get_simulate_transaction_result(
1819 tx_info.meta,
1820 accounts,
1821 None,
1822 replacement_blockhash,
1823 config.inner_instructions,
1824 &tx_message,
1825 loaded_addresses_data.as_ref(),
1826 Some(loaded_accounts_data_size as u32),
1827 )
1828 }
1829 Err(tx_info) => get_simulate_transaction_result(
1830 tx_info.meta,
1831 None,
1832 Some(tx_info.err),
1833 replacement_blockhash,
1834 config.inner_instructions,
1835 &tx_message,
1836 loaded_addresses_data.as_ref(),
1837 Some(loaded_accounts_data_size as u32),
1838 ),
1839 };
1840
1841 Ok(RpcResponse {
1842 context: RpcResponseContext::new(slot),
1843 value,
1844 })
1845 })
1846 }
1847
1848 fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>> {
1849 let SurfnetRpcContext {
1850 svm_locker,
1851 remote_ctx,
1852 } = match meta.get_rpc_context(()) {
1853 Ok(res) => res,
1854 Err(e) => return e.into(),
1855 };
1856
1857 Box::pin(async move {
1858 if let Some((remote_client, _)) = remote_ctx {
1861 remote_client
1862 .client
1863 .minimum_ledger_slot()
1864 .await
1865 .map_err(|e| SurfpoolError::client_error(e).into())
1866 } else {
1867 Ok(svm_locker.with_svm_reader(|svm| svm.genesis_slot))
1868 }
1869 })
1870 }
1871
1872 fn get_block(
1873 &self,
1874 meta: Self::Metadata,
1875 slot: Slot,
1876 config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
1877 ) -> BoxFuture<Result<Option<UiConfirmedBlock>>> {
1878 let config = config.map(|c| c.convert_to_current()).unwrap_or_default();
1879
1880 let SurfnetRpcContext {
1881 svm_locker,
1882 remote_ctx,
1883 } = match meta.get_rpc_context(config.commitment) {
1884 Ok(res) => res,
1885 Err(e) => return e.into(),
1886 };
1887
1888 Box::pin(async move {
1889 let remote_client = remote_ctx.as_ref().map(|(client, _)| client.clone());
1890 let result = svm_locker.get_block(&remote_client, &slot, &config).await;
1891 Ok(result?.inner)
1892 })
1893 }
1894
1895 fn get_block_time(
1896 &self,
1897 meta: Self::Metadata,
1898 slot: Slot,
1899 ) -> BoxFuture<Result<Option<UnixTimestamp>>> {
1900 let svm_locker = match meta.get_svm_locker() {
1901 Ok(locker) => locker,
1902 Err(e) => return e.into(),
1903 };
1904
1905 Box::pin(async move {
1906 let block_time = svm_locker.with_svm_reader(|svm_reader| {
1907 Ok::<_, jsonrpc_core::Error>(match svm_reader.blocks.get(&slot)? {
1908 Some(block) => Some(block.block_time),
1909 None => {
1910 if svm_reader.is_slot_in_valid_range(slot) {
1912 let time_ms = svm_reader.calculate_block_time_for_slot(slot);
1913 Some((time_ms / 1_000) as i64)
1914 } else {
1915 None
1916 }
1917 }
1918 })
1919 })?;
1920 Ok(block_time)
1921 })
1922 }
1923
1924 fn get_blocks(
1925 &self,
1926 meta: Self::Metadata,
1927 start_slot: Slot,
1928 wrapper: Option<RpcBlocksConfigWrapper>,
1929 config: Option<RpcContextConfig>,
1930 ) -> BoxFuture<Result<Vec<Slot>>> {
1931 let end_slot = match wrapper {
1932 Some(RpcBlocksConfigWrapper::EndSlotOnly(end_slot)) => end_slot,
1933 Some(RpcBlocksConfigWrapper::ConfigOnly(_)) => None,
1934 None => None,
1935 };
1936
1937 let config = config.unwrap_or_default();
1938 let commitment = config.commitment.unwrap_or(CommitmentConfig {
1940 commitment: CommitmentLevel::Processed,
1941 });
1942
1943 const MAX_SLOT_RANGE: u64 = 500_000;
1944 if let Some(end) = end_slot {
1945 if end < start_slot {
1946 return Box::pin(async { Ok(vec![]) });
1948 }
1949 if end.saturating_sub(start_slot) > MAX_SLOT_RANGE {
1950 return Box::pin(async move {
1951 Err(Error::invalid_params(format!(
1952 "Slot range too large. Maximum: {}, Requested: {}",
1953 MAX_SLOT_RANGE,
1954 end.saturating_sub(start_slot)
1955 )))
1956 });
1957 }
1958 }
1959
1960 let SurfnetRpcContext {
1961 svm_locker,
1962 remote_ctx,
1963 } = match meta.get_rpc_context(commitment) {
1964 Ok(res) => res,
1965 Err(e) => return e.into(),
1966 };
1967
1968 Box::pin(async move {
1969 let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
1970 let effective_end_slot = end_slot
1971 .map(|end| end.min(committed_latest_slot))
1972 .unwrap_or(committed_latest_slot);
1973
1974 let genesis_slot = svm_locker.with_svm_reader(|svm| svm.genesis_slot);
1975
1976 let (local_min_slot, local_slots, effective_end_slot) = if effective_end_slot
1977 < start_slot
1978 {
1979 (None, vec![], effective_end_slot)
1980 } else {
1981 let local_min_slot = Some(genesis_slot);
1984 let local_slots: Vec<Slot> = (start_slot.max(genesis_slot)..=effective_end_slot)
1985 .filter(|slot| *slot <= committed_latest_slot)
1986 .collect();
1987
1988 (local_min_slot, local_slots, effective_end_slot)
1989 };
1990
1991 if let Some(min_context_slot) = config.min_context_slot {
1992 if committed_latest_slot < min_context_slot {
1993 return Err(RpcCustomError::MinContextSlotNotReached {
1994 context_slot: min_context_slot,
1995 }
1996 .into());
1997 }
1998 }
1999
2000 if effective_end_slot.saturating_sub(start_slot) > MAX_SLOT_RANGE {
2001 return Err(Error::invalid_params(format!(
2002 "Slot range too large. Maximum: {}, Requested: {}",
2003 MAX_SLOT_RANGE,
2004 effective_end_slot.saturating_sub(start_slot)
2005 )));
2006 }
2007
2008 let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
2009 (&remote_ctx, local_min_slot)
2010 {
2011 if start_slot < local_min {
2012 let remote_end = effective_end_slot.min(local_min.saturating_sub(1));
2013 if start_slot <= remote_end {
2014 remote_client
2015 .client
2016 .get_blocks(start_slot, Some(remote_end))
2017 .await
2018 .unwrap_or_else(|_| vec![])
2019 } else {
2020 vec![]
2021 }
2022 } else {
2023 vec![]
2024 }
2025 } else if remote_ctx.is_some() && local_min_slot.is_none() {
2026 remote_ctx
2027 .as_ref()
2028 .unwrap()
2029 .0
2030 .client
2031 .get_blocks(start_slot, Some(effective_end_slot))
2032 .await
2033 .unwrap_or_else(|_| vec![])
2034 } else {
2035 vec![]
2036 };
2037
2038 let mut combined_slots = remote_slots;
2040 combined_slots.extend(local_slots);
2041 combined_slots.sort_unstable();
2042 combined_slots.dedup();
2043
2044 if combined_slots.len() > MAX_SLOT_RANGE as usize {
2045 combined_slots.truncate(MAX_SLOT_RANGE as usize);
2046 }
2047
2048 Ok(combined_slots)
2049 })
2050 }
2051
2052 fn get_blocks_with_limit(
2053 &self,
2054 meta: Self::Metadata,
2055 start_slot: Slot,
2056 limit: usize,
2057 config: Option<RpcContextConfig>,
2058 ) -> BoxFuture<Result<Vec<Slot>>> {
2059 let config = config.unwrap_or_default();
2060 let commitment = config.commitment.unwrap_or(CommitmentConfig {
2061 commitment: CommitmentLevel::Processed,
2062 });
2063
2064 if limit == 0 {
2065 return Box::pin(
2066 async move { Err(Error::invalid_params("Limit must be greater than 0")) },
2067 );
2068 }
2069
2070 const MAX_LIMIT: usize = 500_000;
2071 if limit > MAX_LIMIT {
2072 return Box::pin(async move {
2073 Err(Error::invalid_params(format!(
2074 "Limit too large. Maximum limit allowed: {}",
2075 MAX_LIMIT
2076 )))
2077 });
2078 }
2079
2080 let SurfnetRpcContext {
2081 svm_locker,
2082 remote_ctx,
2083 } = match meta.get_rpc_context(commitment) {
2084 Ok(res) => res,
2085 Err(e) => return e.into(),
2086 };
2087
2088 Box::pin(async move {
2089 let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
2090 let genesis_slot = svm_locker.with_svm_reader(|svm| svm.genesis_slot);
2091
2092 let local_min_slot = Some(genesis_slot);
2095 let local_slots: Vec<Slot> =
2096 (start_slot.max(genesis_slot)..=committed_latest_slot).collect();
2097
2098 if let Some(min_context_slot) = config.min_context_slot {
2099 if committed_latest_slot < min_context_slot {
2100 return Err(RpcCustomError::MinContextSlotNotReached {
2101 context_slot: min_context_slot,
2102 }
2103 .into());
2104 }
2105 }
2106
2107 let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
2109 (&remote_ctx, local_min_slot)
2110 {
2111 if start_slot < local_min {
2112 let remote_end = committed_latest_slot.min(local_min.saturating_sub(1));
2113 if start_slot <= remote_end {
2114 remote_client
2115 .client
2116 .get_blocks(start_slot, Some(remote_end))
2117 .await
2118 .unwrap_or_else(|_| vec![])
2119 } else {
2120 vec![]
2121 }
2122 } else {
2123 vec![]
2124 }
2125 } else if remote_ctx.is_some() && local_min_slot.is_none() {
2126 remote_ctx
2128 .as_ref()
2129 .unwrap()
2130 .0
2131 .client
2132 .get_blocks(start_slot, Some(committed_latest_slot))
2133 .await
2134 .unwrap_or_else(|_| vec![])
2135 } else {
2136 vec![]
2137 };
2138
2139 let mut combined_slots = remote_slots;
2140 combined_slots.extend(local_slots);
2141 combined_slots.sort_unstable();
2142 combined_slots.dedup();
2143
2144 combined_slots.truncate(limit);
2146
2147 Ok(combined_slots)
2148 })
2149 }
2150
2151 fn get_transaction(
2152 &self,
2153 meta: Self::Metadata,
2154 signature_str: String,
2155 config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
2156 ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>> {
2157 let mut config = config.map(|c| c.convert_to_current()).unwrap_or_default();
2158 adjust_default_transaction_config(&mut config);
2159
2160 Box::pin(async move {
2161 let signature = Signature::from_str(&signature_str)
2162 .map_err(|e| SurfpoolError::invalid_signature(&signature_str, e.to_string()))?;
2163
2164 let SurfnetRpcContext {
2165 svm_locker,
2166 remote_ctx,
2167 } = meta.get_rpc_context(())?;
2168
2169 match svm_locker
2172 .get_transaction(&remote_ctx.map(|(r, _)| r), &signature, config)
2173 .await?
2174 {
2175 GetTransactionResult::None(_) => Ok(None),
2176 GetTransactionResult::FoundTransaction(_, meta, _) => Ok(Some(meta)),
2177 }
2178 })
2179 }
2180
2181 fn get_signatures_for_address(
2182 &self,
2183 meta: Self::Metadata,
2184 address: String,
2185 config: Option<RpcSignaturesForAddressConfig>,
2186 ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>> {
2187 let pubkey = match verify_pubkey(&address) {
2188 Ok(s) => s,
2189 Err(e) => return e.into(),
2190 };
2191 let SurfnetRpcContext {
2192 svm_locker,
2193 remote_ctx,
2194 } = match meta.get_rpc_context(()) {
2195 Ok(res) => res,
2196 Err(e) => return e.into(),
2197 };
2198
2199 Box::pin(async move {
2200 let signatures = svm_locker
2201 .get_signatures_for_address(&remote_ctx, &pubkey, config)
2202 .await?
2203 .inner;
2204 Ok(signatures)
2205 })
2206 }
2207
2208 fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot> {
2209 meta.with_svm_reader(|svm_reader| {
2210 Ok::<_, jsonrpc_core::Error>(
2211 svm_reader
2212 .blocks
2213 .keys()?
2214 .into_iter()
2215 .min()
2216 .unwrap_or_default(),
2217 )
2218 })?
2219 .map_err(Into::into)
2220 }
2221
2222 fn get_latest_blockhash(
2223 &self,
2224 meta: Self::Metadata,
2225 config: Option<RpcContextConfig>,
2226 ) -> Result<RpcResponse<RpcBlockhash>> {
2227 let svm_locker = meta.get_svm_locker()?;
2228
2229 let config = config.unwrap_or_default();
2230 let commitment = config.commitment.unwrap_or_default();
2231
2232 let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
2233 if let Some(min_context_slot) = config.min_context_slot {
2234 if committed_latest_slot < min_context_slot {
2235 return Err(RpcCustomError::MinContextSlotNotReached {
2236 context_slot: min_context_slot,
2237 }
2238 .into());
2239 }
2240 }
2241
2242 let blockhash = svm_locker
2243 .get_latest_blockhash(&commitment)
2244 .unwrap_or_else(|| svm_locker.latest_absolute_blockhash());
2245
2246 let current_block_height = svm_locker.get_epoch_info().block_height;
2247 let last_valid_block_height = current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
2248 Ok(RpcResponse {
2249 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2250 value: RpcBlockhash {
2251 blockhash: blockhash.to_string(),
2252 last_valid_block_height,
2253 },
2254 })
2255 }
2256
2257 fn is_blockhash_valid(
2258 &self,
2259 meta: Self::Metadata,
2260 blockhash: String,
2261 config: Option<RpcContextConfig>,
2262 ) -> Result<RpcResponse<bool>> {
2263 let hash = blockhash
2264 .parse::<solana_hash::Hash>()
2265 .map_err(|e| Error::invalid_params(format!("Invalid blockhash: {e:?}")))?;
2266 let config = config.unwrap_or_default();
2267
2268 let svm_locker = meta.get_svm_locker()?;
2269
2270 let committed_latest_slot =
2271 svm_locker.get_slot_for_commitment(&config.commitment.unwrap_or_default());
2272
2273 let is_valid =
2274 svm_locker.with_svm_reader(|svm_reader| svm_reader.check_blockhash_is_recent(&hash));
2275
2276 if let Some(min_context_slot) = config.min_context_slot {
2277 if committed_latest_slot < min_context_slot {
2278 return Err(RpcCustomError::MinContextSlotNotReached {
2279 context_slot: min_context_slot,
2280 }
2281 .into());
2282 }
2283 }
2284
2285 Ok(RpcResponse {
2286 context: RpcResponseContext::new(committed_latest_slot),
2287 value: is_valid,
2288 })
2289 }
2290
2291 fn get_fee_for_message(
2292 &self,
2293 meta: Self::Metadata,
2294 encoded: String,
2295 config: Option<RpcContextConfig>,
2296 ) -> Result<RpcResponse<Option<u64>>> {
2297 let (_, message) =
2298 decode_and_deserialize::<VersionedMessage>(encoded, TransactionBinaryEncoding::Base64)?;
2299
2300 let RpcContextConfig {
2301 commitment,
2302 min_context_slot,
2303 } = config.unwrap_or_default();
2304 let min_ctx_slot = min_context_slot.unwrap_or_default();
2305
2306 let svm_locker = meta.get_svm_locker()?;
2307
2308 let slot = if let Some(commitment_config) = commitment {
2309 svm_locker.get_slot_for_commitment(&commitment_config)
2310 } else {
2311 svm_locker.get_latest_absolute_slot()
2312 };
2313
2314 if let Some(min_slot) = min_context_slot
2315 && slot < min_slot
2316 {
2317 return Err(RpcCustomError::MinContextSlotNotReached {
2318 context_slot: min_ctx_slot,
2319 }
2320 .into());
2321 }
2322
2323 Ok(RpcResponse {
2324 context: RpcResponseContext::new(slot),
2325 value: Some((message.header().num_required_signatures as u64) * 5000),
2326 })
2327 }
2328
2329 fn get_stake_minimum_delegation(
2330 &self,
2331 meta: Self::Metadata,
2332 config: Option<RpcContextConfig>,
2333 ) -> Result<RpcResponse<u64>> {
2334 let config = config.unwrap_or_default();
2335 let commitment_config = config.commitment.unwrap_or(CommitmentConfig {
2336 commitment: CommitmentLevel::Processed,
2337 });
2338
2339 meta.with_svm_reader(|svm_reader| {
2340 let context_slot = match commitment_config.commitment {
2341 CommitmentLevel::Processed => svm_reader.get_latest_absolute_slot(),
2342 CommitmentLevel::Confirmed => {
2343 svm_reader.get_latest_absolute_slot().saturating_sub(1)
2344 }
2345 CommitmentLevel::Finalized => svm_reader
2346 .get_latest_absolute_slot()
2347 .saturating_sub(FINALIZATION_SLOT_THRESHOLD),
2348 };
2349
2350 RpcResponse {
2351 context: RpcResponseContext::new(context_slot),
2352 value: 0,
2353 }
2354 })
2355 .map_err(Into::into)
2356 }
2357
2358 fn get_recent_prioritization_fees(
2359 &self,
2360 meta: Self::Metadata,
2361 pubkey_strs: Option<Vec<String>>,
2362 ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>> {
2363 let pubkeys_filter = match pubkey_strs
2364 .map(|strs| {
2365 strs.iter()
2366 .map(|s| verify_pubkey(s))
2367 .collect::<SurfpoolResult<Vec<_>>>()
2368 })
2369 .transpose()
2370 {
2371 Ok(pubkeys) => pubkeys,
2372 Err(e) => return e.into(),
2373 };
2374
2375 let SurfnetRpcContext {
2376 svm_locker,
2377 remote_ctx,
2378 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2379 Ok(res) => res,
2380 Err(e) => return e.into(),
2381 };
2382
2383 Box::pin(async move {
2384 let (blocks, transactions) = svm_locker.with_svm_reader(|svm_reader| {
2385 (svm_reader.blocks.clone(), svm_reader.transactions.clone())
2386 });
2387
2388 let recent_headers = blocks
2390 .into_iter()?
2391 .sorted_by_key(|(slot, _)| std::cmp::Reverse(*slot))
2392 .take(MAX_PRIORITIZATION_FEE_BLOCKS_CACHE)
2393 .collect::<Vec<_>>();
2394
2395 let recent_transactions = recent_headers
2397 .into_iter()
2398 .flat_map(|(slot, header)| {
2399 header
2400 .signatures
2401 .iter()
2402 .filter_map(|signature| {
2403 transactions
2405 .get(&signature.to_string())
2406 .ok()
2407 .flatten()
2408 .map(|tx| (slot, tx))
2409 })
2410 .collect::<Vec<_>>()
2411 })
2412 .collect::<Vec<_>>();
2413
2414 fn get_compute_unit_price(ix: CompiledInstruction, accounts: &[Pubkey]) -> Option<u64> {
2416 let program_account = accounts.get(ix.program_id_index as usize)?;
2417 if *program_account != compute_budget::id() {
2418 return None;
2419 }
2420
2421 if let Ok(ComputeBudgetInstruction::SetComputeUnitPrice(price)) =
2422 borsh::from_slice::<ComputeBudgetInstruction>(&ix.data)
2423 {
2424 return Some(price);
2425 }
2426
2427 None
2428 }
2429
2430 let mut prioritization_fees = vec![];
2431 for (slot, tx) in recent_transactions {
2432 match tx {
2433 SurfnetTransactionStatus::Received => {}
2434 SurfnetTransactionStatus::Processed(data) => {
2435 let (status_meta, _) = data.as_ref();
2436 let tx = &status_meta.transaction;
2437
2438 let loaded_addresses = svm_locker
2442 .get_loaded_addresses(&remote_ctx, &tx.message)
2443 .await?;
2444 let account_keys = svm_locker.get_pubkeys_from_message(
2445 &tx.message,
2446 loaded_addresses.as_ref().map(|l| l.all_loaded_addresses()),
2447 );
2448
2449 let instructions = match &tx.message {
2450 VersionedMessage::V0(msg) => &msg.instructions,
2451 VersionedMessage::Legacy(msg) => &msg.instructions,
2452 };
2453
2454 let compute_unit_prices = instructions
2456 .iter()
2457 .filter_map(|ix| get_compute_unit_price(ix.clone(), &account_keys))
2458 .collect::<Vec<_>>();
2459
2460 for compute_unit_price in compute_unit_prices {
2461 if let Some(pubkeys_filter) = &pubkeys_filter {
2462 if !pubkeys_filter
2465 .iter()
2466 .any(|pk| account_keys.iter().any(|a| a == pk))
2467 {
2468 continue;
2469 }
2470 }
2471 prioritization_fees.push(RpcPrioritizationFee {
2473 slot,
2474 prioritization_fee: compute_unit_price,
2475 });
2476 }
2477 }
2478 }
2479 }
2480 Ok(prioritization_fees)
2481 })
2482 }
2483}
2484
2485fn get_simulate_transaction_result(
2486 metadata: TransactionMetadata,
2487 accounts: Option<Vec<Option<UiAccount>>>,
2488 error: Option<TransactionError>,
2489 replacement_blockhash: Option<RpcBlockhash>,
2490 include_inner_instructions: bool,
2491 message: &VersionedMessage,
2492 loaded_addresses: Option<&solana_message::v0::LoadedAddresses>,
2493 loaded_accounts_data_size: Option<u32>,
2494) -> RpcSimulateTransactionResult {
2495 RpcSimulateTransactionResult {
2496 accounts,
2497 err: error.map(|e| e.into()),
2498 inner_instructions: if include_inner_instructions {
2499 Some(transform_tx_metadata_to_ui_accounts(
2500 metadata.clone(),
2501 message,
2502 loaded_addresses,
2503 ))
2504 } else {
2505 None
2506 },
2507 logs: Some(metadata.logs.clone()),
2508 replacement_blockhash,
2509 return_data: if metadata.return_data.program_id == system_program::id()
2510 && metadata.return_data.data.is_empty()
2511 {
2512 None
2513 } else {
2514 Some(metadata.return_data.clone().into())
2515 },
2516 units_consumed: Some(metadata.compute_units_consumed),
2517 loaded_accounts_data_size,
2518 fee: None,
2519 pre_balances: None,
2520 post_balances: None,
2521 pre_token_balances: None,
2522 post_token_balances: None,
2523 loaded_addresses: None,
2524 }
2525}
2526
2527#[cfg(test)]
2528mod tests {
2529 pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
2530
2531 use std::thread::JoinHandle;
2532
2533 use base64::{Engine, prelude::BASE64_STANDARD};
2534 use bincode::Options;
2535 use crossbeam_channel::Receiver;
2536 use solana_account_decoder::{UiAccount, UiAccountData, UiAccountEncoding};
2537 use solana_client::rpc_config::RpcSimulateTransactionAccountsConfig;
2538 use solana_commitment_config::CommitmentConfig;
2539 use solana_hash::Hash;
2540 use solana_instruction::Instruction;
2541 use solana_keypair::Keypair;
2542 use solana_message::{
2543 MessageHeader, legacy::Message as LegacyMessage, v0::Message as V0Message,
2544 };
2545 use solana_pubkey::Pubkey;
2546 use solana_signer::Signer;
2547 use solana_system_interface::{
2548 instruction::{self as system_instruction, transfer},
2549 program as system_program,
2550 };
2551 use solana_transaction::{
2552 Transaction,
2553 versioned::{Legacy, TransactionVersion},
2554 };
2555 use solana_transaction_error::TransactionError;
2556 use solana_transaction_status::{
2557 EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiMessage,
2558 UiRawMessage, UiTransaction,
2559 };
2560 use surfpool_types::{SimnetCommand, TransactionConfirmationStatus};
2561 use test_case::test_case;
2562
2563 use super::*;
2564 use crate::{
2565 surfnet::{BlockHeader, BlockIdentifier, remote::SurfnetRemoteClient},
2566 tests::helpers::TestSetup,
2567 types::{SyntheticBlockhash, TransactionWithStatusMeta},
2568 };
2569
2570 fn build_v0_transaction(
2571 payer: &Pubkey,
2572 signers: &[&Keypair],
2573 instructions: &[Instruction],
2574 recent_blockhash: &Hash,
2575 ) -> VersionedTransaction {
2576 let msg = VersionedMessage::V0(
2577 V0Message::try_compile(&payer, instructions, &[], *recent_blockhash).unwrap(),
2578 );
2579 VersionedTransaction::try_new(msg, signers).unwrap()
2580 }
2581
2582 fn build_legacy_transaction(
2583 payer: &Pubkey,
2584 signers: &[&Keypair],
2585 instructions: &[Instruction],
2586 recent_blockhash: &Hash,
2587 ) -> VersionedTransaction {
2588 let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
2589 instructions,
2590 Some(payer),
2591 recent_blockhash,
2592 ));
2593 VersionedTransaction::try_new(msg, signers).unwrap()
2594 }
2595
2596 async fn send_and_await_transaction(
2597 tx: VersionedTransaction,
2598 setup: TestSetup<SurfpoolFullRpc>,
2599 mempool_rx: Receiver<SimnetCommand>,
2600 ) -> JoinHandle<String> {
2601 let setup_clone = setup.clone();
2602 let handle = hiro_system_kit::thread_named("send_tx")
2603 .spawn(move || {
2604 let res = setup_clone
2605 .rpc
2606 .send_transaction(
2607 Some(setup_clone.context),
2608 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2609 None,
2610 )
2611 .unwrap();
2612
2613 res
2614 })
2615 .unwrap();
2616 loop {
2617 match mempool_rx.recv() {
2618 Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
2619 let mut writer = setup.context.svm_locker.0.write().await;
2620 let slot = writer.get_latest_absolute_slot();
2621 writer.transactions_queued_for_confirmation.push_back((
2622 tx.clone(),
2623 status_tx.clone(),
2624 None,
2625 ));
2626 let sig = tx.signatures[0];
2627 let tx_with_status_meta = TransactionWithStatusMeta {
2628 slot,
2629 transaction: tx,
2630 ..Default::default()
2631 };
2632 let mutated_accounts = std::collections::HashSet::new();
2633 writer
2634 .transactions
2635 .store(
2636 sig.to_string(),
2637 SurfnetTransactionStatus::processed(
2638 tx_with_status_meta,
2639 mutated_accounts,
2640 ),
2641 )
2642 .unwrap();
2643 status_tx
2644 .send(TransactionStatusEvent::Success(
2645 TransactionConfirmationStatus::Confirmed,
2646 ))
2647 .unwrap();
2648 break;
2649 }
2650 Ok(SimnetCommand::AirdropProcessed) => continue,
2651 _ => panic!("failed to receive transaction from mempool"),
2652 }
2653 }
2654
2655 handle
2656 }
2657
2658 #[test_case(None, false ; "when limit is None")]
2659 #[test_case(Some(1), false ; "when limit is ok")]
2660 #[test_case(Some(1000), true ; "when limit is above max spec")]
2661 fn test_get_recent_performance_samples(limit: Option<usize>, fails: bool) {
2662 let setup = TestSetup::new(SurfpoolFullRpc);
2663 let res = setup
2664 .rpc
2665 .get_recent_performance_samples(Some(setup.context), limit);
2666
2667 if fails {
2668 assert!(res.is_err());
2669 } else {
2670 assert!(res.is_ok());
2671 }
2672 }
2673
2674 #[tokio::test(flavor = "multi_thread")]
2675 async fn test_get_fee_for_message() {
2676 let setup = TestSetup::new(SurfpoolFullRpc);
2677 let runloop_context = setup.context;
2678 let rpc_server = setup.rpc;
2679 let payer = Keypair::new();
2680 let recipient = Pubkey::new_unique();
2681 let lamports_to_send = 5 * LAMPORTS_PER_SOL;
2682 let commitment_config_to_use = CommitmentConfig::confirmed();
2683
2684 let wrong_comm_min_ctx_slot = runloop_context
2685 .svm_locker
2686 .get_slot_for_commitment(&commitment_config_to_use)
2687 + 10;
2688
2689 let wrong_min_slot = runloop_context.svm_locker.get_latest_absolute_slot() + 10;
2690 let rpc_ctx_config_with_wrong_commitment = RpcContextConfig {
2691 commitment: Some(commitment_config_to_use),
2692 min_context_slot: Some(wrong_comm_min_ctx_slot),
2693 };
2694 let rpc_ctx_config_with_wrong_min_slot = RpcContextConfig {
2695 commitment: None,
2696 min_context_slot: Some(wrong_min_slot),
2697 };
2698
2699 let instruction = transfer(&payer.pubkey(), &recipient, lamports_to_send);
2700
2701 let latest_blockhash = runloop_context
2702 .svm_locker
2703 .with_svm_reader(|svm| svm.latest_blockhash());
2704 let message = solana_message::Message::new_with_blockhash(
2705 &[instruction],
2706 Some(&payer.pubkey()),
2707 &latest_blockhash,
2708 );
2709 let num_required_signatures = message.header.num_required_signatures as u64;
2710 let transaction =
2711 VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
2712
2713 let message_bytes = bincode::options()
2714 .with_fixint_encoding()
2715 .serialize(&transaction.message)
2716 .expect("message serialization");
2717 let encoded_message = base64::engine::general_purpose::STANDARD.encode(&message_bytes);
2718
2719 let get_fee_with_correct_config_pass_result = rpc_server.get_fee_for_message(
2720 Some(runloop_context.clone()),
2721 encoded_message.clone(),
2722 None,
2723 );
2724
2725 assert!(
2726 get_fee_with_correct_config_pass_result.is_ok(),
2727 "Expected get_fee_for_message to pass with correct configs"
2728 );
2729 assert_eq!(
2730 get_fee_with_correct_config_pass_result
2731 .unwrap()
2732 .value
2733 .unwrap(),
2734 (num_required_signatures as u64) * 5_000,
2735 "Invalid return value"
2736 );
2737
2738 let get_fee_with_wrong_commitment_fail_result = rpc_server.get_fee_for_message(
2739 Some(runloop_context.clone()),
2740 encoded_message.clone(),
2741 Some(rpc_ctx_config_with_wrong_commitment),
2742 );
2743
2744 let wrong_comm_expected_err: Result<()> = Result::Err(
2745 RpcCustomError::MinContextSlotNotReached {
2746 context_slot: wrong_comm_min_ctx_slot,
2747 }
2748 .into(),
2749 );
2750
2751 assert!(
2752 get_fee_with_wrong_commitment_fail_result.is_err(),
2753 "expected this txn to fail when min_ctx_slot > slot_for_commitment"
2754 );
2755
2756 assert_eq!(
2757 get_fee_with_wrong_commitment_fail_result.err().unwrap(),
2758 wrong_comm_expected_err.err().unwrap()
2759 );
2760
2761 let get_fee_with_wrong_mint_slot_fail_result = rpc_server.get_fee_for_message(
2762 Some(runloop_context.clone()),
2763 encoded_message,
2764 Some(rpc_ctx_config_with_wrong_min_slot),
2765 );
2766
2767 let wrong_min_slot_expected_err: Result<()> = Result::Err(
2768 RpcCustomError::MinContextSlotNotReached {
2769 context_slot: wrong_min_slot,
2770 }
2771 .into(),
2772 );
2773 assert!(
2774 get_fee_with_wrong_mint_slot_fail_result.is_err(),
2775 "expected this txn to fail when min_ctx_slot > absolute_latest_slot"
2776 );
2777 assert_eq!(
2778 get_fee_with_wrong_mint_slot_fail_result.err().unwrap(),
2779 wrong_min_slot_expected_err.err().unwrap()
2780 );
2781 }
2782
2783 #[tokio::test(flavor = "multi_thread")]
2784 async fn test_get_signature_statuses() {
2785 let pks = (0..10).map(|_| Pubkey::new_unique());
2786 let valid_txs = pks.len();
2787 let invalid_txs = pks.len();
2788 let payer = Keypair::new();
2789 let mut setup = TestSetup::new(SurfpoolFullRpc).without_blockhash().await;
2790 let recent_blockhash = setup
2791 .context
2792 .svm_locker
2793 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2794
2795 let valid = pks
2796 .clone()
2797 .map(|pk| {
2798 Transaction::new_signed_with_payer(
2799 &[system_instruction::transfer(
2800 &payer.pubkey(),
2801 &pk,
2802 LAMPORTS_PER_SOL,
2803 )],
2804 Some(&payer.pubkey()),
2805 &[payer.insecure_clone()],
2806 recent_blockhash,
2807 )
2808 })
2809 .collect::<Vec<_>>();
2810 let invalid = pks
2811 .map(|pk| {
2812 Transaction::new_unsigned(LegacyMessage::new(
2813 &[system_instruction::transfer(
2814 &pk,
2815 &payer.pubkey(),
2816 LAMPORTS_PER_SOL,
2817 )],
2818 Some(&payer.pubkey()),
2819 ))
2820 })
2821 .collect::<Vec<_>>();
2822 let txs = valid
2823 .into_iter()
2824 .chain(invalid.into_iter())
2825 .map(|tx| VersionedTransaction {
2826 signatures: tx.signatures,
2827 message: VersionedMessage::Legacy(tx.message),
2828 })
2829 .collect::<Vec<_>>();
2830 let _ = setup.context.svm_locker.0.write().await.airdrop(
2831 &payer.pubkey(),
2832 (valid_txs + invalid_txs) as u64 * 2 * LAMPORTS_PER_SOL,
2833 );
2834 setup.process_txs(txs.clone()).await;
2835
2836 let current_slot = setup
2838 .context
2839 .svm_locker
2840 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
2841
2842 {
2844 let res = setup
2845 .rpc
2846 .get_signature_statuses(
2847 Some(setup.context.clone()),
2848 txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2849 None,
2850 )
2851 .await
2852 .unwrap();
2853 assert_eq!(
2854 res.value.iter().flatten().collect::<Vec<_>>().len(),
2855 0,
2856 "processed transactions should not be returning values"
2857 );
2858 }
2859
2860 setup
2862 .context
2863 .svm_locker
2864 .confirm_current_block(&None)
2865 .await
2866 .unwrap();
2867 let res = setup
2868 .rpc
2869 .get_signature_statuses(
2870 Some(setup.context),
2871 txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2872 None,
2873 )
2874 .await
2875 .unwrap();
2876
2877 assert_eq!(
2879 res.context.slot,
2880 current_slot + 1,
2881 "Context slot should be captured at the beginning of the call, not after lookups"
2882 );
2883
2884 assert_eq!(
2885 res.value
2886 .iter()
2887 .filter(|status| {
2888 println!("status: {:?}", status);
2889 if let Some(s) = status {
2890 s.status.is_ok()
2891 } else {
2892 false
2893 }
2894 })
2895 .count(),
2896 valid_txs,
2897 "incorrect number of valid txs"
2898 );
2899 assert_eq!(
2900 res.value
2901 .iter()
2902 .filter(|status| if let Some(s) = status {
2903 s.status.is_err()
2904 } else {
2905 true
2906 })
2907 .count(),
2908 invalid_txs,
2909 "incorrect number of invalid txs"
2910 );
2911 }
2912
2913 #[test]
2914 fn test_request_airdrop() {
2915 let pk = Pubkey::new_unique();
2916 let lamports = 1000;
2917 let setup = TestSetup::new(SurfpoolFullRpc);
2918 let res = setup
2919 .rpc
2920 .request_airdrop(Some(setup.context.clone()), pk.to_string(), lamports, None)
2921 .unwrap();
2922 let sig = Signature::from_str(res.as_str()).unwrap();
2923 let state_reader = setup.context.svm_locker.0.blocking_read();
2924 assert_eq!(
2925 state_reader
2926 .inner
2927 .get_account(&pk)
2928 .unwrap()
2929 .unwrap()
2930 .lamports,
2931 lamports,
2932 "airdropped amount is incorrect"
2933 );
2934 assert!(
2935 state_reader.get_transaction(&sig).unwrap().is_some(),
2936 "transaction is not found in the SVM"
2937 );
2938 assert!(
2939 state_reader
2940 .transactions
2941 .get(&sig.to_string())
2942 .unwrap()
2943 .is_some(),
2944 "transaction is not found in the history"
2945 );
2946 }
2947
2948 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2949 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2950 #[tokio::test(flavor = "multi_thread")]
2951 async fn test_send_transaction(version: TransactionVersion) {
2952 let payer = Keypair::new();
2953 let pk = Pubkey::new_unique();
2954 let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
2955 let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
2956 let recent_blockhash = setup
2957 .context
2958 .svm_locker
2959 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2960
2961 let tx = match version {
2962 TransactionVersion::Legacy(_) => build_legacy_transaction(
2963 &payer.pubkey(),
2964 &[&payer.insecure_clone()],
2965 &[system_instruction::transfer(
2966 &payer.pubkey(),
2967 &pk,
2968 LAMPORTS_PER_SOL,
2969 )],
2970 &recent_blockhash,
2971 ),
2972 TransactionVersion::Number(0) => build_v0_transaction(
2973 &payer.pubkey(),
2974 &[&payer.insecure_clone()],
2975 &[system_instruction::transfer(
2976 &payer.pubkey(),
2977 &pk,
2978 LAMPORTS_PER_SOL,
2979 )],
2980 &recent_blockhash,
2981 ),
2982 _ => unimplemented!(),
2983 };
2984
2985 let _ = setup
2986 .context
2987 .svm_locker
2988 .0
2989 .write()
2990 .await
2991 .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
2992
2993 let handle = send_and_await_transaction(tx.clone(), setup.clone(), mempool_rx).await;
2994 assert_eq!(
2995 handle.join().unwrap(),
2996 tx.signatures[0].to_string(),
2997 "incorrect signature"
2998 );
2999 }
3000
3001 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3002 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3003 #[tokio::test(flavor = "multi_thread")]
3004 async fn test_simulate_transaction(version: TransactionVersion) {
3005 let payer = Keypair::new();
3006 let pk = Pubkey::new_unique();
3007 let lamports = LAMPORTS_PER_SOL;
3008 let setup = TestSetup::new(SurfpoolFullRpc);
3009 let recent_blockhash = setup
3010 .context
3011 .svm_locker
3012 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3013
3014 let _ = setup
3015 .rpc
3016 .request_airdrop(
3017 Some(setup.context.clone()),
3018 payer.pubkey().to_string(),
3019 2 * lamports,
3020 None,
3021 )
3022 .unwrap();
3023
3024 let tx = match version {
3025 TransactionVersion::Legacy(_) => build_legacy_transaction(
3026 &payer.pubkey(),
3027 &[&payer.insecure_clone()],
3028 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3029 &recent_blockhash,
3030 ),
3031 TransactionVersion::Number(0) => build_v0_transaction(
3032 &payer.pubkey(),
3033 &[&payer.insecure_clone()],
3034 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3035 &recent_blockhash,
3036 ),
3037 _ => unimplemented!(),
3038 };
3039
3040 let simulation_res = setup
3041 .rpc
3042 .simulate_transaction(
3043 Some(setup.context),
3044 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3045 Some(RpcSimulateTransactionConfig {
3046 sig_verify: true,
3047 replace_recent_blockhash: false,
3048 commitment: Some(CommitmentConfig::finalized()),
3049 encoding: None,
3050 accounts: Some(RpcSimulateTransactionAccountsConfig {
3051 encoding: None,
3052 addresses: vec![pk.to_string()],
3053 }),
3054 min_context_slot: None,
3055 inner_instructions: false,
3056 }),
3057 )
3058 .await
3059 .unwrap();
3060
3061 assert_eq!(
3062 simulation_res.value.err, None,
3063 "Unexpected simulation error"
3064 );
3065 assert_eq!(
3066 simulation_res.value.accounts,
3067 Some(vec![Some(UiAccount {
3068 lamports,
3069 data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
3070 owner: system_program::id().to_string(),
3071 executable: false,
3072 rent_epoch: 0,
3073 space: Some(0),
3074 })]),
3075 "Wrong account content"
3076 );
3077 }
3078
3079 #[tokio::test(flavor = "multi_thread")]
3080 async fn test_simulate_transaction_oversized_base64_returns_invalid_params() {
3081 let setup = TestSetup::new(SurfpoolFullRpc);
3082
3083 let err = setup
3084 .rpc
3085 .simulate_transaction(
3086 Some(setup.context),
3087 "A".repeat(1645),
3088 Some(RpcSimulateTransactionConfig {
3089 encoding: Some(UiTransactionEncoding::Base64),
3090 ..RpcSimulateTransactionConfig::default()
3091 }),
3092 )
3093 .await
3094 .unwrap_err();
3095
3096 assert_eq!(err.code, jsonrpc_core::ErrorCode::InvalidParams);
3097 assert!(
3098 err.message.contains("base64 encoded"),
3099 "expected base64 size validation error, got: {}",
3100 err.message
3101 );
3102 }
3103
3104 #[tokio::test(flavor = "multi_thread")]
3105 async fn test_simulate_transaction_no_signers() {
3106 let payer = Keypair::new();
3107 let pk = Pubkey::new_unique();
3108 let lamports = LAMPORTS_PER_SOL;
3109 let setup = TestSetup::new(SurfpoolFullRpc);
3110 setup
3111 .context
3112 .svm_locker
3113 .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
3114 let recent_blockhash = setup
3115 .context
3116 .svm_locker
3117 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3118
3119 let _ = setup
3120 .rpc
3121 .request_airdrop(
3122 Some(setup.context.clone()),
3123 payer.pubkey().to_string(),
3124 2 * lamports,
3125 None,
3126 )
3127 .unwrap();
3128 let mut msg = LegacyMessage::new(
3130 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3131 Some(&payer.pubkey()),
3132 );
3133 msg.recent_blockhash = recent_blockhash;
3134 let tx = Transaction::new_unsigned(msg);
3135
3136 let simulation_res = setup
3137 .rpc
3138 .simulate_transaction(
3139 Some(setup.context),
3140 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3141 Some(RpcSimulateTransactionConfig {
3142 sig_verify: false,
3143 replace_recent_blockhash: false,
3144 commitment: Some(CommitmentConfig::finalized()),
3145 encoding: None,
3146 accounts: Some(RpcSimulateTransactionAccountsConfig {
3147 encoding: None,
3148 addresses: vec![pk.to_string()],
3149 }),
3150 min_context_slot: None,
3151 inner_instructions: false,
3152 }),
3153 )
3154 .await
3155 .unwrap();
3156
3157 assert_eq!(
3158 simulation_res.value.err, None,
3159 "Unexpected simulation error"
3160 );
3161 assert_eq!(
3162 simulation_res.value.accounts,
3163 Some(vec![Some(UiAccount {
3164 lamports,
3165 data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
3166 owner: system_program::id().to_string(),
3167 executable: false,
3168 rent_epoch: 0,
3169 space: Some(0),
3170 })]),
3171 "Wrong account content"
3172 );
3173 }
3174 #[tokio::test(flavor = "multi_thread")]
3175 async fn test_simulate_transaction_no_signers_err() {
3176 let payer = Keypair::new();
3177 let pk = Pubkey::new_unique();
3178 let lamports = LAMPORTS_PER_SOL;
3179 let setup = TestSetup::new(SurfpoolFullRpc);
3180 let recent_blockhash = setup
3181 .context
3182 .svm_locker
3183 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3184
3185 let _ = setup
3186 .rpc
3187 .request_airdrop(
3188 Some(setup.context.clone()),
3189 payer.pubkey().to_string(),
3190 2 * lamports,
3191 None,
3192 )
3193 .unwrap();
3194 setup
3195 .context
3196 .svm_locker
3197 .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
3198
3199 let mut msg = LegacyMessage::new(
3201 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3202 Some(&payer.pubkey()),
3203 );
3204 msg.recent_blockhash = recent_blockhash;
3205 let tx = Transaction::new_unsigned(msg);
3206
3207 let simulation_res = setup
3208 .rpc
3209 .simulate_transaction(
3210 Some(setup.context),
3211 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3212 Some(RpcSimulateTransactionConfig {
3213 sig_verify: true,
3214 replace_recent_blockhash: false,
3215 commitment: Some(CommitmentConfig::finalized()),
3216 encoding: None,
3217 accounts: Some(RpcSimulateTransactionAccountsConfig {
3218 encoding: None,
3219 addresses: vec![pk.to_string()],
3220 }),
3221 min_context_slot: None,
3222 inner_instructions: false,
3223 }),
3224 )
3225 .await
3226 .unwrap();
3227
3228 assert_eq!(
3229 simulation_res.value.err,
3230 Some(TransactionError::SignatureFailure.into())
3231 );
3232 }
3233
3234 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3235 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3236 #[tokio::test(flavor = "multi_thread")]
3237 async fn test_simulate_transaction_replace_recent_blockhash(version: TransactionVersion) {
3238 let payer = Keypair::new();
3239 let pk = Pubkey::new_unique();
3240 let lamports = LAMPORTS_PER_SOL;
3241 let setup = TestSetup::new(SurfpoolFullRpc);
3242 let recent_blockhash = setup
3243 .context
3244 .svm_locker
3245 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3246 let block_height = setup
3247 .context
3248 .svm_locker
3249 .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
3250 let bad_blockhash = Hash::new_unique();
3251
3252 let _ = setup
3253 .rpc
3254 .request_airdrop(
3255 Some(setup.context.clone()),
3256 payer.pubkey().to_string(),
3257 2 * lamports,
3258 None,
3259 )
3260 .unwrap();
3261
3262 let mut tx = match version {
3263 TransactionVersion::Legacy(_) => build_legacy_transaction(
3264 &payer.pubkey(),
3265 &[&payer.insecure_clone()],
3266 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3267 &recent_blockhash,
3268 ),
3269 TransactionVersion::Number(0) => build_v0_transaction(
3270 &payer.pubkey(),
3271 &[&payer.insecure_clone()],
3272 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3273 &recent_blockhash,
3274 ),
3275 _ => unimplemented!(),
3276 };
3277 match &mut tx.message {
3278 VersionedMessage::Legacy(msg) => {
3279 msg.recent_blockhash = bad_blockhash;
3280 }
3281 VersionedMessage::V0(msg) => {
3282 msg.recent_blockhash = bad_blockhash;
3283 }
3284 }
3285
3286 let invalid_config = RpcSimulateTransactionConfig {
3287 sig_verify: true,
3288 replace_recent_blockhash: true,
3289 commitment: Some(CommitmentConfig::finalized()),
3290 encoding: None,
3291 accounts: Some(RpcSimulateTransactionAccountsConfig {
3292 encoding: None,
3293 addresses: vec![pk.to_string()],
3294 }),
3295 min_context_slot: None,
3296 inner_instructions: false,
3297 };
3298 let err = setup
3299 .rpc
3300 .simulate_transaction(
3301 Some(setup.context.clone()),
3302 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3303 Some(invalid_config.clone()),
3304 )
3305 .await
3306 .unwrap_err();
3307
3308 assert_eq!(
3309 err.message, "sigVerify may not be used with replaceRecentBlockhash",
3310 "sigVerify should not be allowed to be used with replaceRecentBlockhash"
3311 );
3312
3313 let mut valid_config = invalid_config;
3314 valid_config.sig_verify = false;
3315 let simulation_res = setup
3316 .rpc
3317 .simulate_transaction(
3318 Some(setup.context),
3319 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
3320 Some(valid_config),
3321 )
3322 .await
3323 .unwrap();
3324
3325 assert_eq!(
3326 simulation_res.value.err, None,
3327 "Unexpected simulation error"
3328 );
3329 assert_eq!(
3330 simulation_res.value.replacement_blockhash,
3331 Some(RpcBlockhash {
3332 blockhash: recent_blockhash.to_string(),
3333 last_valid_block_height: block_height
3334 }),
3335 "Replacement blockhash should be the latest blockhash"
3336 );
3337 }
3338
3339 #[tokio::test(flavor = "multi_thread")]
3340 async fn test_get_block() {
3341 let setup = TestSetup::new(SurfpoolFullRpc);
3342
3343 setup.context.svm_locker.with_svm_writer(|svm_writer| {
3345 svm_writer.latest_epoch_info.absolute_slot = 10;
3346 });
3347
3348 let res = setup
3349 .rpc
3350 .get_block(Some(setup.context), 0, None)
3351 .await
3352 .unwrap();
3353
3354 assert!(res.is_some(), "Empty blocks should be reconstructed");
3356 let block = res.unwrap();
3357 assert!(
3358 block.signatures.is_none() || block.signatures.as_ref().unwrap().is_empty(),
3359 "Reconstructed empty block should have no signatures"
3360 );
3361 }
3362
3363 #[tokio::test(flavor = "multi_thread")]
3364 async fn test_get_block_time() {
3365 let setup = TestSetup::new(SurfpoolFullRpc);
3366
3367 setup.context.svm_locker.with_svm_writer(|svm_writer| {
3369 svm_writer.latest_epoch_info.absolute_slot = 10;
3370 });
3371
3372 let res = setup
3373 .rpc
3374 .get_block_time(Some(setup.context), 0)
3375 .await
3376 .unwrap();
3377
3378 assert!(
3380 res.is_some(),
3381 "Block time should be calculated for valid slots"
3382 );
3383 }
3384
3385 #[tokio::test(flavor = "multi_thread")]
3386 async fn test_get_block_respects_confirmed_commitment_visibility() {
3387 let setup = TestSetup::new(SurfpoolFullRpc);
3388
3389 setup.context.svm_locker.with_svm_writer(|svm_writer| {
3390 svm_writer.latest_epoch_info.absolute_slot = 10;
3391 });
3392
3393 let res = setup
3394 .rpc
3395 .get_block(
3396 Some(setup.context),
3397 10,
3398 Some(RpcEncodingConfigWrapper::Current(Some(RpcBlockConfig {
3399 commitment: Some(CommitmentConfig::confirmed()),
3400 ..RpcBlockConfig::default()
3401 }))),
3402 )
3403 .await
3404 .unwrap();
3405
3406 assert!(
3407 res.is_none(),
3408 "A confirmed getBlock request should not expose a slot newer than the confirmed slot"
3409 );
3410 }
3411
3412 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
3413 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
3414 #[tokio::test(flavor = "multi_thread")]
3415 async fn test_get_transaction(version: TransactionVersion) {
3416 let payer = Keypair::new();
3417 let pk = Pubkey::new_unique();
3418 let lamports = LAMPORTS_PER_SOL;
3419 let mut setup = TestSetup::new(SurfpoolFullRpc);
3420 let recent_blockhash = setup
3421 .context
3422 .svm_locker
3423 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3424
3425 let _ = setup
3426 .rpc
3427 .request_airdrop(
3428 Some(setup.context.clone()),
3429 payer.pubkey().to_string(),
3430 2 * lamports,
3431 None,
3432 )
3433 .unwrap();
3434
3435 let tx = match version {
3436 TransactionVersion::Legacy(_) => build_legacy_transaction(
3437 &payer.pubkey(),
3438 &[&payer.insecure_clone()],
3439 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3440 &recent_blockhash,
3441 ),
3442 TransactionVersion::Number(0) => build_v0_transaction(
3443 &payer.pubkey(),
3444 &[&payer.insecure_clone()],
3445 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3446 &recent_blockhash,
3447 ),
3448 _ => unimplemented!(),
3449 };
3450
3451 setup.process_txs(vec![tx.clone()]).await;
3452
3453 let res = setup
3454 .rpc
3455 .get_transaction(
3456 Some(setup.context.clone()),
3457 tx.signatures[0].to_string(),
3458 Some(RpcEncodingConfigWrapper::Current(Some(
3459 get_default_transaction_config(),
3460 ))),
3461 )
3462 .await
3463 .unwrap()
3464 .unwrap();
3465
3466 let instructions = match tx.message.clone() {
3467 VersionedMessage::Legacy(message) => message
3468 .instructions
3469 .iter()
3470 .map(|ix| UiCompiledInstruction::from(ix, Some(1)))
3471 .collect(),
3472 VersionedMessage::V0(message) => message
3473 .instructions
3474 .iter()
3475 .map(|ix| UiCompiledInstruction::from(ix, Some(1)))
3476 .collect(),
3477 };
3478
3479 assert_eq!(
3480 res,
3481 EncodedConfirmedTransactionWithStatusMeta {
3482 slot: 123,
3483 transaction: EncodedTransactionWithStatusMeta {
3484 transaction: EncodedTransaction::Json(UiTransaction {
3485 signatures: vec![tx.signatures[0].to_string()],
3486 message: UiMessage::Raw(UiRawMessage {
3487 header: MessageHeader {
3488 num_required_signatures: 1,
3489 num_readonly_signed_accounts: 0,
3490 num_readonly_unsigned_accounts: 1
3491 },
3492 account_keys: vec![
3493 payer.pubkey().to_string(),
3494 pk.to_string(),
3495 system_program::id().to_string()
3496 ],
3497 recent_blockhash: recent_blockhash.to_string(),
3498 instructions,
3499 address_table_lookups: match tx.message {
3500 VersionedMessage::Legacy(_) => None,
3501 VersionedMessage::V0(_) => Some(vec![]),
3502 },
3503 })
3504 }),
3505 meta: res.transaction.clone().meta, version: Some(version)
3507 },
3508 block_time: res.block_time }
3510 );
3511 }
3512
3513 #[tokio::test(flavor = "multi_thread")]
3514 #[allow(deprecated)]
3515 async fn test_get_first_available_block() {
3516 let setup = TestSetup::new(SurfpoolFullRpc);
3517
3518 {
3519 let mut svm_writer = setup.context.svm_locker.0.write().await;
3520
3521 let previous_chain_tip = svm_writer.chain_tip.clone();
3522
3523 let latest_entries = svm_writer
3524 .inner
3525 .get_sysvar::<solana_sysvar::recent_blockhashes::RecentBlockhashes>(
3526 );
3527 let latest_entry = latest_entries.first().unwrap();
3528
3529 svm_writer.chain_tip = BlockIdentifier::new(
3530 svm_writer.chain_tip.index + 1,
3531 latest_entry.blockhash.to_string().as_str(),
3532 );
3533
3534 let hash = svm_writer.chain_tip.hash.clone();
3535 let block_height = svm_writer.chain_tip.index;
3536 let parent_slot = svm_writer.get_latest_absolute_slot();
3537
3538 svm_writer
3539 .blocks
3540 .store(
3541 parent_slot,
3542 BlockHeader {
3543 hash,
3544 previous_blockhash: previous_chain_tip.hash.clone(),
3545 block_time: chrono::Utc::now().timestamp_millis(),
3546 block_height,
3547 parent_slot,
3548 signatures: Vec::new(),
3549 },
3550 )
3551 .unwrap();
3552 }
3553
3554 let res = setup
3555 .rpc
3556 .get_first_available_block(Some(setup.context))
3557 .unwrap();
3558
3559 assert_eq!(res, 123);
3560 }
3561
3562 #[test]
3563 fn test_get_latest_blockhash() {
3564 let setup = TestSetup::new(SurfpoolFullRpc);
3565
3566 insert_test_blocks(&setup, 100..=150);
3567
3568 {
3570 let commitment = CommitmentConfig::processed();
3571 let res = setup
3572 .rpc
3573 .get_latest_blockhash(
3574 Some(setup.context.clone()),
3575 Some(RpcContextConfig {
3576 commitment: Some(commitment.clone()),
3577 ..Default::default()
3578 }),
3579 )
3580 .unwrap();
3581 let expected_blockhash = setup
3582 .context
3583 .svm_locker
3584 .get_latest_blockhash(&commitment)
3585 .unwrap();
3586
3587 let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3588 let expected_last_valid_block_height =
3589 current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3590
3591 assert_eq!(
3592 res.value.blockhash,
3593 expected_blockhash.to_string(),
3594 "Latest blockhash does not match expected value"
3595 );
3596 assert_eq!(
3597 res.value.last_valid_block_height, expected_last_valid_block_height,
3598 "Last valid block height does not match expected value"
3599 );
3600 }
3601
3602 {
3604 let commitment = CommitmentConfig::confirmed();
3605 let res = setup
3606 .rpc
3607 .get_latest_blockhash(
3608 Some(setup.context.clone()),
3609 Some(RpcContextConfig {
3610 commitment: Some(commitment.clone()),
3611 ..Default::default()
3612 }),
3613 )
3614 .unwrap();
3615 let expected_blockhash = setup
3616 .context
3617 .svm_locker
3618 .get_latest_blockhash(&commitment)
3619 .unwrap();
3620
3621 let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3622 let expected_last_valid_block_height =
3623 current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3624
3625 assert_eq!(
3626 res.value.blockhash,
3627 expected_blockhash.to_string(),
3628 "Latest blockhash does not match expected value"
3629 );
3630 assert_eq!(
3631 res.value.last_valid_block_height, expected_last_valid_block_height,
3632 "Last valid block height does not match expected value"
3633 );
3634 }
3635
3636 {
3638 let commitment = CommitmentConfig::finalized();
3639 let res = setup
3640 .rpc
3641 .get_latest_blockhash(
3642 Some(setup.context.clone()),
3643 Some(RpcContextConfig {
3644 commitment: Some(commitment.clone()),
3645 ..Default::default()
3646 }),
3647 )
3648 .unwrap();
3649 let expected_blockhash = setup
3650 .context
3651 .svm_locker
3652 .get_latest_blockhash(&commitment)
3653 .unwrap();
3654
3655 let current_block_height = setup.context.svm_locker.get_epoch_info().block_height;
3656 let expected_last_valid_block_height =
3657 current_block_height + MAX_RECENT_BLOCKHASHES_STANDARD as u64;
3658
3659 assert_eq!(
3660 res.value.blockhash,
3661 expected_blockhash.to_string(),
3662 "Latest blockhash does not match expected value"
3663 );
3664 assert_eq!(
3665 res.value.last_valid_block_height, expected_last_valid_block_height,
3666 "Last valid block height does not match expected value"
3667 );
3668 }
3669 }
3670
3671 #[tokio::test(flavor = "multi_thread")]
3672 async fn test_get_recent_prioritization_fees() {
3673 let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
3674 let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
3675
3676 let recent_blockhash = setup
3677 .context
3678 .svm_locker
3679 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3680
3681 let payer_1 = Keypair::new();
3682 let payer_2 = Keypair::new();
3683 let receiver_pubkey = Pubkey::new_unique();
3684 let random_pubkey = Pubkey::new_unique();
3685
3686 {
3688 let _ = setup
3689 .rpc
3690 .request_airdrop(
3691 Some(setup.context.clone()),
3692 payer_1.pubkey().to_string(),
3693 2 * LAMPORTS_PER_SOL,
3694 None,
3695 )
3696 .unwrap();
3697 let _ = setup
3698 .rpc
3699 .request_airdrop(
3700 Some(setup.context.clone()),
3701 payer_2.pubkey().to_string(),
3702 2 * LAMPORTS_PER_SOL,
3703 None,
3704 )
3705 .unwrap();
3706
3707 setup
3708 .context
3709 .svm_locker
3710 .confirm_current_block(&None)
3711 .await
3712 .unwrap();
3713 }
3714
3715 {
3717 let tx_1 = build_legacy_transaction(
3718 &payer_1.pubkey(),
3719 &[&payer_1.insecure_clone()],
3720 &[
3721 system_instruction::transfer(
3722 &payer_1.pubkey(),
3723 &receiver_pubkey,
3724 LAMPORTS_PER_SOL,
3725 ),
3726 ComputeBudgetInstruction::set_compute_unit_price(1000),
3727 ],
3728 &recent_blockhash,
3729 );
3730 let tx_2 = build_legacy_transaction(
3731 &payer_2.pubkey(),
3732 &[&payer_2.insecure_clone()],
3733 &[
3734 system_instruction::transfer(
3735 &payer_2.pubkey(),
3736 &receiver_pubkey,
3737 LAMPORTS_PER_SOL,
3738 ),
3739 ComputeBudgetInstruction::set_compute_unit_price(1002),
3740 ],
3741 &recent_blockhash,
3742 );
3743
3744 send_and_await_transaction(tx_1, setup.clone(), mempool_rx.clone())
3745 .await
3746 .join()
3747 .unwrap();
3748 send_and_await_transaction(tx_2, setup.clone(), mempool_rx)
3749 .await
3750 .join()
3751 .unwrap();
3752 setup
3753 .context
3754 .svm_locker
3755 .confirm_current_block(&None)
3756 .await
3757 .unwrap();
3758 }
3759
3760 let res = setup
3763 .rpc
3764 .get_recent_prioritization_fees(
3765 Some(setup.context.clone()),
3766 Some(vec![payer_1.pubkey().to_string()]),
3767 )
3768 .await
3769 .unwrap();
3770 assert_eq!(res.len(), 1);
3771 assert_eq!(res[0].prioritization_fee, 1000);
3772
3773 let res = setup
3776 .rpc
3777 .get_recent_prioritization_fees(Some(setup.context.clone()), None)
3778 .await
3779 .unwrap();
3780 assert_eq!(res.len(), 2);
3781 assert_eq!(res[0].prioritization_fee, 1000);
3782 assert_eq!(res[1].prioritization_fee, 1002);
3783
3784 let res = setup
3787 .rpc
3788 .get_recent_prioritization_fees(
3789 Some(setup.context.clone()),
3790 Some(vec![random_pubkey.to_string()]),
3791 )
3792 .await
3793 .unwrap();
3794 assert!(
3795 res.is_empty(),
3796 "Expected no prioritization fees for random account"
3797 );
3798 }
3799
3800 #[tokio::test(flavor = "multi_thread")]
3801 async fn test_get_blocks_with_limit() {
3802 let setup = TestSetup::new(SurfpoolFullRpc);
3803
3804 insert_test_blocks(&setup, 100..=110);
3805
3806 let result = setup
3807 .rpc
3808 .get_blocks_with_limit(Some(setup.context.clone()), 100, 5, None)
3809 .await
3810 .unwrap();
3811
3812 assert_eq!(result, vec![100, 101, 102, 103, 104]);
3813 }
3814
3815 #[tokio::test(flavor = "multi_thread")]
3816 async fn test_get_blocks_with_limit_exceeds_available() {
3817 let setup = TestSetup::new(SurfpoolFullRpc);
3818
3819 insert_test_blocks(&setup, 100..=102);
3820
3821 let result = setup
3822 .rpc
3823 .get_blocks_with_limit(Some(setup.context.clone()), 100, 10, None)
3824 .await
3825 .unwrap();
3826
3827 assert_eq!(result, vec![100, 101, 102]);
3828 }
3829
3830 #[tokio::test(flavor = "multi_thread")]
3831 async fn test_get_blocks_with_limit_commitment_levels() {
3832 let setup = TestSetup::new(SurfpoolFullRpc);
3833
3834 insert_test_blocks(&setup, 80..=120);
3835
3836 let processed_result = setup
3838 .rpc
3839 .get_blocks_with_limit(
3840 Some(setup.context.clone()),
3841 115,
3842 10,
3843 Some(RpcContextConfig {
3844 commitment: Some(CommitmentConfig {
3845 commitment: CommitmentLevel::Processed,
3846 }),
3847 min_context_slot: None,
3848 }),
3849 )
3850 .await
3851 .unwrap();
3852 assert_eq!(processed_result, vec![115, 116, 117, 118, 119, 120]);
3853
3854 let confirmed_result = setup
3856 .rpc
3857 .get_blocks_with_limit(
3858 Some(setup.context.clone()),
3859 115,
3860 10,
3861 Some(RpcContextConfig {
3862 commitment: Some(CommitmentConfig {
3863 commitment: CommitmentLevel::Confirmed,
3864 }),
3865 min_context_slot: None,
3866 }),
3867 )
3868 .await
3869 .unwrap();
3870 assert_eq!(confirmed_result, vec![115, 116, 117, 118, 119]);
3871
3872 let finalized_result = setup
3874 .rpc
3875 .get_blocks_with_limit(
3876 Some(setup.context.clone()),
3877 85,
3878 10,
3879 Some(RpcContextConfig {
3880 commitment: Some(CommitmentConfig {
3881 commitment: CommitmentLevel::Finalized,
3882 }),
3883 min_context_slot: None,
3884 }),
3885 )
3886 .await
3887 .unwrap();
3888 assert_eq!(finalized_result, vec![85, 86, 87, 88, 89]);
3889 }
3890
3891 #[tokio::test(flavor = "multi_thread")]
3892 async fn test_get_blocks_with_limit_sparse_blocks() {
3893 let setup = TestSetup::new(SurfpoolFullRpc);
3894
3895 insert_test_blocks(
3896 &setup,
3897 vec![100, 103, 105, 107, 109, 112, 115, 118, 120, 122],
3898 );
3899
3900 let result = setup
3901 .rpc
3902 .get_blocks_with_limit(Some(setup.context.clone()), 100, 6, None)
3903 .await
3904 .unwrap();
3905
3906 assert_eq!(result, vec![100, 101, 102, 103, 104, 105]);
3908 }
3909
3910 #[tokio::test(flavor = "multi_thread")]
3911 async fn test_get_blocks_with_limit_empty_result() {
3912 let setup = TestSetup::new(SurfpoolFullRpc);
3913
3914 {
3915 let mut svm_writer = setup.context.svm_locker.0.write().await;
3916 svm_writer.latest_epoch_info.absolute_slot = 100;
3917 }
3919
3920 let result = setup
3922 .rpc
3923 .get_blocks_with_limit(Some(setup.context.clone()), 50, 10, None)
3924 .await
3925 .unwrap();
3926
3927 let expected: Vec<Slot> = (50..60).collect();
3929 assert_eq!(result, expected);
3930 }
3931
3932 #[tokio::test(flavor = "multi_thread")]
3933 async fn test_get_blocks_with_limit_large_limit() {
3934 let setup = TestSetup::new(SurfpoolFullRpc);
3935
3936 insert_test_blocks(&setup, 0..1000);
3937
3938 let result = setup
3939 .rpc
3940 .get_blocks_with_limit(Some(setup.context.clone()), 0, 1000, None)
3941 .await
3942 .unwrap();
3943
3944 assert_eq!(result.len(), 1000);
3945 assert_eq!(result[0], 0);
3946 assert_eq!(result[999], 999);
3947
3948 for i in 1..result.len() {
3949 assert!(
3950 result[i] > result[i - 1],
3951 "Results should be in ascending order"
3952 );
3953 }
3954 }
3955
3956 #[tokio::test(flavor = "multi_thread")]
3957 async fn test_get_blocks_basic() {
3958 let setup = TestSetup::new(SurfpoolFullRpc);
3960
3961 insert_test_blocks(&setup, 100..=102);
3962
3963 setup.context.svm_locker.with_svm_writer(|svm_writer| {
3964 svm_writer.latest_epoch_info.absolute_slot = 150;
3965 });
3966
3967 let result = setup
3968 .rpc
3969 .get_blocks(
3970 Some(setup.context.clone()),
3971 100,
3972 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(102))),
3973 None,
3974 )
3975 .await
3976 .unwrap();
3977
3978 assert_eq!(result, vec![100, 101, 102]);
3979 }
3980
3981 #[tokio::test(flavor = "multi_thread")]
3982 async fn test_get_blocks_no_end_slot() {
3983 let setup = TestSetup::new(SurfpoolFullRpc);
3984
3985 insert_test_blocks(&setup, 100..=105);
3986
3987 let result = setup
3989 .rpc
3990 .get_blocks(
3991 Some(setup.context.clone()),
3992 100,
3993 None,
3994 Some(RpcContextConfig {
3995 commitment: Some(CommitmentConfig {
3996 commitment: CommitmentLevel::Confirmed,
3997 }),
3998 min_context_slot: None,
3999 }),
4000 )
4001 .await
4002 .unwrap();
4003
4004 assert_eq!(result, vec![100, 101, 102, 103, 104]);
4006 }
4007
4008 #[tokio::test(flavor = "multi_thread")]
4009 async fn test_get_blocks_commitment_levels() {
4010 let setup = TestSetup::new(SurfpoolFullRpc);
4011
4012 insert_test_blocks(&setup, 50..=100);
4013
4014 let processed_result = setup
4016 .rpc
4017 .get_blocks(
4018 Some(setup.context.clone()),
4019 95,
4020 None,
4021 Some(RpcContextConfig {
4022 commitment: Some(CommitmentConfig {
4023 commitment: CommitmentLevel::Processed,
4024 }),
4025 min_context_slot: None,
4026 }),
4027 )
4028 .await
4029 .unwrap();
4030 assert_eq!(processed_result, vec![95, 96, 97, 98, 99, 100]);
4031
4032 let confirmed_result = setup
4034 .rpc
4035 .get_blocks(
4036 Some(setup.context.clone()),
4037 95,
4038 None,
4039 Some(RpcContextConfig {
4040 commitment: Some(CommitmentConfig {
4041 commitment: CommitmentLevel::Confirmed,
4042 }),
4043 min_context_slot: None,
4044 }),
4045 )
4046 .await
4047 .unwrap();
4048 assert_eq!(confirmed_result, vec![95, 96, 97, 98, 99]);
4049
4050 let finalized_result = setup
4052 .rpc
4053 .get_blocks(
4054 Some(setup.context.clone()),
4055 65,
4056 None,
4057 Some(RpcContextConfig {
4058 commitment: Some(CommitmentConfig {
4059 commitment: CommitmentLevel::Finalized,
4060 }),
4061 min_context_slot: None,
4062 }),
4063 )
4064 .await
4065 .unwrap();
4066 assert_eq!(finalized_result, vec![65, 66, 67, 68, 69]);
4067 }
4068
4069 #[tokio::test(flavor = "multi_thread")]
4070 async fn test_get_blocks_min_context_slot() {
4071 let setup = TestSetup::new(SurfpoolFullRpc);
4072
4073 insert_test_blocks(&setup, 100..=110);
4074
4075 let result = setup
4077 .rpc
4078 .get_blocks(
4079 Some(setup.context.clone()),
4080 100,
4081 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(105))),
4082 Some(RpcContextConfig {
4083 commitment: Some(CommitmentConfig::finalized()),
4084 min_context_slot: Some(105),
4085 }),
4086 )
4087 .await;
4088
4089 assert!(result.is_err());
4090
4091 let result = setup
4092 .rpc
4093 .get_blocks(
4094 Some(setup.context.clone()),
4095 105,
4096 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(108))),
4097 Some(RpcContextConfig {
4098 commitment: Some(CommitmentConfig {
4099 commitment: CommitmentLevel::Processed,
4100 }),
4101 min_context_slot: Some(105),
4102 }),
4103 )
4104 .await
4105 .unwrap();
4106
4107 assert_eq!(result, vec![105, 106, 107, 108]);
4108 }
4109
4110 #[tokio::test(flavor = "multi_thread")]
4111 async fn test_get_blocks_sparse_blocks() {
4112 let setup = TestSetup::new(SurfpoolFullRpc);
4113
4114 insert_test_blocks(&setup, vec![100, 102, 105, 107, 110]);
4116
4117 let result = setup
4118 .rpc
4119 .get_blocks(
4120 Some(setup.context.clone()),
4121 100,
4122 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(115))),
4123 None,
4124 )
4125 .await
4126 .unwrap();
4127
4128 let expected: Vec<Slot> = (100..=110).collect();
4131 assert_eq!(result, expected);
4132 }
4133
4134 fn insert_test_blocks<I>(setup: &TestSetup<SurfpoolFullRpc>, slots: I)
4136 where
4137 I: IntoIterator<Item = u64>,
4138 {
4139 let slots: Vec<u64> = slots.into_iter().collect();
4140 setup.context.svm_locker.with_svm_writer(|svm_writer| {
4141 for slot in slots.iter() {
4142 svm_writer
4143 .blocks
4144 .store(
4145 *slot,
4146 BlockHeader {
4147 hash: SyntheticBlockhash::new(*slot).to_string(),
4148 previous_blockhash: SyntheticBlockhash::new(slot.saturating_sub(1))
4149 .to_string(),
4150 block_time: chrono::Utc::now().timestamp_millis(),
4151 block_height: *slot,
4152 parent_slot: slot.saturating_sub(1),
4153 signatures: vec![],
4154 },
4155 )
4156 .unwrap();
4157 }
4158 svm_writer.latest_epoch_info.absolute_slot = slots.into_iter().max().unwrap_or(0);
4159 });
4160 }
4161
4162 #[tokio::test(flavor = "multi_thread")]
4163 async fn test_get_blocks_local_only() {
4164 let setup = TestSetup::new(SurfpoolFullRpc);
4165
4166 insert_test_blocks(&setup, 50..=100);
4167
4168 let result = setup
4170 .rpc
4171 .get_blocks(
4172 Some(setup.context),
4173 75,
4174 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(90))),
4175 None,
4176 )
4177 .await
4178 .unwrap();
4179
4180 let expected: Vec<Slot> = (75..=90).collect();
4181 assert_eq!(result, expected, "Should return all local blocks in range");
4182 }
4183
4184 #[tokio::test(flavor = "multi_thread")]
4185 async fn test_get_blocks_no_remote_context() {
4186 let setup = TestSetup::new(SurfpoolFullRpc);
4187
4188 insert_test_blocks(&setup, 50..=100);
4189
4190 let result = setup
4191 .rpc
4192 .get_blocks(
4193 Some(setup.context),
4194 10,
4195 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
4196 None,
4197 )
4198 .await
4199 .unwrap();
4200
4201 let expected: Vec<Slot> = (10..=60).collect();
4204 assert_eq!(
4205 result, expected,
4206 "Should return all local blocks in range including reconstructed empty blocks"
4207 );
4208 }
4209
4210 #[tokio::test(flavor = "multi_thread")]
4211 async fn test_get_blocks_remote_fetch_below_local_minimum() {
4212 let setup = TestSetup::new(SurfpoolFullRpc);
4213
4214 let local_slots = vec![50, 51, 52, 60, 61, 70, 80, 90, 100];
4215 insert_test_blocks(&setup, local_slots.clone());
4216
4217 let stored_min = setup
4219 .context
4220 .svm_locker
4221 .with_svm_reader(|svm_reader| svm_reader.blocks.keys().unwrap().into_iter().min());
4222 assert_eq!(
4223 stored_min,
4224 Some(50),
4225 "Stored blocks minimum should be slot 50"
4226 );
4227
4228 let result = setup
4231 .rpc
4232 .get_blocks(
4233 Some(setup.context.clone()),
4234 10,
4235 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(30))),
4236 None,
4237 )
4238 .await
4239 .unwrap();
4240
4241 let expected: Vec<Slot> = (10..=30).collect();
4243 assert_eq!(
4244 result, expected,
4245 "Should return all slots in range (empty blocks are reconstructed)"
4246 );
4247
4248 let result = setup
4250 .rpc
4251 .get_blocks(
4252 Some(setup.context.clone()),
4253 10,
4254 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
4255 None,
4256 )
4257 .await
4258 .unwrap();
4259
4260 let expected: Vec<Slot> = (10..=60).collect();
4261 assert_eq!(
4262 result, expected,
4263 "Should return all local slots (empty blocks reconstructed)"
4264 );
4265
4266 let result = setup
4268 .rpc
4269 .get_blocks(
4270 Some(setup.context.clone()),
4271 45,
4272 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(55))),
4273 None,
4274 )
4275 .await
4276 .unwrap();
4277
4278 let expected: Vec<Slot> = (45..=55).collect();
4279 assert_eq!(
4280 result, expected,
4281 "Should return all slots in range (empty blocks reconstructed)"
4282 );
4283
4284 let result = setup
4286 .rpc
4287 .get_blocks(
4288 Some(setup.context),
4289 55,
4290 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(65))),
4291 None,
4292 )
4293 .await
4294 .unwrap();
4295
4296 let expected: Vec<Slot> = (55..=65).collect();
4297 assert_eq!(
4298 result, expected,
4299 "Should return all slots in range (empty blocks reconstructed)"
4300 );
4301 }
4302
4303 #[tokio::test(flavor = "multi_thread")]
4304 async fn test_get_blocks_all_below_range_mock_remote() {
4305 let setup = TestSetup::new(SurfpoolFullRpc);
4306
4307 insert_test_blocks(&setup, 100..=150);
4308
4309 setup.context.svm_locker.with_svm_writer(|svm_writer| {
4310 svm_writer.latest_epoch_info.absolute_slot = 200; });
4312
4313 let (stored_min, latest_slot) = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4314 let min = svm_reader.blocks.keys().unwrap().into_iter().min();
4315 let latest = svm_reader.get_latest_absolute_slot();
4316 (min, latest)
4317 });
4318 assert_eq!(stored_min, Some(100), "Stored blocks minimum should be 100");
4319 assert_eq!(latest_slot, 200, "Latest slot should be 200");
4320
4321 let result = setup
4323 .rpc
4324 .get_blocks(
4325 Some(setup.context.clone()),
4326 10,
4327 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(50))),
4328 None,
4329 )
4330 .await
4331 .unwrap();
4332
4333 let expected: Vec<Slot> = (10..=50).collect();
4334 assert_eq!(
4335 result, expected,
4336 "Should return all slots (empty blocks reconstructed)"
4337 );
4338
4339 let result = setup
4341 .rpc
4342 .get_blocks(
4343 Some(setup.context.clone()),
4344 5,
4345 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(30))),
4346 None,
4347 )
4348 .await
4349 .unwrap();
4350
4351 let expected: Vec<Slot> = (5..=30).collect();
4352 assert_eq!(
4353 result, expected,
4354 "Should return all slots (empty blocks reconstructed)"
4355 );
4356
4357 let result = setup
4359 .rpc
4360 .get_blocks(
4361 Some(setup.context),
4362 80,
4363 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(120))),
4364 None,
4365 )
4366 .await
4367 .unwrap();
4368
4369 let expected: Vec<Slot> = (80..=120).collect();
4370 assert_eq!(result, expected, "Should return all slots 80-120");
4371 }
4372
4373 #[test]
4374 fn test_get_max_shred_insert_slot() {
4375 let setup = TestSetup::new(SurfpoolFullRpc);
4376
4377 let result = setup
4378 .rpc
4379 .get_max_shred_insert_slot(Some(setup.context.clone()))
4380 .unwrap();
4381 let stake_min_delegation = setup
4382 .rpc
4383 .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4384 .unwrap();
4385
4386 let expected_slot = setup
4387 .context
4388 .svm_locker
4389 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4390
4391 assert_eq!(result, expected_slot);
4392 assert_eq!(stake_min_delegation.context.slot, expected_slot);
4393 assert_eq!(stake_min_delegation.value, 0); }
4395
4396 #[test]
4397 fn test_get_max_retransmit_slot() {
4398 let setup = TestSetup::new(SurfpoolFullRpc);
4399
4400 let result = setup
4401 .rpc
4402 .get_max_retransmit_slot(Some(setup.context.clone()))
4403 .unwrap();
4404 let slot = setup
4405 .context
4406 .clone()
4407 .svm_locker
4408 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4409
4410 assert_eq!(result, slot)
4411 }
4412
4413 #[test]
4414 fn test_get_cluster_nodes() {
4415 let setup = TestSetup::new(SurfpoolFullRpc);
4416
4417 let cluster_nodes = setup.rpc.get_cluster_nodes(Some(setup.context)).unwrap();
4418
4419 assert_eq!(
4420 cluster_nodes,
4421 vec![RpcContactInfo {
4422 pubkey: SURFPOOL_IDENTITY_PUBKEY.to_string(),
4423 gossip: Some("127.0.0.1:8001".parse().unwrap()),
4424 tvu: None,
4425 tpu: Some("127.0.0.1:8003".parse().unwrap()),
4426 tpu_quic: Some("127.0.0.1:8004".parse().unwrap()),
4427 tpu_forwards: None,
4428 tpu_forwards_quic: None,
4429 tpu_vote: None,
4430 serve_repair: None,
4431 rpc: Some("127.0.0.1:8899".parse().unwrap()),
4432 pubsub: Some("127.0.0.1:8900".parse().unwrap()),
4433 version: None,
4434 feature_set: None,
4435 shred_version: None,
4436 }]
4437 );
4438 }
4439
4440 #[test]
4441 fn test_get_stake_minimum_delegation_default() {
4442 let setup = TestSetup::new(SurfpoolFullRpc);
4443
4444 let result = setup
4445 .rpc
4446 .get_max_shred_insert_slot(Some(setup.context.clone()))
4447 .unwrap();
4448
4449 let stake_min_delegation = setup
4450 .rpc
4451 .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4452 .unwrap();
4453
4454 let expected_slot = setup
4455 .context
4456 .svm_locker
4457 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4458
4459 assert_eq!(result, expected_slot);
4460 assert_eq!(stake_min_delegation.context.slot, expected_slot);
4461 assert_eq!(stake_min_delegation.value, 0); }
4463
4464 #[test]
4465 fn test_get_stake_minimum_delegation_with_finalized_commitment() {
4466 let setup = TestSetup::new(SurfpoolFullRpc);
4467
4468 let config = Some(RpcContextConfig {
4469 commitment: Some(CommitmentConfig {
4470 commitment: CommitmentLevel::Finalized,
4471 }),
4472 min_context_slot: None,
4473 });
4474
4475 let result = setup
4476 .rpc
4477 .get_stake_minimum_delegation(Some(setup.context.clone()), config)
4478 .unwrap();
4479
4480 let expected_slot = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4482 svm_reader
4483 .get_latest_absolute_slot()
4484 .saturating_sub(FINALIZATION_SLOT_THRESHOLD)
4485 });
4486
4487 assert_eq!(result.context.slot, expected_slot);
4488 assert_eq!(result.value, 0);
4489 }
4490
4491 #[tokio::test(flavor = "multi_thread")]
4492 async fn test_is_blockhash_valid_recent_blockhash() {
4493 let setup = TestSetup::new(SurfpoolFullRpc);
4494
4495 let recent_blockhash = setup
4497 .context
4498 .svm_locker
4499 .with_svm_reader(|svm| svm.latest_blockhash());
4500
4501 let result = setup
4502 .rpc
4503 .is_blockhash_valid(
4504 Some(setup.context.clone()),
4505 recent_blockhash.to_string(),
4506 None,
4507 )
4508 .unwrap();
4509
4510 assert_eq!(result.value, true);
4511 assert!(result.context.slot > 0);
4512
4513 let result_processed = setup
4515 .rpc
4516 .is_blockhash_valid(
4517 Some(setup.context.clone()),
4518 recent_blockhash.to_string(),
4519 Some(RpcContextConfig {
4520 commitment: Some(CommitmentConfig {
4521 commitment: CommitmentLevel::Processed,
4522 }),
4523 min_context_slot: None,
4524 }),
4525 )
4526 .unwrap();
4527
4528 assert_eq!(result_processed.value, true);
4529 }
4530
4531 #[tokio::test(flavor = "multi_thread")]
4532 async fn test_is_blockhash_valid_invalid_blockhash() {
4533 let setup = TestSetup::new(SurfpoolFullRpc);
4534
4535 let fake_blockhash = Hash::new_from_array([1u8; 32]);
4536
4537 let result = setup
4539 .rpc
4540 .is_blockhash_valid(
4541 Some(setup.context.clone()),
4542 fake_blockhash.to_string(),
4543 None,
4544 )
4545 .unwrap();
4546
4547 assert_eq!(result.value, false);
4548
4549 let result_confirmed = setup
4551 .rpc
4552 .is_blockhash_valid(
4553 Some(setup.context.clone()),
4554 fake_blockhash.to_string(),
4555 Some(RpcContextConfig {
4556 commitment: Some(CommitmentConfig {
4557 commitment: CommitmentLevel::Confirmed,
4558 }),
4559 min_context_slot: None,
4560 }),
4561 )
4562 .unwrap();
4563
4564 assert_eq!(result_confirmed.value, false);
4565
4566 let another_fake = Hash::new_from_array([255u8; 32]);
4568 let result2 = setup
4569 .rpc
4570 .is_blockhash_valid(Some(setup.context.clone()), another_fake.to_string(), None)
4571 .unwrap();
4572
4573 assert_eq!(result2.value, false);
4574
4575 let invalid_result = setup.rpc.is_blockhash_valid(
4576 Some(setup.context.clone()),
4577 "invalid-blockhash-format".to_string(),
4578 None,
4579 );
4580
4581 assert!(invalid_result.is_err());
4582
4583 let short_result =
4584 setup
4585 .rpc
4586 .is_blockhash_valid(Some(setup.context.clone()), "123".to_string(), None);
4587 assert!(short_result.is_err());
4588
4589 let invalid_chars_result =
4591 setup
4592 .rpc
4593 .is_blockhash_valid(Some(setup.context.clone()), "0OIl".to_string(), None);
4594 assert!(invalid_chars_result.is_err());
4595 }
4596
4597 #[tokio::test(flavor = "multi_thread")]
4598 async fn test_is_blockhash_valid_commitment_and_context_slot() {
4599 let setup = TestSetup::new(SurfpoolFullRpc);
4600
4601 insert_test_blocks(&setup, 70..=100);
4603
4604 let recent_blockhash = setup
4605 .context
4606 .svm_locker
4607 .with_svm_reader(|svm| svm.latest_blockhash());
4608
4609 let processed_result = setup
4611 .rpc
4612 .is_blockhash_valid(
4613 Some(setup.context.clone()),
4614 recent_blockhash.to_string(),
4615 Some(RpcContextConfig {
4616 commitment: Some(CommitmentConfig {
4617 commitment: CommitmentLevel::Processed,
4618 }),
4619 min_context_slot: None,
4620 }),
4621 )
4622 .unwrap();
4623
4624 assert_eq!(processed_result.value, true);
4625 assert_eq!(processed_result.context.slot, 100);
4626
4627 let confirmed_result = setup
4629 .rpc
4630 .is_blockhash_valid(
4631 Some(setup.context.clone()),
4632 recent_blockhash.to_string(),
4633 Some(RpcContextConfig {
4634 commitment: Some(CommitmentConfig {
4635 commitment: CommitmentLevel::Confirmed,
4636 }),
4637 min_context_slot: None,
4638 }),
4639 )
4640 .unwrap();
4641
4642 assert_eq!(confirmed_result.value, true);
4643 assert_eq!(confirmed_result.context.slot, 99);
4644
4645 let finalized_result = setup
4647 .rpc
4648 .is_blockhash_valid(
4649 Some(setup.context.clone()),
4650 recent_blockhash.to_string(),
4651 Some(RpcContextConfig {
4652 commitment: Some(CommitmentConfig {
4653 commitment: CommitmentLevel::Finalized,
4654 }),
4655 min_context_slot: None,
4656 }),
4657 )
4658 .unwrap();
4659
4660 assert_eq!(finalized_result.value, true);
4661 assert_eq!(finalized_result.context.slot, 69);
4662
4663 let min_context_success = setup
4665 .rpc
4666 .is_blockhash_valid(
4667 Some(setup.context.clone()),
4668 recent_blockhash.to_string(),
4669 Some(RpcContextConfig {
4670 commitment: Some(CommitmentConfig {
4671 commitment: CommitmentLevel::Processed,
4672 }),
4673 min_context_slot: Some(95),
4674 }),
4675 )
4676 .unwrap();
4677
4678 assert_eq!(min_context_success.value, true);
4679
4680 let min_context_failure = setup.rpc.is_blockhash_valid(
4682 Some(setup.context.clone()),
4683 recent_blockhash.to_string(),
4684 Some(RpcContextConfig {
4685 commitment: Some(CommitmentConfig {
4686 commitment: CommitmentLevel::Finalized,
4687 }),
4688 min_context_slot: Some(80),
4689 }),
4690 );
4691
4692 assert!(min_context_failure.is_err());
4693 }
4694
4695 #[ignore = "requires-network"]
4696 #[tokio::test(flavor = "multi_thread")]
4697 async fn test_minimum_ledger_slot_from_remote() {
4698 let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
4700 let mut setup = TestSetup::new(SurfpoolFullRpc);
4701 setup.context.remote_rpc_client = Some(remote_client);
4702
4703 let result = setup
4704 .rpc
4705 .minimum_ledger_slot(Some(setup.context))
4706 .await
4707 .unwrap();
4708
4709 assert!(
4710 result > 0,
4711 "Mainnet should return a valid minimum ledger slot > 0"
4712 );
4713 println!("Mainnet minimum ledger slot: {}", result);
4714 }
4715
4716 #[tokio::test(flavor = "multi_thread")]
4717 async fn test_minimum_ledger_slot_missing_context_fails() {
4718 let setup = TestSetup::new(SurfpoolFullRpc);
4720
4721 let result = setup.rpc.minimum_ledger_slot(None).await;
4722
4723 assert!(
4724 result.is_err(),
4725 "Should fail when called without metadata context"
4726 );
4727 }
4728
4729 #[tokio::test(flavor = "multi_thread")]
4730 async fn test_minimum_ledger_slot_finds_minimum() {
4731 let setup = TestSetup::new(SurfpoolFullRpc);
4733
4734 insert_test_blocks(&setup, vec![500, 100, 1000, 50, 750]);
4735
4736 let result = setup
4737 .rpc
4738 .minimum_ledger_slot(Some(setup.context))
4739 .await
4740 .unwrap();
4741
4742 assert_eq!(
4745 result, 0,
4746 "Should return 0 since empty blocks can be reconstructed from slot 0"
4747 );
4748 }
4749
4750 #[tokio::test(flavor = "multi_thread")]
4751 async fn test_get_inflation_reward() {
4752 let setup = TestSetup::new(SurfpoolFullRpc);
4753
4754 let (epoch, effective_slot) =
4755 setup
4756 .context
4757 .clone()
4758 .svm_locker
4759 .with_svm_reader(|svm_reader| {
4760 (
4761 svm_reader.latest_epoch_info().epoch,
4762 svm_reader.get_latest_absolute_slot(),
4763 )
4764 });
4765
4766 let result = setup
4767 .rpc
4768 .get_inflation_reward(
4769 Some(setup.context),
4770 vec![Pubkey::new_unique().to_string()],
4771 None,
4772 )
4773 .await
4774 .unwrap();
4775
4776 assert_eq!(
4777 result[0],
4778 Some(RpcInflationReward {
4779 epoch,
4780 effective_slot,
4781 amount: 0,
4782 post_balance: 0,
4783 commission: None
4784 })
4785 )
4786 }
4787
4788 mod test_skip_sig_verify {
4790 use solana_client::rpc_config::RpcSendTransactionConfig;
4791 use solana_signature::Signature;
4792
4793 use super::*;
4794
4795 fn build_transaction_with_invalid_signature(
4796 payer: &Keypair,
4797 recipient: &Pubkey,
4798 recent_blockhash: &Hash,
4799 ) -> VersionedTransaction {
4800 let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
4801 &[system_instruction::transfer(
4802 &payer.pubkey(),
4803 recipient,
4804 LAMPORTS_PER_SOL,
4805 )],
4806 Some(&payer.pubkey()),
4807 recent_blockhash,
4808 ));
4809
4810 VersionedTransaction {
4811 signatures: vec![Signature::new_unique()],
4812 message: msg,
4813 }
4814 }
4815
4816 #[tokio::test(flavor = "multi_thread")]
4817 async fn test_send_transaction_with_skip_sig_verify_succeeds() {
4818 let payer = Keypair::new();
4819 let recipient = Pubkey::new_unique();
4820 let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
4821 let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
4822 let recent_blockhash = setup
4823 .context
4824 .svm_locker
4825 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
4826
4827 let _ = setup
4828 .context
4829 .svm_locker
4830 .0
4831 .write()
4832 .await
4833 .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
4834
4835 let tx =
4836 build_transaction_with_invalid_signature(&payer, &recipient, &recent_blockhash);
4837 let tx_encoded = bs58::encode(bincode::serialize(&tx).unwrap()).into_string();
4838
4839 let config = SurfpoolRpcSendTransactionConfig {
4840 base: RpcSendTransactionConfig::default(),
4841 skip_sig_verify: Some(true),
4842 };
4843
4844 let setup_clone = setup.clone();
4845 let handle = hiro_system_kit::thread_named("send_tx_skip_verify")
4846 .spawn(move || {
4847 setup_clone.rpc.send_transaction(
4848 Some(setup_clone.context),
4849 tx_encoded,
4850 Some(config),
4851 )
4852 })
4853 .unwrap();
4854
4855 loop {
4856 match mempool_rx.recv() {
4857 Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
4858 let mut writer = setup.context.svm_locker.0.write().await;
4859 let slot = writer.get_latest_absolute_slot();
4860 writer.transactions_queued_for_confirmation.push_back((
4861 tx.clone(),
4862 status_tx.clone(),
4863 None,
4864 ));
4865 let sig = tx.signatures[0];
4866 let tx_with_status_meta = TransactionWithStatusMeta {
4867 slot,
4868 transaction: tx,
4869 ..Default::default()
4870 };
4871 let mutated_accounts = std::collections::HashSet::new();
4872 writer
4873 .transactions
4874 .store(
4875 sig.to_string(),
4876 SurfnetTransactionStatus::processed(
4877 tx_with_status_meta,
4878 mutated_accounts,
4879 ),
4880 )
4881 .unwrap();
4882 status_tx
4883 .send(TransactionStatusEvent::Success(
4884 TransactionConfirmationStatus::Processed,
4885 ))
4886 .unwrap();
4887 break;
4888 }
4889 _ => continue,
4890 }
4891 }
4892
4893 let result = handle.join().unwrap();
4894 assert!(
4895 result.is_ok(),
4896 "Transaction with skip_sig_verify=true should succeed: {:?}",
4897 result
4898 );
4899 }
4900
4901 #[test]
4902 fn test_surfpool_rpc_send_transaction_config_json_serialization() {
4903 let config = SurfpoolRpcSendTransactionConfig {
4905 base: RpcSendTransactionConfig {
4906 skip_preflight: true,
4907 ..Default::default()
4908 },
4909 skip_sig_verify: Some(true),
4910 };
4911
4912 let json = serde_json::to_string(&config).unwrap();
4913 assert!(json.contains("skipSigVerify"));
4914 assert!(json.contains("skipPreflight"));
4915
4916 let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(&json).unwrap();
4918 assert_eq!(parsed.skip_sig_verify, Some(true));
4919 assert!(parsed.base.skip_preflight);
4920 }
4921
4922 #[test]
4923 fn test_surfpool_rpc_send_transaction_config_backwards_compatible() {
4924 let json = r#"{"skipPreflight": true}"#;
4926 let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(json).unwrap();
4927 assert!(parsed.base.skip_preflight);
4928 assert!(
4929 parsed.skip_sig_verify.is_none(),
4930 "skip_sig_verify should be None when not provided"
4931 );
4932 }
4933
4934 #[test]
4935 fn test_surfpool_rpc_send_transaction_config_defaults() {
4936 let config = SurfpoolRpcSendTransactionConfig::default();
4937 assert!(
4938 config.skip_sig_verify.is_none(),
4939 "skip_sig_verify should default to None"
4940 );
4941 assert!(
4942 !config.base.skip_preflight,
4943 "skip_preflight should default to false"
4944 );
4945 }
4946
4947 #[test]
4948 fn test_surfpool_rpc_send_transaction_config_with_skip_sig_verify() {
4949 let config = SurfpoolRpcSendTransactionConfig {
4950 base: RpcSendTransactionConfig::default(),
4951 skip_sig_verify: Some(true),
4952 };
4953 assert_eq!(config.skip_sig_verify, Some(true));
4954
4955 let config_false = SurfpoolRpcSendTransactionConfig {
4956 base: RpcSendTransactionConfig::default(),
4957 skip_sig_verify: Some(false),
4958 };
4959 assert_eq!(config_false.skip_sig_verify, Some(false));
4960 }
4961 }
4962}