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 RpcApiVersion, RpcBlockhash, RpcConfirmedTransactionStatusWithSignature, RpcContactInfo,
18 RpcInflationReward, RpcPerfSample, RpcPrioritizationFee, RpcResponseContext,
19 RpcSimulateTransactionResult,
20 },
21};
22use solana_clock::{MAX_RECENT_BLOCKHASHES, Slot, UnixTimestamp};
23use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
24use solana_message::VersionedMessage;
25use solana_pubkey::Pubkey;
26use solana_rpc_client_api::response::Response as RpcResponse;
27use solana_sdk::{
28 compute_budget::{self, ComputeBudgetInstruction},
29 instruction::CompiledInstruction,
30 system_program,
31};
32use solana_signature::Signature;
33use solana_transaction::versioned::VersionedTransaction;
34use solana_transaction_error::TransactionError;
35use solana_transaction_status::{
36 EncodedConfirmedTransactionWithStatusMeta, TransactionBinaryEncoding, TransactionStatus,
37 UiConfirmedBlock, UiTransactionEncoding,
38};
39use surfpool_types::{SimnetCommand, TransactionStatusEvent};
40
41use super::{
42 RunloopContext, State, SurfnetRpcContext,
43 utils::{decode_and_deserialize, transform_tx_metadata_to_ui_accounts, verify_pubkey},
44};
45use crate::{
46 error::{SurfpoolError, SurfpoolResult},
47 surfnet::{FINALIZATION_SLOT_THRESHOLD, GetTransactionResult, locker::SvmAccessContext},
48 types::{SurfnetTransactionStatus, surfpool_tx_metadata_to_litesvm_tx_metadata},
49};
50
51const MAX_PRIORITIZATION_FEE_BLOCKS_CACHE: usize = 150;
52
53#[rpc]
54pub trait Full {
55 type Metadata;
56
57 #[rpc(meta, name = "getInflationReward")]
117 fn get_inflation_reward(
118 &self,
119 meta: Self::Metadata,
120 address_strs: Vec<String>,
121 config: Option<RpcEpochConfig>,
122 ) -> BoxFuture<Result<Vec<Option<RpcInflationReward>>>>;
123
124 #[rpc(meta, name = "getClusterNodes")]
169 fn get_cluster_nodes(&self, meta: Self::Metadata) -> Result<Vec<RpcContactInfo>>;
170
171 #[rpc(meta, name = "getRecentPerformanceSamples")]
214 fn get_recent_performance_samples(
215 &self,
216 meta: Self::Metadata,
217 limit: Option<usize>,
218 ) -> Result<Vec<RpcPerfSample>>;
219
220 #[rpc(meta, name = "getSignatureStatuses")]
305 fn get_signature_statuses(
306 &self,
307 meta: Self::Metadata,
308 signature_strs: Vec<String>,
309 config: Option<RpcSignatureStatusConfig>,
310 ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>>;
311
312 #[rpc(meta, name = "getMaxRetransmitSlot")]
353 fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot>;
354
355 #[rpc(meta, name = "getMaxShredInsertSlot")]
396 fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot>;
397
398 #[rpc(meta, name = "requestAirdrop")]
446 fn request_airdrop(
447 &self,
448 meta: Self::Metadata,
449 pubkey_str: String,
450 lamports: u64,
451 config: Option<RpcRequestAirdropConfig>,
452 ) -> Result<String>;
453
454 #[rpc(meta, name = "sendTransaction")]
507 fn send_transaction(
508 &self,
509 meta: Self::Metadata,
510 data: String,
511 config: Option<RpcSendTransactionConfig>,
512 ) -> Result<String>;
513
514 #[rpc(meta, name = "simulateTransaction")]
582 fn simulate_transaction(
583 &self,
584 meta: Self::Metadata,
585 data: String,
586 config: Option<RpcSimulateTransactionConfig>,
587 ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>>;
588
589 #[rpc(meta, name = "minimumLedgerSlot")]
629 fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>>;
630
631 #[rpc(meta, name = "getBlock")]
688 fn get_block(
689 &self,
690 meta: Self::Metadata,
691 slot: Slot,
692 config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
693 ) -> BoxFuture<Result<Option<UiConfirmedBlock>>>;
694
695 #[rpc(meta, name = "getBlockTime")]
737 fn get_block_time(
738 &self,
739 meta: Self::Metadata,
740 slot: Slot,
741 ) -> BoxFuture<Result<Option<UnixTimestamp>>>;
742
743 #[rpc(meta, name = "getBlocks")]
789 fn get_blocks(
790 &self,
791 meta: Self::Metadata,
792 start_slot: Slot,
793 wrapper: Option<RpcBlocksConfigWrapper>,
794 config: Option<RpcContextConfig>,
795 ) -> BoxFuture<Result<Vec<Slot>>>;
796
797 #[rpc(meta, name = "getBlocksWithLimit")]
842 fn get_blocks_with_limit(
843 &self,
844 meta: Self::Metadata,
845 start_slot: Slot,
846 limit: usize,
847 config: Option<RpcContextConfig>,
848 ) -> BoxFuture<Result<Vec<Slot>>>;
849
850 #[rpc(meta, name = "getTransaction")]
921 fn get_transaction(
922 &self,
923 meta: Self::Metadata,
924 signature_str: String,
925 config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
926 ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>>;
927
928 #[rpc(meta, name = "getSignaturesForAddress")]
1005 fn get_signatures_for_address(
1006 &self,
1007 meta: Self::Metadata,
1008 address: String,
1009 config: Option<RpcSignaturesForAddressConfig>,
1010 ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>>;
1011
1012 #[rpc(meta, name = "getFirstAvailableBlock")]
1053 fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot>;
1054
1055 #[rpc(meta, name = "getLatestBlockhash")]
1109 fn get_latest_blockhash(
1110 &self,
1111 meta: Self::Metadata,
1112 config: Option<RpcContextConfig>,
1113 ) -> Result<RpcResponse<RpcBlockhash>>;
1114
1115 #[rpc(meta, name = "isBlockhashValid")]
1168 fn is_blockhash_valid(
1169 &self,
1170 meta: Self::Metadata,
1171 blockhash: String,
1172 config: Option<RpcContextConfig>,
1173 ) -> Result<RpcResponse<bool>>;
1174
1175 #[rpc(meta, name = "getFeeForMessage")]
1230 fn get_fee_for_message(
1231 &self,
1232 meta: Self::Metadata,
1233 data: String,
1234 config: Option<RpcContextConfig>,
1235 ) -> Result<RpcResponse<Option<u64>>>;
1236
1237 #[rpc(meta, name = "getStakeMinimumDelegation")]
1284 fn get_stake_minimum_delegation(
1285 &self,
1286 meta: Self::Metadata,
1287 config: Option<RpcContextConfig>,
1288 ) -> Result<RpcResponse<u64>>;
1289
1290 #[rpc(meta, name = "getRecentPrioritizationFees")]
1339 fn get_recent_prioritization_fees(
1340 &self,
1341 meta: Self::Metadata,
1342 pubkey_strs: Option<Vec<String>>,
1343 ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>>;
1344}
1345
1346#[derive(Clone)]
1347pub struct SurfpoolFullRpc;
1348impl Full for SurfpoolFullRpc {
1349 type Metadata = Option<RunloopContext>;
1350
1351 fn get_inflation_reward(
1352 &self,
1353 meta: Self::Metadata,
1354 address_strs: Vec<String>,
1355 config: Option<RpcEpochConfig>,
1356 ) -> BoxFuture<Result<Vec<Option<RpcInflationReward>>>> {
1357 Box::pin(async move {
1358 let svm_locker = meta.get_svm_locker()?;
1359
1360 let current_epoch = svm_locker.get_epoch_info().epoch;
1361 if let Some(epoch) = config.as_ref().and_then(|config| config.epoch) {
1362 if epoch > current_epoch {
1363 return Err(Error::invalid_params(
1364 "Invalid epoch. Epoch is larger that current epoch",
1365 ));
1366 }
1367 };
1368
1369 let current_slot = svm_locker.get_epoch_info().absolute_slot;
1370 if let Some(slot) = config.as_ref().and_then(|config| config.min_context_slot) {
1371 if slot > current_slot {
1372 return Err(Error::invalid_params(
1373 "Minimum context slot has not been reached",
1374 ));
1375 }
1376 };
1377
1378 let pubkeys = address_strs
1379 .iter()
1380 .map(|addr| verify_pubkey(addr))
1381 .collect::<std::result::Result<Vec<Pubkey>, SurfpoolError>>()?;
1382
1383 meta.with_svm_reader(|svm_reader| {
1384 pubkeys
1385 .iter()
1386 .map(|_| {
1387 Some(RpcInflationReward {
1388 amount: 0,
1389 commission: None,
1390 effective_slot: svm_reader.get_latest_absolute_slot(),
1391 epoch: svm_reader.latest_epoch_info().epoch,
1392 post_balance: 0,
1393 })
1394 })
1395 .collect()
1396 })
1397 .map_err(Into::into)
1398 })
1399 }
1400
1401 fn get_cluster_nodes(&self, _meta: Self::Metadata) -> Result<Vec<RpcContactInfo>> {
1402 Ok(vec![])
1403 }
1404
1405 fn get_recent_performance_samples(
1406 &self,
1407 meta: Self::Metadata,
1408 limit: Option<usize>,
1409 ) -> Result<Vec<RpcPerfSample>> {
1410 let limit = limit.unwrap_or(720);
1411 if limit > 720 {
1412 return Err(Error::invalid_params("Invalid limit; max 720"));
1413 }
1414
1415 meta.with_svm_reader(|svm_reader| {
1416 svm_reader
1417 .perf_samples
1418 .iter()
1419 .take(limit)
1420 .cloned()
1421 .collect::<Vec<_>>()
1422 })
1423 .map_err(Into::into)
1424 }
1425
1426 fn get_signature_statuses(
1427 &self,
1428 meta: Self::Metadata,
1429 signature_strs: Vec<String>,
1430 _config: Option<RpcSignatureStatusConfig>,
1431 ) -> BoxFuture<Result<RpcResponse<Vec<Option<TransactionStatus>>>>> {
1432 let signatures = match signature_strs
1433 .iter()
1434 .map(|s| {
1435 Signature::from_str(s)
1436 .map_err(|e| SurfpoolError::invalid_signature(s, e.to_string()))
1437 })
1438 .collect::<std::result::Result<Vec<Signature>, SurfpoolError>>()
1439 {
1440 Ok(sigs) => sigs,
1441 Err(e) => return e.into(),
1442 };
1443
1444 let SurfnetRpcContext {
1445 svm_locker,
1446 remote_ctx,
1447 } = match meta.get_rpc_context(()) {
1448 Ok(res) => res,
1449 Err(e) => return e.into(),
1450 };
1451 let remote_client = remote_ctx.map(|(r, _)| r);
1452
1453 Box::pin(async move {
1454 let mut responses = Vec::with_capacity(signatures.len());
1455 let mut last_latest_absolute_slot = 0;
1456 for signature in signatures.into_iter() {
1457 let res = svm_locker
1458 .get_transaction(&remote_client, &signature, RpcTransactionConfig::default())
1459 .await?;
1460
1461 last_latest_absolute_slot = svm_locker.get_latest_absolute_slot();
1462 responses.push(res.map_some_transaction_status());
1463 }
1464 Ok(RpcResponse {
1465 context: RpcResponseContext::new(last_latest_absolute_slot),
1466 value: responses,
1467 })
1468 })
1469 }
1470
1471 fn get_max_retransmit_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1472 meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1473 .map_err(Into::into)
1474 }
1475
1476 fn get_max_shred_insert_slot(&self, meta: Self::Metadata) -> Result<Slot> {
1477 meta.with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot())
1478 .map_err(Into::into)
1479 }
1480
1481 fn request_airdrop(
1482 &self,
1483 meta: Self::Metadata,
1484 pubkey_str: String,
1485 lamports: u64,
1486 _config: Option<RpcRequestAirdropConfig>,
1487 ) -> Result<String> {
1488 let pubkey = verify_pubkey(&pubkey_str)?;
1489 let svm_locker = meta.get_svm_locker()?;
1490 let res = svm_locker
1491 .airdrop(&pubkey, lamports)
1492 .map_err(|err| Error::invalid_params(format!("failed to send transaction: {err:?}")))?;
1493 Ok(res.signature.to_string())
1494 }
1495
1496 fn send_transaction(
1497 &self,
1498 meta: Self::Metadata,
1499 data: String,
1500 config: Option<RpcSendTransactionConfig>,
1501 ) -> Result<String> {
1502 let config = config.unwrap_or_default();
1503 let tx_encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
1504 let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
1505 Error::invalid_params(format!(
1506 "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
1507 ))
1508 })?;
1509 let (_, unsanitized_tx) =
1510 decode_and_deserialize::<VersionedTransaction>(data, binary_encoding)?;
1511 let signatures = unsanitized_tx.signatures.clone();
1512 let signature = signatures[0];
1513 let Some(ctx) = meta else {
1514 return Err(RpcCustomError::NodeUnhealthy {
1515 num_slots_behind: None,
1516 }
1517 .into());
1518 };
1519
1520 let (status_update_tx, status_update_rx) = crossbeam_channel::bounded(1);
1521 ctx.simnet_commands_tx
1522 .send(SimnetCommand::TransactionReceived(
1523 ctx.id,
1524 unsanitized_tx,
1525 status_update_tx,
1526 config.skip_preflight,
1527 ))
1528 .map_err(|_| RpcCustomError::NodeUnhealthy {
1529 num_slots_behind: None,
1530 })?;
1531
1532 match status_update_rx.recv() {
1533 Ok(TransactionStatusEvent::SimulationFailure((error, metadata))) => {
1534 return Err(Error {
1535 data: Some(
1536 serde_json::to_value(get_simulate_transaction_result(
1537 surfpool_tx_metadata_to_litesvm_tx_metadata(&metadata),
1538 None,
1539 Some(error.clone()),
1540 None,
1541 false,
1542 ))
1543 .map_err(|e| {
1544 Error::invalid_params(format!(
1545 "Failed to serialize simulation result: {e}"
1546 ))
1547 })?,
1548 ),
1549 message: format!(
1550 "Transaction simulation failed: {}{}",
1551 error,
1552 if metadata.logs.is_empty() {
1553 String::new()
1554 } else {
1555 format!(
1556 ": {} log messages:\n{}",
1557 metadata.logs.len(),
1558 metadata.logs.iter().map(|l| l.to_string()).join("\n")
1559 )
1560 }
1561 ),
1562 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1563 });
1564 }
1565 Ok(TransactionStatusEvent::ExecutionFailure((error, metadata))) => {
1566 return Err(Error {
1567 data: None,
1568 message: format!(
1569 "Transaction execution failed: {}{}",
1570 error,
1571 if metadata.logs.is_empty() {
1572 String::new()
1573 } else {
1574 format!(
1575 ": {} log messages:\n{}",
1576 metadata.logs.len(),
1577 metadata.logs.iter().map(|l| l.to_string()).join("\n")
1578 )
1579 }
1580 ),
1581 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1582 });
1583 }
1584 Ok(TransactionStatusEvent::VerificationFailure(signature)) => {
1585 return Err(Error {
1586 data: None,
1587 message: format!("Transaction verification failed for transaction {signature}"),
1588 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1589 });
1590 }
1591 Err(e) => {
1592 return Err(Error {
1593 data: None,
1594 message: format!("Failed to process transaction: {e}"),
1595 code: jsonrpc_core::ErrorCode::ServerError(-32002),
1596 });
1597 }
1598 Ok(TransactionStatusEvent::Success(_)) => {}
1599 }
1600 Ok(signature.to_string())
1601 }
1602
1603 fn simulate_transaction(
1604 &self,
1605 meta: Self::Metadata,
1606 data: String,
1607 config: Option<RpcSimulateTransactionConfig>,
1608 ) -> BoxFuture<Result<RpcResponse<RpcSimulateTransactionResult>>> {
1609 let config = config.unwrap_or_default();
1610
1611 if config.sig_verify && config.replace_recent_blockhash {
1612 return SurfpoolError::sig_verify_replace_recent_blockhash_collision().into();
1613 }
1614
1615 let tx_encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
1616 let binary_encoding = tx_encoding
1617 .into_binary_encoding()
1618 .ok_or_else(|| {
1619 Error::invalid_params(format!(
1620 "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
1621 ))
1622 })
1623 .unwrap();
1624 let (_, mut unsanitized_tx) =
1625 decode_and_deserialize::<VersionedTransaction>(data, binary_encoding).unwrap();
1626
1627 let SurfnetRpcContext {
1628 svm_locker,
1629 remote_ctx,
1630 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1631 Ok(res) => res,
1632 Err(e) => return e.into(),
1633 };
1634
1635 Box::pin(async move {
1636 let loaded_addresses = svm_locker
1637 .get_loaded_addresses(&remote_ctx, &unsanitized_tx.message)
1638 .await?;
1639 let pubkeys =
1640 svm_locker.get_pubkeys_from_message(&unsanitized_tx.message, loaded_addresses);
1641
1642 let SvmAccessContext {
1643 slot,
1644 inner: account_updates,
1645 latest_blockhash,
1646 latest_epoch_info,
1647 } = svm_locker
1648 .get_multiple_accounts(&remote_ctx, &pubkeys, None)
1649 .await?;
1650
1651 svm_locker.write_multiple_account_updates(&account_updates);
1652
1653 let replacement_blockhash = if config.replace_recent_blockhash {
1654 match &mut unsanitized_tx.message {
1655 VersionedMessage::Legacy(message) => {
1656 message.recent_blockhash = latest_blockhash
1657 }
1658 VersionedMessage::V0(message) => message.recent_blockhash = latest_blockhash,
1659 }
1660 Some(RpcBlockhash {
1661 blockhash: latest_blockhash.to_string(),
1662 last_valid_block_height: latest_epoch_info.block_height,
1663 })
1664 } else {
1665 None
1666 };
1667
1668 let value = match svm_locker.simulate_transaction(unsanitized_tx, config.sig_verify) {
1669 Ok(tx_info) => {
1670 let mut accounts = None;
1671 if let Some(observed_accounts) = config.accounts {
1672 let mut ui_accounts = vec![];
1673 for observed_pubkey in observed_accounts.addresses.iter() {
1674 let mut ui_account = None;
1675 for (updated_pubkey, account) in tx_info.post_accounts.iter() {
1676 if observed_pubkey.eq(&updated_pubkey.to_string()) {
1677 ui_account = Some(
1678 svm_locker
1679 .account_to_rpc_keyed_account(
1680 &*updated_pubkey,
1681 account,
1682 &RpcAccountInfoConfig::default(),
1683 None,
1684 )
1685 .account,
1686 );
1687 }
1688 }
1689 ui_accounts.push(ui_account);
1690 }
1691 accounts = Some(ui_accounts);
1692 }
1693 get_simulate_transaction_result(
1694 tx_info.meta,
1695 accounts,
1696 None,
1697 replacement_blockhash,
1698 config.inner_instructions,
1699 )
1700 }
1701 Err(tx_info) => get_simulate_transaction_result(
1702 tx_info.meta,
1703 None,
1704 Some(tx_info.err),
1705 replacement_blockhash,
1706 config.inner_instructions,
1707 ),
1708 };
1709
1710 Ok(RpcResponse {
1711 context: RpcResponseContext::new(slot),
1712 value,
1713 })
1714 })
1715 }
1716
1717 fn minimum_ledger_slot(&self, meta: Self::Metadata) -> BoxFuture<Result<Slot>> {
1718 let SurfnetRpcContext {
1719 svm_locker,
1720 remote_ctx,
1721 } = match meta.get_rpc_context(()) {
1722 Ok(res) => res,
1723 Err(e) => return e.into(),
1724 };
1725
1726 Box::pin(async move {
1727 if let Some((remote_client, _)) = remote_ctx {
1729 remote_client
1730 .client
1731 .minimum_ledger_slot()
1732 .await
1733 .map_err(|e| SurfpoolError::client_error(e).into())
1734 } else {
1735 let min_slot = svm_locker.with_svm_reader(|svm_reader| {
1736 svm_reader.blocks.keys().min().copied().unwrap_or(0)
1737 });
1738
1739 Ok(min_slot)
1740 }
1741 })
1742 }
1743
1744 fn get_block(
1745 &self,
1746 meta: Self::Metadata,
1747 slot: Slot,
1748 config: Option<RpcEncodingConfigWrapper<RpcBlockConfig>>,
1749 ) -> BoxFuture<Result<Option<UiConfirmedBlock>>> {
1750 let config = config.map(|c| c.convert_to_current()).unwrap_or_default();
1751
1752 let SurfnetRpcContext {
1753 svm_locker,
1754 remote_ctx,
1755 } = match meta.get_rpc_context(config.commitment) {
1756 Ok(res) => res,
1757 Err(e) => return e.into(),
1758 };
1759
1760 Box::pin(async move {
1761 let remote_client = remote_ctx.as_ref().map(|(client, _)| client.clone());
1762 let result = svm_locker.get_block(&remote_client, &slot, &config).await;
1763 Ok(result?.inner)
1764 })
1765 }
1766
1767 fn get_block_time(
1768 &self,
1769 meta: Self::Metadata,
1770 slot: Slot,
1771 ) -> BoxFuture<Result<Option<UnixTimestamp>>> {
1772 let svm_locker = match meta.get_svm_locker() {
1773 Ok(locker) => locker,
1774 Err(e) => return e.into(),
1775 };
1776
1777 Box::pin(async move {
1778 let block_time = svm_locker.with_svm_reader(|svm_reader| {
1779 svm_reader
1780 .blocks
1781 .get(&slot)
1782 .map(|block| (block.block_time / 1000) as UnixTimestamp)
1783 });
1784 Ok(block_time)
1785 })
1786 }
1787
1788 fn get_blocks(
1789 &self,
1790 meta: Self::Metadata,
1791 start_slot: Slot,
1792 wrapper: Option<RpcBlocksConfigWrapper>,
1793 config: Option<RpcContextConfig>,
1794 ) -> BoxFuture<Result<Vec<Slot>>> {
1795 let end_slot = match wrapper {
1796 Some(RpcBlocksConfigWrapper::EndSlotOnly(end_slot)) => end_slot,
1797 Some(RpcBlocksConfigWrapper::ConfigOnly(_)) => None,
1798 None => None,
1799 };
1800
1801 let config = config.unwrap_or_default();
1802 let commitment = config.commitment.unwrap_or(CommitmentConfig {
1804 commitment: CommitmentLevel::Processed,
1805 });
1806
1807 const MAX_SLOT_RANGE: u64 = 500_000;
1808 if let Some(end) = end_slot {
1809 if end < start_slot {
1810 return Box::pin(async { Ok(vec![]) });
1812 }
1813 if end.saturating_sub(start_slot) > MAX_SLOT_RANGE {
1814 return Box::pin(async move {
1815 Err(Error::invalid_params(format!(
1816 "Slot range too large. Maximum: {}, Requested: {}",
1817 MAX_SLOT_RANGE,
1818 end.saturating_sub(start_slot)
1819 )))
1820 });
1821 }
1822 }
1823
1824 let SurfnetRpcContext {
1825 svm_locker,
1826 remote_ctx,
1827 } = match meta.get_rpc_context(commitment) {
1828 Ok(res) => res,
1829 Err(e) => return e.into(),
1830 };
1831
1832 Box::pin(async move {
1833 let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
1834 let effective_end_slot = end_slot
1835 .map(|end| end.min(committed_latest_slot))
1836 .unwrap_or(committed_latest_slot);
1837
1838 let (local_min_slot, local_slots, effective_end_slot) =
1839 if effective_end_slot < start_slot {
1840 (None, vec![], effective_end_slot)
1841 } else {
1842 svm_locker.with_svm_reader(|svm_reader| {
1843 let local_min_slot = svm_reader.blocks.keys().min().copied();
1844
1845 let local_slots: Vec<Slot> = svm_reader
1846 .blocks
1847 .keys()
1848 .filter(|&&slot| {
1849 slot >= start_slot
1850 && slot <= effective_end_slot
1851 && slot <= committed_latest_slot
1852 })
1853 .copied()
1854 .collect();
1855
1856 (local_min_slot, local_slots, effective_end_slot)
1857 })
1858 };
1859
1860 if let Some(min_context_slot) = config.min_context_slot {
1861 if committed_latest_slot < min_context_slot {
1862 return Err(RpcCustomError::MinContextSlotNotReached {
1863 context_slot: min_context_slot,
1864 }
1865 .into());
1866 }
1867 }
1868
1869 if effective_end_slot.saturating_sub(start_slot) > MAX_SLOT_RANGE {
1870 return Err(Error::invalid_params(format!(
1871 "Slot range too large. Maximum: {}, Requested: {}",
1872 MAX_SLOT_RANGE,
1873 effective_end_slot.saturating_sub(start_slot)
1874 )));
1875 }
1876
1877 let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
1878 (&remote_ctx, local_min_slot)
1879 {
1880 if start_slot < local_min {
1881 let remote_end = effective_end_slot.min(local_min.saturating_sub(1));
1882 if start_slot <= remote_end {
1883 remote_client
1884 .client
1885 .get_blocks(start_slot, Some(remote_end))
1886 .await
1887 .unwrap_or_else(|_| vec![])
1888 } else {
1889 vec![]
1890 }
1891 } else {
1892 vec![]
1893 }
1894 } else if remote_ctx.is_some() && local_min_slot.is_none() {
1895 remote_ctx
1896 .as_ref()
1897 .unwrap()
1898 .0
1899 .client
1900 .get_blocks(start_slot, Some(effective_end_slot))
1901 .await
1902 .unwrap_or_else(|_| vec![])
1903 } else {
1904 vec![]
1905 };
1906
1907 let mut combined_slots = remote_slots;
1909 combined_slots.extend(local_slots);
1910 combined_slots.sort_unstable();
1911 combined_slots.dedup();
1912
1913 if combined_slots.len() > MAX_SLOT_RANGE as usize {
1914 combined_slots.truncate(MAX_SLOT_RANGE as usize);
1915 }
1916
1917 Ok(combined_slots)
1918 })
1919 }
1920
1921 fn get_blocks_with_limit(
1922 &self,
1923 meta: Self::Metadata,
1924 start_slot: Slot,
1925 limit: usize,
1926 config: Option<RpcContextConfig>,
1927 ) -> BoxFuture<Result<Vec<Slot>>> {
1928 let config = config.unwrap_or_default();
1929 let commitment = config.commitment.unwrap_or(CommitmentConfig {
1930 commitment: CommitmentLevel::Processed,
1931 });
1932
1933 if limit == 0 {
1934 return Box::pin(
1935 async move { Err(Error::invalid_params("Limit must be greater than 0")) },
1936 );
1937 }
1938
1939 const MAX_LIMIT: usize = 500_000;
1940 if limit > MAX_LIMIT {
1941 return Box::pin(async move {
1942 Err(Error::invalid_params(format!(
1943 "Limit too large. Maximum limit allowed: {}",
1944 MAX_LIMIT
1945 )))
1946 });
1947 }
1948
1949 let SurfnetRpcContext {
1950 svm_locker,
1951 remote_ctx,
1952 } = match meta.get_rpc_context(commitment) {
1953 Ok(res) => res,
1954 Err(e) => return e.into(),
1955 };
1956
1957 Box::pin(async move {
1958 let committed_latest_slot = svm_locker.get_slot_for_commitment(&commitment);
1959 let (local_min_slot, local_slots) = svm_locker.with_svm_reader(|svm_reader| {
1960 let local_min_slot = svm_reader.blocks.keys().min().copied();
1961
1962 let local_slots: Vec<Slot> = svm_reader
1963 .blocks
1964 .keys()
1965 .filter(|&&slot| slot >= start_slot && slot <= committed_latest_slot)
1966 .copied()
1967 .collect();
1968
1969 (local_min_slot, local_slots)
1970 });
1971
1972 if let Some(min_context_slot) = config.min_context_slot {
1973 if committed_latest_slot < min_context_slot {
1974 return Err(RpcCustomError::MinContextSlotNotReached {
1975 context_slot: min_context_slot,
1976 }
1977 .into());
1978 }
1979 }
1980
1981 let remote_slots = if let (Some((remote_client, _)), Some(local_min)) =
1983 (&remote_ctx, local_min_slot)
1984 {
1985 if start_slot < local_min {
1986 let remote_end = committed_latest_slot.min(local_min.saturating_sub(1));
1987 if start_slot <= remote_end {
1988 remote_client
1989 .client
1990 .get_blocks(start_slot, Some(remote_end))
1991 .await
1992 .unwrap_or_else(|_| vec![])
1993 } else {
1994 vec![]
1995 }
1996 } else {
1997 vec![]
1998 }
1999 } else if remote_ctx.is_some() && local_min_slot.is_none() {
2000 remote_ctx
2002 .as_ref()
2003 .unwrap()
2004 .0
2005 .client
2006 .get_blocks(start_slot, Some(committed_latest_slot))
2007 .await
2008 .unwrap_or_else(|_| vec![])
2009 } else {
2010 vec![]
2011 };
2012
2013 let mut combined_slots = remote_slots;
2014 combined_slots.extend(local_slots);
2015 combined_slots.sort_unstable();
2016 combined_slots.dedup();
2017
2018 combined_slots.truncate(limit);
2020
2021 Ok(combined_slots)
2022 })
2023 }
2024
2025 fn get_transaction(
2026 &self,
2027 meta: Self::Metadata,
2028 signature_str: String,
2029 config: Option<RpcEncodingConfigWrapper<RpcTransactionConfig>>,
2030 ) -> BoxFuture<Result<Option<EncodedConfirmedTransactionWithStatusMeta>>> {
2031 let config = config.map(|c| c.convert_to_current()).unwrap_or_default();
2032
2033 Box::pin(async move {
2034 let signature = Signature::from_str(&signature_str)
2035 .map_err(|e| SurfpoolError::invalid_signature(&signature_str, e.to_string()))?;
2036
2037 let SurfnetRpcContext {
2038 svm_locker,
2039 remote_ctx,
2040 } = meta.get_rpc_context(())?;
2041
2042 match svm_locker
2045 .get_transaction(&remote_ctx.map(|(r, _)| r), &signature, config)
2046 .await?
2047 {
2048 GetTransactionResult::None(_) => Ok(None),
2049 GetTransactionResult::FoundTransaction(_, meta, _) => Ok(Some(meta)),
2050 }
2051 })
2052 }
2053
2054 fn get_signatures_for_address(
2055 &self,
2056 meta: Self::Metadata,
2057 address: String,
2058 config: Option<RpcSignaturesForAddressConfig>,
2059 ) -> BoxFuture<Result<Vec<RpcConfirmedTransactionStatusWithSignature>>> {
2060 let pubkey = match verify_pubkey(&address) {
2061 Ok(s) => s,
2062 Err(e) => return e.into(),
2063 };
2064 let SurfnetRpcContext {
2065 svm_locker,
2066 remote_ctx,
2067 } = match meta.get_rpc_context(()) {
2068 Ok(res) => res,
2069 Err(e) => return e.into(),
2070 };
2071
2072 Box::pin(async move {
2073 let signatures = svm_locker
2074 .get_signatures_for_address(&remote_ctx, &pubkey, config)
2075 .await?
2076 .inner;
2077 Ok(signatures)
2078 })
2079 }
2080
2081 fn get_first_available_block(&self, meta: Self::Metadata) -> Result<Slot> {
2082 meta.with_svm_reader(|svm_reader| {
2083 svm_reader.blocks.keys().min().copied().unwrap_or_default()
2084 })
2085 .map_err(Into::into)
2086 }
2087
2088 fn get_latest_blockhash(
2089 &self,
2090 meta: Self::Metadata,
2091 _config: Option<RpcContextConfig>,
2092 ) -> Result<RpcResponse<RpcBlockhash>> {
2093 meta.with_svm_reader(|svm_reader| {
2094 let last_valid_block_height =
2095 svm_reader.latest_epoch_info.block_height + MAX_RECENT_BLOCKHASHES as u64;
2096 let value = RpcBlockhash {
2097 blockhash: svm_reader.latest_blockhash().to_string(),
2098 last_valid_block_height,
2099 };
2100 RpcResponse {
2101 context: RpcResponseContext {
2102 slot: svm_reader.get_latest_absolute_slot(),
2103 api_version: Some(RpcApiVersion::default()),
2104 },
2105 value,
2106 }
2107 })
2108 .map_err(Into::into)
2109 }
2110
2111 fn is_blockhash_valid(
2112 &self,
2113 meta: Self::Metadata,
2114 blockhash: String,
2115 config: Option<RpcContextConfig>,
2116 ) -> Result<RpcResponse<bool>> {
2117 let hash = blockhash
2118 .parse::<solana_hash::Hash>()
2119 .map_err(|e| Error::invalid_params(format!("Invalid blockhash: {e:?}")))?;
2120 let config = config.unwrap_or_default();
2121
2122 let svm_locker = meta.get_svm_locker()?;
2123
2124 let committed_latest_slot =
2125 svm_locker.get_slot_for_commitment(&config.commitment.unwrap_or_default());
2126
2127 let is_valid =
2128 svm_locker.with_svm_reader(|svm_reader| svm_reader.check_blockhash_is_recent(&hash));
2129
2130 if let Some(min_context_slot) = config.min_context_slot {
2131 if committed_latest_slot < min_context_slot {
2132 return Err(RpcCustomError::MinContextSlotNotReached {
2133 context_slot: min_context_slot,
2134 }
2135 .into());
2136 }
2137 }
2138
2139 Ok(RpcResponse {
2140 context: RpcResponseContext::new(committed_latest_slot),
2141 value: is_valid,
2142 })
2143 }
2144
2145 fn get_fee_for_message(
2146 &self,
2147 meta: Self::Metadata,
2148 encoded: String,
2149 _config: Option<RpcContextConfig>, ) -> Result<RpcResponse<Option<u64>>> {
2151 let (_, message) =
2152 decode_and_deserialize::<VersionedMessage>(encoded, TransactionBinaryEncoding::Base64)?;
2153
2154 meta.with_svm_reader(|svm_reader| RpcResponse {
2155 context: RpcResponseContext::new(svm_reader.get_latest_absolute_slot()),
2156 value: Some((message.header().num_required_signatures as u64) * 5000),
2157 })
2158 .map_err(Into::into)
2159 }
2160
2161 fn get_stake_minimum_delegation(
2162 &self,
2163 meta: Self::Metadata,
2164 config: Option<RpcContextConfig>,
2165 ) -> Result<RpcResponse<u64>> {
2166 let config = config.unwrap_or_default();
2167 let commitment_config = config.commitment.unwrap_or(CommitmentConfig {
2168 commitment: CommitmentLevel::Processed,
2169 });
2170
2171 meta.with_svm_reader(|svm_reader| {
2172 let context_slot = match commitment_config.commitment {
2173 CommitmentLevel::Processed => svm_reader.get_latest_absolute_slot(),
2174 CommitmentLevel::Confirmed => {
2175 svm_reader.get_latest_absolute_slot().saturating_sub(1)
2176 }
2177 CommitmentLevel::Finalized => svm_reader
2178 .get_latest_absolute_slot()
2179 .saturating_sub(FINALIZATION_SLOT_THRESHOLD),
2180 };
2181
2182 RpcResponse {
2183 context: RpcResponseContext::new(context_slot),
2184 value: 0,
2185 }
2186 })
2187 .map_err(Into::into)
2188 }
2189
2190 fn get_recent_prioritization_fees(
2191 &self,
2192 meta: Self::Metadata,
2193 pubkey_strs: Option<Vec<String>>,
2194 ) -> BoxFuture<Result<Vec<RpcPrioritizationFee>>> {
2195 let pubkeys_filter = match pubkey_strs
2196 .map(|strs| {
2197 strs.iter()
2198 .map(|s| verify_pubkey(s))
2199 .collect::<SurfpoolResult<Vec<_>>>()
2200 })
2201 .transpose()
2202 {
2203 Ok(pubkeys) => pubkeys,
2204 Err(e) => return e.into(),
2205 };
2206
2207 let SurfnetRpcContext {
2208 svm_locker,
2209 remote_ctx,
2210 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2211 Ok(res) => res,
2212 Err(e) => return e.into(),
2213 };
2214
2215 Box::pin(async move {
2216 let (blocks, transactions) = svm_locker.with_svm_reader(|svm_reader| {
2217 (svm_reader.blocks.clone(), svm_reader.transactions.clone())
2218 });
2219
2220 let recent_headers = blocks
2222 .into_iter()
2223 .sorted_by_key(|(slot, _)| std::cmp::Reverse(*slot))
2224 .take(MAX_PRIORITIZATION_FEE_BLOCKS_CACHE)
2225 .collect::<Vec<_>>();
2226
2227 let recent_transactions = recent_headers
2229 .into_iter()
2230 .flat_map(|(slot, header)| {
2231 header
2232 .signatures
2233 .iter()
2234 .filter_map(|signature| {
2235 transactions.get(signature).map(|tx| (slot, tx))
2237 })
2238 .collect::<Vec<_>>()
2239 })
2240 .collect::<Vec<_>>();
2241
2242 fn get_compute_unit_price(ix: CompiledInstruction, accounts: &[Pubkey]) -> Option<u64> {
2244 let program_account = accounts.get(ix.program_id_index as usize)?;
2245 if *program_account != compute_budget::id() {
2246 return None;
2247 }
2248
2249 if let Ok(ComputeBudgetInstruction::SetComputeUnitPrice(price)) =
2250 borsh::from_slice::<ComputeBudgetInstruction>(&ix.data)
2251 {
2252 return Some(price);
2253 }
2254
2255 None
2256 }
2257
2258 let mut prioritization_fees = vec![];
2259 for (slot, tx) in recent_transactions {
2260 match tx {
2261 SurfnetTransactionStatus::Received => {}
2262 SurfnetTransactionStatus::Processed(status_meta) => {
2263 let tx = &status_meta.transaction;
2264
2265 let loaded_addresses = svm_locker
2269 .get_loaded_addresses(&remote_ctx, &tx.message)
2270 .await?;
2271 let account_keys =
2272 svm_locker.get_pubkeys_from_message(&tx.message, loaded_addresses);
2273
2274 let instructions = match &tx.message {
2275 VersionedMessage::V0(msg) => &msg.instructions,
2276 VersionedMessage::Legacy(msg) => &msg.instructions,
2277 };
2278
2279 let compute_unit_prices = instructions
2281 .iter()
2282 .filter_map(|ix| get_compute_unit_price(ix.clone(), &account_keys))
2283 .collect::<Vec<_>>();
2284
2285 for compute_unit_price in compute_unit_prices {
2286 if let Some(pubkeys_filter) = &pubkeys_filter {
2287 if !pubkeys_filter
2290 .iter()
2291 .any(|pk| account_keys.iter().any(|a| a == pk))
2292 {
2293 continue;
2294 }
2295 }
2296 prioritization_fees.push(RpcPrioritizationFee {
2298 slot,
2299 prioritization_fee: compute_unit_price,
2300 });
2301 }
2302 }
2303 }
2304 }
2305 Ok(prioritization_fees)
2306 })
2307 }
2308}
2309
2310fn get_simulate_transaction_result(
2311 metadata: TransactionMetadata,
2312 accounts: Option<Vec<Option<UiAccount>>>,
2313 error: Option<TransactionError>,
2314 replacement_blockhash: Option<RpcBlockhash>,
2315 include_inner_instructions: bool,
2316) -> RpcSimulateTransactionResult {
2317 RpcSimulateTransactionResult {
2318 accounts,
2319 err: error,
2320 inner_instructions: if include_inner_instructions {
2321 Some(transform_tx_metadata_to_ui_accounts(metadata.clone()))
2322 } else {
2323 None
2324 },
2325 logs: Some(metadata.logs.clone()),
2326 replacement_blockhash,
2327 return_data: if metadata.return_data.program_id == system_program::id()
2328 && metadata.return_data.data.len() == 0
2329 {
2330 None
2331 } else {
2332 Some(metadata.return_data.clone().into())
2333 },
2334 units_consumed: Some(metadata.compute_units_consumed),
2335 }
2336}
2337
2338#[cfg(test)]
2339mod tests {
2340
2341 use std::thread::JoinHandle;
2342
2343 use base64::{Engine, prelude::BASE64_STANDARD};
2344 use crossbeam_channel::Receiver;
2345 use solana_account_decoder::{UiAccount, UiAccountData, UiAccountEncoding};
2346 use solana_client::rpc_config::RpcSimulateTransactionAccountsConfig;
2347 use solana_commitment_config::CommitmentConfig;
2348 use solana_hash::Hash;
2349 use solana_keypair::Keypair;
2350 use solana_message::{
2351 MessageHeader, legacy::Message as LegacyMessage, v0::Message as V0Message,
2352 };
2353 use solana_native_token::LAMPORTS_PER_SOL;
2354 use solana_pubkey::Pubkey;
2355 use solana_sdk::{instruction::Instruction, system_instruction};
2356 use solana_signer::Signer;
2357 use solana_system_interface::program as system_program;
2358 use solana_transaction::{
2359 Transaction,
2360 versioned::{Legacy, TransactionVersion},
2361 };
2362 use solana_transaction_error::TransactionError;
2363 use solana_transaction_status::{
2364 EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiMessage,
2365 UiRawMessage, UiTransaction,
2366 };
2367 use surfpool_types::{SimnetCommand, TransactionConfirmationStatus};
2368 use test_case::test_case;
2369
2370 use super::*;
2371 use crate::{
2372 surfnet::{BlockHeader, BlockIdentifier, remote::SurfnetRemoteClient},
2373 tests::helpers::TestSetup,
2374 types::TransactionWithStatusMeta,
2375 };
2376
2377 fn build_v0_transaction(
2378 payer: &Pubkey,
2379 signers: &[&Keypair],
2380 instructions: &[Instruction],
2381 recent_blockhash: &Hash,
2382 ) -> VersionedTransaction {
2383 let msg = VersionedMessage::V0(
2384 V0Message::try_compile(&payer, instructions, &[], *recent_blockhash).unwrap(),
2385 );
2386 VersionedTransaction::try_new(msg, signers).unwrap()
2387 }
2388
2389 fn build_legacy_transaction(
2390 payer: &Pubkey,
2391 signers: &[&Keypair],
2392 instructions: &[Instruction],
2393 recent_blockhash: &Hash,
2394 ) -> VersionedTransaction {
2395 let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
2396 instructions,
2397 Some(payer),
2398 recent_blockhash,
2399 ));
2400 VersionedTransaction::try_new(msg, signers).unwrap()
2401 }
2402
2403 async fn send_and_await_transaction(
2404 tx: VersionedTransaction,
2405 setup: TestSetup<SurfpoolFullRpc>,
2406 mempool_rx: Receiver<SimnetCommand>,
2407 ) -> JoinHandle<String> {
2408 let setup_clone = setup.clone();
2409 let handle = hiro_system_kit::thread_named("send_tx")
2410 .spawn(move || {
2411 let res = setup_clone
2412 .rpc
2413 .send_transaction(
2414 Some(setup_clone.context),
2415 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2416 None,
2417 )
2418 .unwrap();
2419
2420 res
2421 })
2422 .unwrap();
2423
2424 match mempool_rx.recv() {
2425 Ok(SimnetCommand::TransactionReceived(_, tx, status_tx, _)) => {
2426 let mut writer = setup.context.svm_locker.0.write().await;
2427 let slot = writer.get_latest_absolute_slot();
2428 writer
2429 .transactions_queued_for_confirmation
2430 .push_back((tx.clone(), status_tx.clone()));
2431 writer.transactions.insert(
2432 tx.signatures[0],
2433 SurfnetTransactionStatus::Processed(Box::new(TransactionWithStatusMeta {
2434 slot,
2435 transaction: tx,
2436 ..Default::default()
2437 })),
2438 );
2439 status_tx
2440 .send(TransactionStatusEvent::Success(
2441 TransactionConfirmationStatus::Confirmed,
2442 ))
2443 .unwrap();
2444 }
2445 _ => panic!("failed to receive transaction from mempool"),
2446 }
2447
2448 handle
2449 }
2450
2451 #[test_case(None, false ; "when limit is None")]
2452 #[test_case(Some(1), false ; "when limit is ok")]
2453 #[test_case(Some(1000), true ; "when limit is above max spec")]
2454 fn test_get_recent_performance_samples(limit: Option<usize>, fails: bool) {
2455 let setup = TestSetup::new(SurfpoolFullRpc);
2456 let res = setup
2457 .rpc
2458 .get_recent_performance_samples(Some(setup.context), limit);
2459
2460 if fails {
2461 assert!(res.is_err());
2462 } else {
2463 assert!(res.is_ok());
2464 }
2465 }
2466
2467 #[tokio::test(flavor = "multi_thread")]
2468 async fn test_get_signature_statuses() {
2469 let pks = (0..10).map(|_| Pubkey::new_unique());
2470 let valid_txs = pks.len();
2471 let invalid_txs = pks.len();
2472 let payer = Keypair::new();
2473 let mut setup = TestSetup::new(SurfpoolFullRpc).without_blockhash().await;
2474 let recent_blockhash = setup
2475 .context
2476 .svm_locker
2477 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2478
2479 let valid = pks
2480 .clone()
2481 .map(|pk| {
2482 Transaction::new_signed_with_payer(
2483 &[system_instruction::transfer(
2484 &payer.pubkey(),
2485 &pk,
2486 LAMPORTS_PER_SOL,
2487 )],
2488 Some(&payer.pubkey()),
2489 &[payer.insecure_clone()],
2490 recent_blockhash,
2491 )
2492 })
2493 .collect::<Vec<_>>();
2494 let invalid = pks
2495 .map(|pk| {
2496 Transaction::new_unsigned(LegacyMessage::new(
2497 &[system_instruction::transfer(
2498 &pk,
2499 &payer.pubkey(),
2500 LAMPORTS_PER_SOL,
2501 )],
2502 Some(&payer.pubkey()),
2503 ))
2504 })
2505 .collect::<Vec<_>>();
2506 let txs = valid
2507 .into_iter()
2508 .chain(invalid.into_iter())
2509 .map(|tx| VersionedTransaction {
2510 signatures: tx.signatures,
2511 message: VersionedMessage::Legacy(tx.message),
2512 })
2513 .collect::<Vec<_>>();
2514 let _ = setup.context.svm_locker.0.write().await.airdrop(
2515 &payer.pubkey(),
2516 (valid_txs + invalid_txs) as u64 * 2 * LAMPORTS_PER_SOL,
2517 );
2518 setup.process_txs(txs.clone()).await;
2519
2520 let res = setup
2521 .rpc
2522 .get_signature_statuses(
2523 Some(setup.context),
2524 txs.iter().map(|tx| tx.signatures[0].to_string()).collect(),
2525 None,
2526 )
2527 .await
2528 .unwrap();
2529
2530 assert_eq!(
2531 res.value
2532 .iter()
2533 .filter(|status| {
2534 println!("status: {:?}", status);
2535 if let Some(s) = status {
2536 s.status.is_ok()
2537 } else {
2538 false
2539 }
2540 })
2541 .count(),
2542 valid_txs,
2543 "incorrect number of valid txs"
2544 );
2545 assert_eq!(
2546 res.value
2547 .iter()
2548 .filter(|status| if let Some(s) = status {
2549 s.status.is_err()
2550 } else {
2551 true
2552 })
2553 .count(),
2554 invalid_txs,
2555 "incorrect number of invalid txs"
2556 );
2557 }
2558
2559 #[test]
2560 fn test_request_airdrop() {
2561 let pk = Pubkey::new_unique();
2562 let lamports = 1000;
2563 let setup = TestSetup::new(SurfpoolFullRpc);
2564 let res = setup
2565 .rpc
2566 .request_airdrop(Some(setup.context.clone()), pk.to_string(), lamports, None)
2567 .unwrap();
2568 let sig = Signature::from_str(res.as_str()).unwrap();
2569 let state_reader = setup.context.svm_locker.0.blocking_read();
2570 assert_eq!(
2571 state_reader.inner.get_account(&pk).unwrap().lamports,
2572 lamports,
2573 "airdropped amount is incorrect"
2574 );
2575 assert!(
2576 state_reader.inner.get_transaction(&sig).is_some(),
2577 "transaction is not found in the SVM"
2578 );
2579 assert!(
2580 state_reader.transactions.get(&sig).is_some(),
2581 "transaction is not found in the history"
2582 );
2583 }
2584
2585 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2586 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2587 #[tokio::test(flavor = "multi_thread")]
2588 async fn test_send_transaction(version: TransactionVersion) {
2589 let payer = Keypair::new();
2590 let pk = Pubkey::new_unique();
2591 let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
2592 let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
2593 let recent_blockhash = setup
2594 .context
2595 .svm_locker
2596 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2597
2598 let tx = match version {
2599 TransactionVersion::Legacy(_) => build_legacy_transaction(
2600 &payer.pubkey(),
2601 &[&payer.insecure_clone()],
2602 &[system_instruction::transfer(
2603 &payer.pubkey(),
2604 &pk,
2605 LAMPORTS_PER_SOL,
2606 )],
2607 &recent_blockhash,
2608 ),
2609 TransactionVersion::Number(0) => build_v0_transaction(
2610 &payer.pubkey(),
2611 &[&payer.insecure_clone()],
2612 &[system_instruction::transfer(
2613 &payer.pubkey(),
2614 &pk,
2615 LAMPORTS_PER_SOL,
2616 )],
2617 &recent_blockhash,
2618 ),
2619 _ => unimplemented!(),
2620 };
2621
2622 let _ = setup
2623 .context
2624 .svm_locker
2625 .0
2626 .write()
2627 .await
2628 .airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);
2629
2630 let handle = send_and_await_transaction(tx.clone(), setup.clone(), mempool_rx).await;
2631 assert_eq!(
2632 handle.join().unwrap(),
2633 tx.signatures[0].to_string(),
2634 "incorrect signature"
2635 );
2636 }
2637
2638 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2639 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2640 #[tokio::test(flavor = "multi_thread")]
2641 async fn test_simulate_transaction(version: TransactionVersion) {
2642 let payer = Keypair::new();
2643 let pk = Pubkey::new_unique();
2644 let lamports = LAMPORTS_PER_SOL;
2645 let setup = TestSetup::new(SurfpoolFullRpc);
2646 let recent_blockhash = setup
2647 .context
2648 .svm_locker
2649 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2650
2651 let _ = setup
2652 .rpc
2653 .request_airdrop(
2654 Some(setup.context.clone()),
2655 payer.pubkey().to_string(),
2656 2 * lamports,
2657 None,
2658 )
2659 .unwrap();
2660
2661 let tx = match version {
2662 TransactionVersion::Legacy(_) => build_legacy_transaction(
2663 &payer.pubkey(),
2664 &[&payer.insecure_clone()],
2665 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
2666 &recent_blockhash,
2667 ),
2668 TransactionVersion::Number(0) => build_v0_transaction(
2669 &payer.pubkey(),
2670 &[&payer.insecure_clone()],
2671 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
2672 &recent_blockhash,
2673 ),
2674 _ => unimplemented!(),
2675 };
2676
2677 let simulation_res = setup
2678 .rpc
2679 .simulate_transaction(
2680 Some(setup.context),
2681 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2682 Some(RpcSimulateTransactionConfig {
2683 sig_verify: true,
2684 replace_recent_blockhash: false,
2685 commitment: Some(CommitmentConfig::finalized()),
2686 encoding: None,
2687 accounts: Some(RpcSimulateTransactionAccountsConfig {
2688 encoding: None,
2689 addresses: vec![pk.to_string()],
2690 }),
2691 min_context_slot: None,
2692 inner_instructions: false,
2693 }),
2694 )
2695 .await
2696 .unwrap();
2697
2698 assert_eq!(
2699 simulation_res.value.err, None,
2700 "Unexpected simulation error"
2701 );
2702 assert_eq!(
2703 simulation_res.value.accounts,
2704 Some(vec![Some(UiAccount {
2705 lamports,
2706 data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
2707 owner: system_program::id().to_string(),
2708 executable: false,
2709 rent_epoch: 0,
2710 space: Some(0),
2711 })]),
2712 "Wrong account content"
2713 );
2714 }
2715
2716 #[tokio::test(flavor = "multi_thread")]
2717 async fn test_simulate_transaction_no_signers() {
2718 let payer = Keypair::new();
2719 let pk = Pubkey::new_unique();
2720 let lamports = LAMPORTS_PER_SOL;
2721 let setup = TestSetup::new(SurfpoolFullRpc);
2722 setup
2723 .context
2724 .svm_locker
2725 .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
2726 let recent_blockhash = setup
2727 .context
2728 .svm_locker
2729 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2730
2731 let _ = setup
2732 .rpc
2733 .request_airdrop(
2734 Some(setup.context.clone()),
2735 payer.pubkey().to_string(),
2736 2 * lamports,
2737 None,
2738 )
2739 .unwrap();
2740 let mut msg = LegacyMessage::new(
2742 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
2743 Some(&payer.pubkey()),
2744 );
2745 msg.recent_blockhash = recent_blockhash;
2746 let tx = Transaction::new_unsigned(msg);
2747
2748 let simulation_res = setup
2749 .rpc
2750 .simulate_transaction(
2751 Some(setup.context),
2752 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2753 Some(RpcSimulateTransactionConfig {
2754 sig_verify: false,
2755 replace_recent_blockhash: false,
2756 commitment: Some(CommitmentConfig::finalized()),
2757 encoding: None,
2758 accounts: Some(RpcSimulateTransactionAccountsConfig {
2759 encoding: None,
2760 addresses: vec![pk.to_string()],
2761 }),
2762 min_context_slot: None,
2763 inner_instructions: false,
2764 }),
2765 )
2766 .await
2767 .unwrap();
2768
2769 assert_eq!(
2770 simulation_res.value.err, None,
2771 "Unexpected simulation error"
2772 );
2773 assert_eq!(
2774 simulation_res.value.accounts,
2775 Some(vec![Some(UiAccount {
2776 lamports,
2777 data: UiAccountData::Binary(BASE64_STANDARD.encode(""), UiAccountEncoding::Base64),
2778 owner: system_program::id().to_string(),
2779 executable: false,
2780 rent_epoch: 0,
2781 space: Some(0),
2782 })]),
2783 "Wrong account content"
2784 );
2785 }
2786 #[tokio::test(flavor = "multi_thread")]
2787 async fn test_simulate_transaction_no_signers_err() {
2788 let payer = Keypair::new();
2789 let pk = Pubkey::new_unique();
2790 let lamports = LAMPORTS_PER_SOL;
2791 let setup = TestSetup::new(SurfpoolFullRpc);
2792 let recent_blockhash = setup
2793 .context
2794 .svm_locker
2795 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2796
2797 let _ = setup
2798 .rpc
2799 .request_airdrop(
2800 Some(setup.context.clone()),
2801 payer.pubkey().to_string(),
2802 2 * lamports,
2803 None,
2804 )
2805 .unwrap();
2806 setup
2807 .context
2808 .svm_locker
2809 .with_svm_writer(|svm_writer| svm_writer.inner.set_sigverify(false));
2810
2811 let mut msg = LegacyMessage::new(
2813 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
2814 Some(&payer.pubkey()),
2815 );
2816 msg.recent_blockhash = recent_blockhash;
2817 let tx = Transaction::new_unsigned(msg);
2818
2819 let simulation_res = setup
2820 .rpc
2821 .simulate_transaction(
2822 Some(setup.context),
2823 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2824 Some(RpcSimulateTransactionConfig {
2825 sig_verify: true,
2826 replace_recent_blockhash: false,
2827 commitment: Some(CommitmentConfig::finalized()),
2828 encoding: None,
2829 accounts: Some(RpcSimulateTransactionAccountsConfig {
2830 encoding: None,
2831 addresses: vec![pk.to_string()],
2832 }),
2833 min_context_slot: None,
2834 inner_instructions: false,
2835 }),
2836 )
2837 .await
2838 .unwrap();
2839
2840 assert_eq!(
2841 simulation_res.value.err,
2842 Some(TransactionError::SignatureFailure)
2843 );
2844 }
2845
2846 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2847 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2848 #[tokio::test(flavor = "multi_thread")]
2849 async fn test_simulate_transaction_replace_recent_blockhash(version: TransactionVersion) {
2850 let payer = Keypair::new();
2851 let pk = Pubkey::new_unique();
2852 let lamports = LAMPORTS_PER_SOL;
2853 let setup = TestSetup::new(SurfpoolFullRpc);
2854 let recent_blockhash = setup
2855 .context
2856 .svm_locker
2857 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2858 let block_height = setup
2859 .context
2860 .svm_locker
2861 .with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.block_height);
2862 let bad_blockhash = Hash::new_unique();
2863
2864 let _ = setup
2865 .rpc
2866 .request_airdrop(
2867 Some(setup.context.clone()),
2868 payer.pubkey().to_string(),
2869 2 * lamports,
2870 None,
2871 )
2872 .unwrap();
2873
2874 let mut tx = match version {
2875 TransactionVersion::Legacy(_) => build_legacy_transaction(
2876 &payer.pubkey(),
2877 &[&payer.insecure_clone()],
2878 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
2879 &recent_blockhash,
2880 ),
2881 TransactionVersion::Number(0) => build_v0_transaction(
2882 &payer.pubkey(),
2883 &[&payer.insecure_clone()],
2884 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
2885 &recent_blockhash,
2886 ),
2887 _ => unimplemented!(),
2888 };
2889 match &mut tx.message {
2890 VersionedMessage::Legacy(msg) => {
2891 msg.recent_blockhash = bad_blockhash;
2892 }
2893 VersionedMessage::V0(msg) => {
2894 msg.recent_blockhash = bad_blockhash;
2895 }
2896 }
2897
2898 let invalid_config = RpcSimulateTransactionConfig {
2899 sig_verify: true,
2900 replace_recent_blockhash: true,
2901 commitment: Some(CommitmentConfig::finalized()),
2902 encoding: None,
2903 accounts: Some(RpcSimulateTransactionAccountsConfig {
2904 encoding: None,
2905 addresses: vec![pk.to_string()],
2906 }),
2907 min_context_slot: None,
2908 inner_instructions: false,
2909 };
2910 let err = setup
2911 .rpc
2912 .simulate_transaction(
2913 Some(setup.context.clone()),
2914 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2915 Some(invalid_config.clone()),
2916 )
2917 .await
2918 .unwrap_err();
2919
2920 assert_eq!(
2921 err.message, "sigVerify may not be used with replaceRecentBlockhash",
2922 "sigVerify should not be allowed to be used with replaceRecentBlockhash"
2923 );
2924
2925 let mut valid_config = invalid_config;
2926 valid_config.sig_verify = false;
2927 let simulation_res = setup
2928 .rpc
2929 .simulate_transaction(
2930 Some(setup.context),
2931 bs58::encode(bincode::serialize(&tx).unwrap()).into_string(),
2932 Some(valid_config),
2933 )
2934 .await
2935 .unwrap();
2936
2937 assert_eq!(
2938 simulation_res.value.err, None,
2939 "Unexpected simulation error"
2940 );
2941 assert_eq!(
2942 simulation_res.value.replacement_blockhash,
2943 Some(RpcBlockhash {
2944 blockhash: recent_blockhash.to_string(),
2945 last_valid_block_height: block_height
2946 }),
2947 "Replacement blockhash should be the latest blockhash"
2948 );
2949 }
2950
2951 #[tokio::test(flavor = "multi_thread")]
2952 async fn test_get_block() {
2953 let setup = TestSetup::new(SurfpoolFullRpc);
2954 let res = setup
2955 .rpc
2956 .get_block(Some(setup.context), 0, None)
2957 .await
2958 .unwrap();
2959
2960 assert_eq!(res, None);
2961 }
2962
2963 #[tokio::test(flavor = "multi_thread")]
2964 async fn test_get_block_time() {
2965 let setup = TestSetup::new(SurfpoolFullRpc);
2966 let res = setup
2967 .rpc
2968 .get_block_time(Some(setup.context), 0)
2969 .await
2970 .unwrap();
2971
2972 assert_eq!(res, None);
2973 }
2974
2975 #[test_case(TransactionVersion::Legacy(Legacy::Legacy) ; "Legacy transactions")]
2976 #[test_case(TransactionVersion::Number(0) ; "V0 transactions")]
2977 #[tokio::test(flavor = "multi_thread")]
2978 async fn test_get_transaction(version: TransactionVersion) {
2979 let payer = Keypair::new();
2980 let pk = Pubkey::new_unique();
2981 let lamports = LAMPORTS_PER_SOL;
2982 let mut setup = TestSetup::new(SurfpoolFullRpc);
2983 let recent_blockhash = setup
2984 .context
2985 .svm_locker
2986 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2987
2988 let _ = setup
2989 .rpc
2990 .request_airdrop(
2991 Some(setup.context.clone()),
2992 payer.pubkey().to_string(),
2993 2 * lamports,
2994 None,
2995 )
2996 .unwrap();
2997
2998 let tx = match version {
2999 TransactionVersion::Legacy(_) => build_legacy_transaction(
3000 &payer.pubkey(),
3001 &[&payer.insecure_clone()],
3002 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3003 &recent_blockhash,
3004 ),
3005 TransactionVersion::Number(0) => build_v0_transaction(
3006 &payer.pubkey(),
3007 &[&payer.insecure_clone()],
3008 &[system_instruction::transfer(&payer.pubkey(), &pk, lamports)],
3009 &recent_blockhash,
3010 ),
3011 _ => unimplemented!(),
3012 };
3013
3014 setup.process_txs(vec![tx.clone()]).await;
3015
3016 let res = setup
3017 .rpc
3018 .get_transaction(
3019 Some(setup.context.clone()),
3020 tx.signatures[0].to_string(),
3021 Some(RpcEncodingConfigWrapper::Current(Some(
3022 RpcTransactionConfig {
3023 max_supported_transaction_version: Some(0),
3024 encoding: Some(UiTransactionEncoding::Json),
3025 ..Default::default()
3026 },
3027 ))),
3028 )
3029 .await
3030 .unwrap()
3031 .unwrap();
3032
3033 let instructions = match tx.message.clone() {
3034 VersionedMessage::Legacy(message) => message
3035 .instructions
3036 .iter()
3037 .map(|ix| UiCompiledInstruction::from(ix, None))
3038 .collect(),
3039 VersionedMessage::V0(message) => message
3040 .instructions
3041 .iter()
3042 .map(|ix| UiCompiledInstruction::from(ix, None))
3043 .collect(),
3044 };
3045
3046 assert_eq!(
3047 res,
3048 EncodedConfirmedTransactionWithStatusMeta {
3049 slot: 123,
3050 transaction: EncodedTransactionWithStatusMeta {
3051 transaction: EncodedTransaction::Json(UiTransaction {
3052 signatures: vec![tx.signatures[0].to_string()],
3053 message: UiMessage::Raw(UiRawMessage {
3054 header: MessageHeader {
3055 num_required_signatures: 1,
3056 num_readonly_signed_accounts: 0,
3057 num_readonly_unsigned_accounts: 1
3058 },
3059 account_keys: vec![
3060 payer.pubkey().to_string(),
3061 pk.to_string(),
3062 system_program::id().to_string()
3063 ],
3064 recent_blockhash: recent_blockhash.to_string(),
3065 instructions,
3066 address_table_lookups: match tx.message {
3067 VersionedMessage::Legacy(_) => None,
3068 VersionedMessage::V0(_) => Some(vec![]),
3069 },
3070 })
3071 }),
3072 meta: res.transaction.clone().meta, version: Some(version)
3074 },
3075 block_time: res.block_time }
3077 );
3078 }
3079
3080 #[tokio::test(flavor = "multi_thread")]
3081 #[allow(deprecated)]
3082 async fn test_get_first_available_block() {
3083 let setup = TestSetup::new(SurfpoolFullRpc);
3084
3085 {
3086 let mut svm_writer = setup.context.svm_locker.0.write().await;
3087
3088 let previous_chain_tip = svm_writer.chain_tip.clone();
3089
3090 let latest_entries = svm_writer
3091 .inner
3092 .get_sysvar::<solana_sdk::sysvar::recent_blockhashes::RecentBlockhashes>(
3093 );
3094 let latest_entry = latest_entries.first().unwrap();
3095
3096 svm_writer.chain_tip = BlockIdentifier::new(
3097 svm_writer.chain_tip.index + 1,
3098 latest_entry.blockhash.to_string().as_str(),
3099 );
3100
3101 let hash = svm_writer.chain_tip.hash.clone();
3102 let block_height = svm_writer.chain_tip.index;
3103 let parent_slot = svm_writer.get_latest_absolute_slot();
3104
3105 svm_writer.blocks.insert(
3106 parent_slot,
3107 BlockHeader {
3108 hash,
3109 previous_blockhash: previous_chain_tip.hash.clone(),
3110 block_time: chrono::Utc::now().timestamp_millis(),
3111 block_height,
3112 parent_slot,
3113 signatures: Vec::new(),
3114 },
3115 );
3116 }
3117
3118 let res = setup
3119 .rpc
3120 .get_first_available_block(Some(setup.context))
3121 .unwrap();
3122
3123 assert_eq!(res, 123);
3124 }
3125
3126 #[test]
3127 fn test_get_latest_blockhash() {
3128 let setup = TestSetup::new(SurfpoolFullRpc);
3129 let res = setup
3130 .rpc
3131 .get_latest_blockhash(Some(setup.context.clone()), None)
3132 .unwrap();
3133 let expected_blockhash = setup
3134 .context
3135 .svm_locker
3136 .0
3137 .blocking_read()
3138 .latest_blockhash();
3139 let expected_last_valid_block_height = setup
3140 .context
3141 .svm_locker
3142 .0
3143 .blocking_read()
3144 .latest_epoch_info
3145 .block_height
3146 + MAX_RECENT_BLOCKHASHES as u64;
3147 assert_eq!(
3148 res.value.blockhash,
3149 expected_blockhash.to_string(),
3150 "Latest blockhash does not match expected value"
3151 );
3152 assert_eq!(
3153 res.value.last_valid_block_height, expected_last_valid_block_height,
3154 "Last valid block height does not match expected value"
3155 );
3156 }
3157
3158 #[tokio::test(flavor = "multi_thread")]
3159 async fn test_get_recent_prioritization_fees() {
3160 let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
3161 let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
3162
3163 let recent_blockhash = setup
3164 .context
3165 .svm_locker
3166 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
3167
3168 let payer_1 = Keypair::new();
3169 let payer_2 = Keypair::new();
3170 let receiver_pubkey = Pubkey::new_unique();
3171 let random_pubkey = Pubkey::new_unique();
3172
3173 {
3175 let _ = setup
3176 .rpc
3177 .request_airdrop(
3178 Some(setup.context.clone()),
3179 payer_1.pubkey().to_string(),
3180 2 * LAMPORTS_PER_SOL,
3181 None,
3182 )
3183 .unwrap();
3184 let _ = setup
3185 .rpc
3186 .request_airdrop(
3187 Some(setup.context.clone()),
3188 payer_2.pubkey().to_string(),
3189 2 * LAMPORTS_PER_SOL,
3190 None,
3191 )
3192 .unwrap();
3193
3194 setup.context.svm_locker.confirm_current_block().unwrap();
3195 }
3196
3197 {
3199 let tx_1 = build_legacy_transaction(
3200 &payer_1.pubkey(),
3201 &[&payer_1.insecure_clone()],
3202 &[
3203 system_instruction::transfer(
3204 &payer_1.pubkey(),
3205 &receiver_pubkey,
3206 LAMPORTS_PER_SOL,
3207 ),
3208 compute_budget::ComputeBudgetInstruction::set_compute_unit_price(1000),
3209 ],
3210 &recent_blockhash,
3211 );
3212 let tx_2 = build_legacy_transaction(
3213 &payer_2.pubkey(),
3214 &[&payer_2.insecure_clone()],
3215 &[
3216 system_instruction::transfer(
3217 &payer_2.pubkey(),
3218 &receiver_pubkey,
3219 LAMPORTS_PER_SOL,
3220 ),
3221 compute_budget::ComputeBudgetInstruction::set_compute_unit_price(1002),
3222 ],
3223 &recent_blockhash,
3224 );
3225
3226 send_and_await_transaction(tx_1, setup.clone(), mempool_rx.clone())
3227 .await
3228 .join()
3229 .unwrap();
3230 send_and_await_transaction(tx_2, setup.clone(), mempool_rx)
3231 .await
3232 .join()
3233 .unwrap();
3234 setup.context.svm_locker.confirm_current_block().unwrap();
3235 }
3236
3237 let res = setup
3240 .rpc
3241 .get_recent_prioritization_fees(
3242 Some(setup.context.clone()),
3243 Some(vec![payer_1.pubkey().to_string()]),
3244 )
3245 .await
3246 .unwrap();
3247 assert_eq!(res.len(), 1);
3248 assert_eq!(res[0].prioritization_fee, 1000);
3249
3250 let res = setup
3253 .rpc
3254 .get_recent_prioritization_fees(Some(setup.context.clone()), None)
3255 .await
3256 .unwrap();
3257 assert_eq!(res.len(), 2);
3258 assert_eq!(res[0].prioritization_fee, 1000);
3259 assert_eq!(res[1].prioritization_fee, 1002);
3260
3261 let res = setup
3264 .rpc
3265 .get_recent_prioritization_fees(
3266 Some(setup.context.clone()),
3267 Some(vec![random_pubkey.to_string()]),
3268 )
3269 .await
3270 .unwrap();
3271 assert!(
3272 res.is_empty(),
3273 "Expected no prioritization fees for random account"
3274 );
3275 }
3276
3277 #[tokio::test(flavor = "multi_thread")]
3278 async fn test_get_blocks_with_limit() {
3279 let setup = TestSetup::new(SurfpoolFullRpc);
3280
3281 {
3282 let mut svm_writer = setup.context.svm_locker.0.write().await;
3283
3284 for slot in 100..=110 {
3285 svm_writer.blocks.insert(
3286 slot,
3287 BlockHeader {
3288 hash: format!("hash_{}", slot),
3289 previous_blockhash: format!("prev_hash_{}", slot - 1),
3290 block_time: chrono::Utc::now().timestamp_millis(),
3291 block_height: slot,
3292 parent_slot: slot.saturating_sub(1),
3293 signatures: Vec::new(),
3294 },
3295 );
3296 }
3297
3298 svm_writer.latest_epoch_info.absolute_slot = 110;
3299 }
3300
3301 let result = setup
3302 .rpc
3303 .get_blocks_with_limit(Some(setup.context.clone()), 100, 5, None)
3304 .await
3305 .unwrap();
3306
3307 assert_eq!(result, vec![100, 101, 102, 103, 104]);
3308 }
3309
3310 #[tokio::test(flavor = "multi_thread")]
3311 async fn test_get_blocks_with_limit_exceeds_available() {
3312 let setup = TestSetup::new(SurfpoolFullRpc);
3313
3314 {
3315 let mut svm_writer = setup.context.svm_locker.0.write().await;
3316
3317 for slot in [100, 101, 102] {
3318 svm_writer.blocks.insert(
3319 slot,
3320 BlockHeader {
3321 hash: format!("hash_{}", slot),
3322 previous_blockhash: format!("prev_hash_{}", slot - 1),
3323 block_time: chrono::Utc::now().timestamp_millis(),
3324 block_height: slot,
3325 parent_slot: slot.saturating_sub(1),
3326 signatures: Vec::new(),
3327 },
3328 );
3329 }
3330
3331 svm_writer.latest_epoch_info.absolute_slot = 102;
3332 }
3333
3334 let result = setup
3335 .rpc
3336 .get_blocks_with_limit(Some(setup.context.clone()), 100, 10, None)
3337 .await
3338 .unwrap();
3339
3340 assert_eq!(result, vec![100, 101, 102]);
3341 }
3342
3343 #[tokio::test(flavor = "multi_thread")]
3344 async fn test_get_blocks_with_limit_commitment_levels() {
3345 let setup = TestSetup::new(SurfpoolFullRpc);
3346
3347 {
3348 let mut svm_writer = setup.context.svm_locker.0.write().await;
3349
3350 for slot in 80..=120 {
3352 svm_writer.blocks.insert(
3353 slot,
3354 BlockHeader {
3355 hash: format!("hash_{}", slot),
3356 previous_blockhash: format!("prev_hash_{}", slot - 1),
3357 block_time: chrono::Utc::now().timestamp_millis(),
3358 block_height: slot,
3359 parent_slot: slot.saturating_sub(1),
3360 signatures: Vec::new(),
3361 },
3362 );
3363 }
3364
3365 svm_writer.latest_epoch_info.absolute_slot = 120;
3366 }
3367
3368 let processed_result = setup
3370 .rpc
3371 .get_blocks_with_limit(
3372 Some(setup.context.clone()),
3373 115,
3374 10,
3375 Some(RpcContextConfig {
3376 commitment: Some(CommitmentConfig {
3377 commitment: CommitmentLevel::Processed,
3378 }),
3379 min_context_slot: None,
3380 }),
3381 )
3382 .await
3383 .unwrap();
3384 assert_eq!(processed_result, vec![115, 116, 117, 118, 119, 120]);
3385
3386 let confirmed_result = setup
3388 .rpc
3389 .get_blocks_with_limit(
3390 Some(setup.context.clone()),
3391 115,
3392 10,
3393 Some(RpcContextConfig {
3394 commitment: Some(CommitmentConfig {
3395 commitment: CommitmentLevel::Confirmed,
3396 }),
3397 min_context_slot: None,
3398 }),
3399 )
3400 .await
3401 .unwrap();
3402 assert_eq!(confirmed_result, vec![115, 116, 117, 118, 119]);
3403
3404 let finalized_result = setup
3406 .rpc
3407 .get_blocks_with_limit(
3408 Some(setup.context.clone()),
3409 85,
3410 10,
3411 Some(RpcContextConfig {
3412 commitment: Some(CommitmentConfig {
3413 commitment: CommitmentLevel::Finalized,
3414 }),
3415 min_context_slot: None,
3416 }),
3417 )
3418 .await
3419 .unwrap();
3420 assert_eq!(finalized_result, vec![85, 86, 87, 88, 89]);
3421 }
3422
3423 #[tokio::test(flavor = "multi_thread")]
3424 async fn test_get_blocks_with_limit_sparse_blocks() {
3425 let setup = TestSetup::new(SurfpoolFullRpc);
3426
3427 {
3428 let mut svm_writer = setup.context.svm_locker.0.write().await;
3429
3430 for slot in [100, 103, 105, 107, 109, 112, 115, 118, 120, 122] {
3432 svm_writer.blocks.insert(
3433 slot,
3434 BlockHeader {
3435 hash: format!("hash_{}", slot),
3436 previous_blockhash: format!("prev_hash_{}", slot - 1),
3437 block_time: chrono::Utc::now().timestamp_millis(),
3438 block_height: slot,
3439 parent_slot: slot.saturating_sub(1),
3440 signatures: Vec::new(),
3441 },
3442 );
3443 }
3444
3445 svm_writer.latest_epoch_info.absolute_slot = 125;
3446 }
3447
3448 let result = setup
3449 .rpc
3450 .get_blocks_with_limit(Some(setup.context.clone()), 100, 6, None)
3451 .await
3452 .unwrap();
3453
3454 assert_eq!(result, vec![100, 103, 105, 107, 109, 112]);
3456 }
3457
3458 #[tokio::test(flavor = "multi_thread")]
3459 async fn test_get_blocks_with_limit_empty_result() {
3460 let setup = TestSetup::new(SurfpoolFullRpc);
3461
3462 {
3463 let mut svm_writer = setup.context.svm_locker.0.write().await;
3464 svm_writer.latest_epoch_info.absolute_slot = 100;
3465 }
3467
3468 let result = setup
3470 .rpc
3471 .get_blocks_with_limit(Some(setup.context.clone()), 50, 10, None)
3472 .await
3473 .unwrap();
3474
3475 assert_eq!(result, Vec::<Slot>::new());
3476 }
3477
3478 #[tokio::test(flavor = "multi_thread")]
3479 async fn test_get_blocks_with_limit_large_limit() {
3480 let setup = TestSetup::new(SurfpoolFullRpc);
3481
3482 {
3483 let mut svm_writer = setup.context.svm_locker.0.write().await;
3484
3485 for slot in 0..1000 {
3486 svm_writer.blocks.insert(
3487 slot,
3488 BlockHeader {
3489 hash: format!("hash_{}", slot),
3490 previous_blockhash: format!("prev_hash_{}", slot.saturating_sub(1)),
3491 block_time: chrono::Utc::now().timestamp_millis(),
3492 block_height: slot,
3493 parent_slot: slot.saturating_sub(1),
3494 signatures: Vec::new(),
3495 },
3496 );
3497 }
3498
3499 svm_writer.latest_epoch_info.absolute_slot = 999;
3500 }
3501
3502 let result = setup
3503 .rpc
3504 .get_blocks_with_limit(Some(setup.context.clone()), 0, 1000, None)
3505 .await
3506 .unwrap();
3507
3508 assert_eq!(result.len(), 1000);
3509 assert_eq!(result[0], 0);
3510 assert_eq!(result[999], 999);
3511
3512 for i in 1..result.len() {
3513 assert!(
3514 result[i] > result[i - 1],
3515 "Results should be in ascending order"
3516 );
3517 }
3518 }
3519
3520 #[tokio::test(flavor = "multi_thread")]
3521 async fn test_get_blocks_basic() {
3522 let setup = TestSetup::new(SurfpoolFullRpc);
3524
3525 {
3526 let mut svm_writer = setup.context.svm_locker.0.write().await;
3527
3528 for slot in 100..=102 {
3529 svm_writer.blocks.insert(
3530 slot,
3531 BlockHeader {
3532 hash: format!("hash_{}", slot),
3533 previous_blockhash: format!("prev_hash_{}", slot - 1),
3534 block_time: chrono::Utc::now().timestamp_millis(),
3535 block_height: slot,
3536 parent_slot: slot.saturating_sub(1),
3537 signatures: Vec::new(),
3538 },
3539 );
3540 }
3541
3542 svm_writer.latest_epoch_info.absolute_slot = 150;
3543 }
3544
3545 let result = setup
3546 .rpc
3547 .get_blocks(
3548 Some(setup.context.clone()),
3549 100,
3550 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(102))),
3551 None,
3552 )
3553 .await
3554 .unwrap();
3555
3556 assert_eq!(result, vec![100, 101, 102]);
3557 }
3558
3559 #[tokio::test(flavor = "multi_thread")]
3560 async fn test_get_blocks_no_end_slot() {
3561 let setup = TestSetup::new(SurfpoolFullRpc);
3562
3563 {
3564 let mut svm_writer = setup.context.svm_locker.0.write().await;
3565
3566 for slot in 100..=105 {
3567 svm_writer.blocks.insert(
3568 slot,
3569 BlockHeader {
3570 hash: format!("hash_{}", slot),
3571 previous_blockhash: format!("prev_hash_{}", slot - 1),
3572 block_time: chrono::Utc::now().timestamp_millis(),
3573 block_height: slot,
3574 parent_slot: slot.saturating_sub(1),
3575 signatures: Vec::new(),
3576 },
3577 );
3578 }
3579
3580 svm_writer.latest_epoch_info.absolute_slot = 105;
3581 }
3582
3583 let result = setup
3585 .rpc
3586 .get_blocks(
3587 Some(setup.context.clone()),
3588 100,
3589 None,
3590 Some(RpcContextConfig {
3591 commitment: Some(CommitmentConfig {
3592 commitment: CommitmentLevel::Confirmed,
3593 }),
3594 min_context_slot: None,
3595 }),
3596 )
3597 .await
3598 .unwrap();
3599
3600 assert_eq!(result, vec![100, 101, 102, 103, 104]);
3602 }
3603
3604 #[tokio::test(flavor = "multi_thread")]
3605 async fn test_get_blocks_commitment_levels() {
3606 let setup = TestSetup::new(SurfpoolFullRpc);
3607
3608 {
3609 let mut svm_writer = setup.context.svm_locker.0.write().await;
3610
3611 for slot in 50..=100 {
3612 svm_writer.blocks.insert(
3613 slot,
3614 BlockHeader {
3615 hash: format!("hash_{}", slot),
3616 previous_blockhash: format!("prev_hash_{}", slot - 1),
3617 block_time: chrono::Utc::now().timestamp_millis(),
3618 block_height: slot,
3619 parent_slot: slot.saturating_sub(1),
3620 signatures: Vec::new(),
3621 },
3622 );
3623 }
3624
3625 svm_writer.latest_epoch_info.absolute_slot = 100;
3626 }
3627
3628 let processed_result = setup
3630 .rpc
3631 .get_blocks(
3632 Some(setup.context.clone()),
3633 95,
3634 None,
3635 Some(RpcContextConfig {
3636 commitment: Some(CommitmentConfig {
3637 commitment: CommitmentLevel::Processed,
3638 }),
3639 min_context_slot: None,
3640 }),
3641 )
3642 .await
3643 .unwrap();
3644 assert_eq!(processed_result, vec![95, 96, 97, 98, 99, 100]);
3645
3646 let confirmed_result = setup
3648 .rpc
3649 .get_blocks(
3650 Some(setup.context.clone()),
3651 95,
3652 None,
3653 Some(RpcContextConfig {
3654 commitment: Some(CommitmentConfig {
3655 commitment: CommitmentLevel::Confirmed,
3656 }),
3657 min_context_slot: None,
3658 }),
3659 )
3660 .await
3661 .unwrap();
3662 assert_eq!(confirmed_result, vec![95, 96, 97, 98, 99]);
3663
3664 let finalized_result = setup
3666 .rpc
3667 .get_blocks(
3668 Some(setup.context.clone()),
3669 65,
3670 None,
3671 Some(RpcContextConfig {
3672 commitment: Some(CommitmentConfig {
3673 commitment: CommitmentLevel::Finalized,
3674 }),
3675 min_context_slot: None,
3676 }),
3677 )
3678 .await
3679 .unwrap();
3680 assert_eq!(finalized_result, vec![65, 66, 67, 68, 69]);
3681 }
3682
3683 #[tokio::test(flavor = "multi_thread")]
3684 async fn test_get_blocks_min_context_slot() {
3685 let setup = TestSetup::new(SurfpoolFullRpc);
3686
3687 {
3688 let mut svm_writer = setup.context.svm_locker.0.write().await;
3689 for slot in 100..=110 {
3690 svm_writer.blocks.insert(
3691 slot,
3692 BlockHeader {
3693 hash: format!("hash_{}", slot),
3694 previous_blockhash: format!("prev_hash_{}", slot - 1),
3695 block_time: chrono::Utc::now().timestamp_millis(),
3696 block_height: slot,
3697 parent_slot: slot.saturating_sub(1),
3698 signatures: Vec::new(),
3699 },
3700 );
3701 }
3702 svm_writer.latest_epoch_info.absolute_slot = 110;
3703 }
3704
3705 let result = setup
3707 .rpc
3708 .get_blocks(
3709 Some(setup.context.clone()),
3710 100,
3711 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(105))),
3712 Some(RpcContextConfig {
3713 commitment: Some(CommitmentConfig::finalized()),
3714 min_context_slot: Some(105),
3715 }),
3716 )
3717 .await;
3718
3719 assert!(result.is_err());
3720
3721 let result = setup
3722 .rpc
3723 .get_blocks(
3724 Some(setup.context.clone()),
3725 105,
3726 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(108))),
3727 Some(RpcContextConfig {
3728 commitment: Some(CommitmentConfig {
3729 commitment: CommitmentLevel::Processed,
3730 }),
3731 min_context_slot: Some(105),
3732 }),
3733 )
3734 .await
3735 .unwrap();
3736
3737 assert_eq!(result, vec![105, 106, 107, 108]);
3738 }
3739
3740 #[tokio::test(flavor = "multi_thread")]
3741 async fn test_get_blocks_sparse_blocks() {
3742 let setup = TestSetup::new(SurfpoolFullRpc);
3743
3744 {
3745 let mut svm_writer = setup.context.svm_locker.0.write().await;
3746
3747 for slot in [100, 102, 105, 107, 110].iter() {
3749 svm_writer.blocks.insert(
3750 *slot,
3751 BlockHeader {
3752 hash: format!("hash_{}", slot),
3753 previous_blockhash: format!("prev_hash_{}", slot - 1),
3754 block_time: chrono::Utc::now().timestamp_millis(),
3755 block_height: *slot,
3756 parent_slot: slot.saturating_sub(1),
3757 signatures: Vec::new(),
3758 },
3759 );
3760 }
3761
3762 svm_writer.latest_epoch_info.absolute_slot = 150;
3763 }
3764
3765 let result = setup
3766 .rpc
3767 .get_blocks(
3768 Some(setup.context.clone()),
3769 100,
3770 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(115))),
3771 None,
3772 )
3773 .await
3774 .unwrap();
3775
3776 assert_eq!(result, vec![100, 102, 105, 107, 110]);
3778 }
3779
3780 fn insert_test_blocks(setup: &TestSetup<SurfpoolFullRpc>, slots: Vec<Slot>) {
3782 setup.context.svm_locker.with_svm_writer(|svm_writer| {
3783 for slot in &slots {
3784 svm_writer.blocks.insert(
3785 *slot,
3786 BlockHeader {
3787 hash: format!("hash_{}", slot),
3788 previous_blockhash: format!("prev_hash_{}", slot.saturating_sub(1)),
3789 block_time: chrono::Utc::now().timestamp_millis(),
3790 block_height: *slot,
3791 parent_slot: slot.saturating_sub(1),
3792 signatures: vec![],
3793 },
3794 );
3795 }
3796 });
3797 }
3798
3799 #[tokio::test(flavor = "multi_thread")]
3800 async fn test_get_blocks_local_only() {
3801 let setup = TestSetup::new(SurfpoolFullRpc);
3802
3803 insert_test_blocks(&setup, (50..=100).collect());
3804
3805 let result = setup
3807 .rpc
3808 .get_blocks(
3809 Some(setup.context),
3810 75,
3811 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(90))),
3812 None,
3813 )
3814 .await
3815 .unwrap();
3816
3817 let expected: Vec<Slot> = (75..=90).collect();
3818 assert_eq!(result, expected, "Should return all local blocks in range");
3819 }
3820
3821 #[tokio::test(flavor = "multi_thread")]
3822 async fn test_get_blocks_no_remote_context() {
3823 let setup = TestSetup::new(SurfpoolFullRpc);
3824
3825 insert_test_blocks(&setup, (50..=100).collect());
3826
3827 let result = setup
3828 .rpc
3829 .get_blocks(
3830 Some(setup.context),
3831 10,
3832 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
3833 None,
3834 )
3835 .await
3836 .unwrap();
3837
3838 let expected: Vec<Slot> = (50..=60).collect();
3840 assert_eq!(
3841 result, expected,
3842 "Should return only local blocks when no remote context"
3843 );
3844 }
3845
3846 #[tokio::test(flavor = "multi_thread")]
3847 async fn test_get_blocks_remote_fetch_below_local_minimum() {
3848 let setup = TestSetup::new(SurfpoolFullRpc);
3849
3850 let local_slots = vec![50, 51, 52, 60, 61, 70, 80, 90, 100];
3851 insert_test_blocks(&setup, local_slots);
3852
3853 let local_min = setup.context.svm_locker.with_svm_reader(|svm_reader| {
3854 let min = svm_reader.blocks.keys().min().copied();
3855 min
3856 });
3857 assert_eq!(local_min, Some(50), "Local minimum should be slot 50");
3858
3859 let result = setup
3861 .rpc
3862 .get_blocks(
3863 Some(setup.context.clone()),
3864 10,
3865 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(30))),
3866 None,
3867 )
3868 .await
3869 .unwrap();
3870
3871 assert_eq!(
3872 result,
3873 Vec::<Slot>::new(),
3874 "Should return empty when no remote context available for pre-local range"
3875 );
3876
3877 let result = setup
3879 .rpc
3880 .get_blocks(
3881 Some(setup.context.clone()),
3882 10,
3883 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(60))),
3884 None,
3885 )
3886 .await
3887 .unwrap();
3888
3889 let expected_local_portion = vec![50, 51, 52, 60];
3890 assert_eq!(
3891 result, expected_local_portion,
3892 "Should return only local blocks when no remote context"
3893 );
3894
3895 let result = setup
3897 .rpc
3898 .get_blocks(
3899 Some(setup.context.clone()),
3900 45,
3901 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(55))),
3902 None,
3903 )
3904 .await
3905 .unwrap();
3906
3907 let expected = vec![50, 51, 52];
3908 assert_eq!(
3909 result, expected,
3910 "Should return only available local blocks in range"
3911 );
3912
3913 let result = setup
3915 .rpc
3916 .get_blocks(
3917 Some(setup.context),
3918 55,
3919 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(65))),
3920 None,
3921 )
3922 .await
3923 .unwrap();
3924
3925 let expected = vec![60, 61];
3927 assert_eq!(
3928 result, expected,
3929 "Should return local blocks when request is at/after local minimum"
3930 );
3931 }
3932
3933 #[tokio::test(flavor = "multi_thread")]
3934 async fn test_get_blocks_all_below_range_mock_remote() {
3935 let setup = TestSetup::new(SurfpoolFullRpc);
3936
3937 setup.context.svm_locker.with_svm_writer(|svm_writer| {
3938 svm_writer.latest_epoch_info.absolute_slot = 200; });
3940
3941 insert_test_blocks(&setup, (100..=150).collect());
3942
3943 let (local_min, latest_slot) = setup.context.svm_locker.with_svm_reader(|svm_reader| {
3944 let min = svm_reader.blocks.keys().min().copied();
3945 let latest = svm_reader.get_latest_absolute_slot();
3946 let _available: Vec<_> = svm_reader.blocks.keys().copied().collect();
3947 (min, latest)
3948 });
3949 assert_eq!(local_min, Some(100), "Local minimum should be 100");
3950 assert_eq!(latest_slot, 200, "Latest slot should be 200");
3951
3952 let result = setup
3953 .rpc
3954 .get_blocks(
3955 Some(setup.context.clone()),
3956 10,
3957 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(50))),
3958 None,
3959 )
3960 .await
3961 .unwrap();
3962
3963 assert_eq!(
3964 result,
3965 Vec::<Slot>::new(),
3966 "Should be empty without remote context"
3967 );
3968
3969 let result = setup
3971 .rpc
3972 .get_blocks(
3973 Some(setup.context.clone()),
3974 5,
3975 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(30))),
3976 None,
3977 )
3978 .await
3979 .unwrap();
3980
3981 assert_eq!(
3982 result,
3983 Vec::<Slot>::new(),
3984 "Should be empty without remote context"
3985 );
3986
3987 let result = setup
3989 .rpc
3990 .get_blocks(
3991 Some(setup.context),
3992 80,
3993 Some(RpcBlocksConfigWrapper::EndSlotOnly(Some(120))),
3994 None,
3995 )
3996 .await
3997 .unwrap();
3998
3999 let expected_local: Vec<Slot> = (100..=120).collect();
4000 assert_eq!(result, expected_local, "Should return local blocks 100-120");
4001 }
4002
4003 #[test]
4004 fn test_get_max_shred_insert_slot() {
4005 let setup = TestSetup::new(SurfpoolFullRpc);
4006
4007 let result = setup
4008 .rpc
4009 .get_max_shred_insert_slot(Some(setup.context.clone()))
4010 .unwrap();
4011 let stake_min_delegation = setup
4012 .rpc
4013 .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4014 .unwrap();
4015
4016 let expected_slot = setup
4017 .context
4018 .svm_locker
4019 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4020
4021 assert_eq!(result, expected_slot);
4022 assert_eq!(stake_min_delegation.context.slot, expected_slot);
4023 assert_eq!(stake_min_delegation.value, 0); }
4025
4026 #[test]
4027 fn test_get_max_retransmit_slot() {
4028 let setup = TestSetup::new(SurfpoolFullRpc);
4029
4030 let result = setup
4031 .rpc
4032 .get_max_retransmit_slot(Some(setup.context.clone()))
4033 .unwrap();
4034 let slot = setup
4035 .context
4036 .clone()
4037 .svm_locker
4038 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4039
4040 assert_eq!(result, slot)
4041 }
4042
4043 #[test]
4044 fn test_get_cluster_nodes() {
4045 let setup = TestSetup::new(SurfpoolFullRpc);
4046
4047 let cluster_nodes = setup.rpc.get_cluster_nodes(Some(setup.context)).unwrap();
4048
4049 assert_eq!(cluster_nodes, vec![]);
4050 }
4051
4052 #[test]
4053 fn test_get_stake_minimum_delegation_default() {
4054 let setup = TestSetup::new(SurfpoolFullRpc);
4055
4056 let result = setup
4057 .rpc
4058 .get_max_shred_insert_slot(Some(setup.context.clone()))
4059 .unwrap();
4060
4061 let stake_min_delegation = setup
4062 .rpc
4063 .get_stake_minimum_delegation(Some(setup.context.clone()), None)
4064 .unwrap();
4065
4066 let expected_slot = setup
4067 .context
4068 .svm_locker
4069 .with_svm_reader(|svm_reader| svm_reader.get_latest_absolute_slot());
4070
4071 assert_eq!(result, expected_slot);
4072 assert_eq!(stake_min_delegation.context.slot, expected_slot);
4073 assert_eq!(stake_min_delegation.value, 0); }
4075
4076 #[test]
4077 fn test_get_stake_minimum_delegation_with_finalized_commitment() {
4078 let setup = TestSetup::new(SurfpoolFullRpc);
4079
4080 let config = Some(RpcContextConfig {
4081 commitment: Some(CommitmentConfig {
4082 commitment: CommitmentLevel::Finalized,
4083 }),
4084 min_context_slot: None,
4085 });
4086
4087 let result = setup
4088 .rpc
4089 .get_stake_minimum_delegation(Some(setup.context.clone()), config)
4090 .unwrap();
4091
4092 let expected_slot = setup.context.svm_locker.with_svm_reader(|svm_reader| {
4094 svm_reader
4095 .get_latest_absolute_slot()
4096 .saturating_sub(FINALIZATION_SLOT_THRESHOLD)
4097 });
4098
4099 assert_eq!(result.context.slot, expected_slot);
4100 assert_eq!(result.value, 0);
4101 }
4102
4103 #[tokio::test(flavor = "multi_thread")]
4104 async fn test_is_blockhash_valid_recent_blockhash() {
4105 let setup = TestSetup::new(SurfpoolFullRpc);
4106
4107 let recent_blockhash = setup
4109 .context
4110 .svm_locker
4111 .with_svm_reader(|svm| svm.latest_blockhash());
4112
4113 let result = setup
4114 .rpc
4115 .is_blockhash_valid(
4116 Some(setup.context.clone()),
4117 recent_blockhash.to_string(),
4118 None,
4119 )
4120 .unwrap();
4121
4122 assert_eq!(result.value, true);
4123 assert!(result.context.slot > 0);
4124
4125 let result_processed = setup
4127 .rpc
4128 .is_blockhash_valid(
4129 Some(setup.context.clone()),
4130 recent_blockhash.to_string(),
4131 Some(RpcContextConfig {
4132 commitment: Some(CommitmentConfig {
4133 commitment: CommitmentLevel::Processed,
4134 }),
4135 min_context_slot: None,
4136 }),
4137 )
4138 .unwrap();
4139
4140 assert_eq!(result_processed.value, true);
4141 }
4142
4143 #[tokio::test(flavor = "multi_thread")]
4144 async fn test_is_blockhash_valid_invalid_blockhash() {
4145 let setup = TestSetup::new(SurfpoolFullRpc);
4146
4147 let fake_blockhash = Hash::new_from_array([1u8; 32]);
4148
4149 let result = setup
4151 .rpc
4152 .is_blockhash_valid(
4153 Some(setup.context.clone()),
4154 fake_blockhash.to_string(),
4155 None,
4156 )
4157 .unwrap();
4158
4159 assert_eq!(result.value, false);
4160
4161 let result_confirmed = setup
4163 .rpc
4164 .is_blockhash_valid(
4165 Some(setup.context.clone()),
4166 fake_blockhash.to_string(),
4167 Some(RpcContextConfig {
4168 commitment: Some(CommitmentConfig {
4169 commitment: CommitmentLevel::Confirmed,
4170 }),
4171 min_context_slot: None,
4172 }),
4173 )
4174 .unwrap();
4175
4176 assert_eq!(result_confirmed.value, false);
4177
4178 let another_fake = Hash::new_from_array([255u8; 32]);
4180 let result2 = setup
4181 .rpc
4182 .is_blockhash_valid(Some(setup.context.clone()), another_fake.to_string(), None)
4183 .unwrap();
4184
4185 assert_eq!(result2.value, false);
4186
4187 let invalid_result = setup.rpc.is_blockhash_valid(
4188 Some(setup.context.clone()),
4189 "invalid-blockhash-format".to_string(),
4190 None,
4191 );
4192
4193 assert!(invalid_result.is_err());
4194
4195 let short_result =
4196 setup
4197 .rpc
4198 .is_blockhash_valid(Some(setup.context.clone()), "123".to_string(), None);
4199 assert!(short_result.is_err());
4200
4201 let invalid_chars_result =
4203 setup
4204 .rpc
4205 .is_blockhash_valid(Some(setup.context.clone()), "0OIl".to_string(), None);
4206 assert!(invalid_chars_result.is_err());
4207 }
4208
4209 #[tokio::test(flavor = "multi_thread")]
4210 async fn test_is_blockhash_valid_commitment_and_context_slot() {
4211 let setup = TestSetup::new(SurfpoolFullRpc);
4212
4213 {
4215 let mut svm_writer = setup.context.svm_locker.0.write().await;
4216
4217 svm_writer.latest_epoch_info.absolute_slot = 100;
4219
4220 for slot in 70..=100 {
4222 svm_writer.blocks.insert(
4223 slot,
4224 BlockHeader {
4225 hash: format!("hash_{}", slot),
4226 previous_blockhash: format!("prev_hash_{}", slot - 1),
4227 block_time: chrono::Utc::now().timestamp_millis(),
4228 block_height: slot,
4229 parent_slot: slot.saturating_sub(1),
4230 signatures: Vec::new(),
4231 },
4232 );
4233 }
4234 }
4235
4236 let recent_blockhash = setup
4237 .context
4238 .svm_locker
4239 .with_svm_reader(|svm| svm.latest_blockhash());
4240
4241 let processed_result = setup
4243 .rpc
4244 .is_blockhash_valid(
4245 Some(setup.context.clone()),
4246 recent_blockhash.to_string(),
4247 Some(RpcContextConfig {
4248 commitment: Some(CommitmentConfig {
4249 commitment: CommitmentLevel::Processed,
4250 }),
4251 min_context_slot: None,
4252 }),
4253 )
4254 .unwrap();
4255
4256 assert_eq!(processed_result.value, true);
4257 assert_eq!(processed_result.context.slot, 100);
4258
4259 let confirmed_result = setup
4261 .rpc
4262 .is_blockhash_valid(
4263 Some(setup.context.clone()),
4264 recent_blockhash.to_string(),
4265 Some(RpcContextConfig {
4266 commitment: Some(CommitmentConfig {
4267 commitment: CommitmentLevel::Confirmed,
4268 }),
4269 min_context_slot: None,
4270 }),
4271 )
4272 .unwrap();
4273
4274 assert_eq!(confirmed_result.value, true);
4275 assert_eq!(confirmed_result.context.slot, 99);
4276
4277 let finalized_result = setup
4279 .rpc
4280 .is_blockhash_valid(
4281 Some(setup.context.clone()),
4282 recent_blockhash.to_string(),
4283 Some(RpcContextConfig {
4284 commitment: Some(CommitmentConfig {
4285 commitment: CommitmentLevel::Finalized,
4286 }),
4287 min_context_slot: None,
4288 }),
4289 )
4290 .unwrap();
4291
4292 assert_eq!(finalized_result.value, true);
4293 assert_eq!(finalized_result.context.slot, 69);
4294
4295 let min_context_success = setup
4297 .rpc
4298 .is_blockhash_valid(
4299 Some(setup.context.clone()),
4300 recent_blockhash.to_string(),
4301 Some(RpcContextConfig {
4302 commitment: Some(CommitmentConfig {
4303 commitment: CommitmentLevel::Processed,
4304 }),
4305 min_context_slot: Some(95),
4306 }),
4307 )
4308 .unwrap();
4309
4310 assert_eq!(min_context_success.value, true);
4311
4312 let min_context_failure = setup.rpc.is_blockhash_valid(
4314 Some(setup.context.clone()),
4315 recent_blockhash.to_string(),
4316 Some(RpcContextConfig {
4317 commitment: Some(CommitmentConfig {
4318 commitment: CommitmentLevel::Finalized,
4319 }),
4320 min_context_slot: Some(80),
4321 }),
4322 );
4323
4324 assert!(min_context_failure.is_err());
4325 }
4326
4327 #[ignore = "requires-network"]
4328 #[tokio::test(flavor = "multi_thread")]
4329 async fn test_minimum_ledger_slot_from_remote() {
4330 let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
4332 let mut setup = TestSetup::new(SurfpoolFullRpc);
4333 setup.context.remote_rpc_client = Some(remote_client);
4334
4335 let result = setup
4336 .rpc
4337 .minimum_ledger_slot(Some(setup.context))
4338 .await
4339 .unwrap();
4340
4341 assert!(
4342 result > 0,
4343 "Mainnet should return a valid minimum ledger slot > 0"
4344 );
4345 println!("Mainnet minimum ledger slot: {}", result);
4346 }
4347
4348 #[tokio::test(flavor = "multi_thread")]
4349 async fn test_minimum_ledger_slot_no_context_fails() {
4350 let setup = TestSetup::new(SurfpoolFullRpc);
4352
4353 let result = setup.rpc.minimum_ledger_slot(None).await;
4354
4355 assert!(
4356 result.is_err(),
4357 "Should fail when called without metadata context"
4358 );
4359 }
4360
4361 #[tokio::test(flavor = "multi_thread")]
4362 async fn test_minimum_ledger_slot_finds_minimum() {
4363 let setup = TestSetup::new(SurfpoolFullRpc);
4365
4366 insert_test_blocks(&setup, vec![500, 100, 1000, 50, 750]);
4367
4368 let result = setup
4369 .rpc
4370 .minimum_ledger_slot(Some(setup.context))
4371 .await
4372 .unwrap();
4373
4374 assert_eq!(
4375 result, 50,
4376 "Should return minimum slot (50) regardless of insertion order"
4377 );
4378 }
4379
4380 #[tokio::test(flavor = "multi_thread")]
4381 async fn test_get_inflation_reward() {
4382 let setup = TestSetup::new(SurfpoolFullRpc);
4383
4384 let (epoch, effective_slot) =
4385 setup
4386 .context
4387 .clone()
4388 .svm_locker
4389 .with_svm_reader(|svm_reader| {
4390 (
4391 svm_reader.latest_epoch_info().epoch,
4392 svm_reader.get_latest_absolute_slot(),
4393 )
4394 });
4395
4396 let result = setup
4397 .rpc
4398 .get_inflation_reward(
4399 Some(setup.context),
4400 vec![Pubkey::new_unique().to_string()],
4401 None,
4402 )
4403 .await
4404 .unwrap();
4405
4406 assert_eq!(
4407 result[0],
4408 Some(RpcInflationReward {
4409 epoch,
4410 effective_slot,
4411 amount: 0,
4412 post_balance: 0,
4413 commission: None
4414 })
4415 )
4416 }
4417}