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