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