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