1use std::{
2 collections::BTreeMap,
3 sync::{Arc, RwLock},
4};
5
6use jsonrpc_core::{BoxFuture, Error, Result, futures::future};
7use jsonrpc_derive::rpc;
8use solana_account::Account;
9use solana_client::rpc_response::{RpcLogsResponse, RpcResponseContext};
10use solana_clock::Slot;
11use solana_commitment_config::CommitmentConfig;
12use solana_epoch_info::EpochInfo;
13use solana_program_option::COption;
14use solana_rpc_client_api::response::Response as RpcResponse;
15use solana_system_interface::program as system_program;
16use solana_transaction::versioned::VersionedTransaction;
17use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
18use surfpool_types::{
19 AccountSnapshot, CheatcodeControlConfig, CheatcodeFilter, ClockCommand, ExportSnapshotConfig,
20 GetStreamedAccountsResponse, GetSurfnetInfoResponse, Idl, OfflineAccountConfig,
21 ResetAccountConfig, RpcProfileResultConfig, Scenario, SimnetCommand, SimnetEvent,
22 StreamAccountConfig, StreamAccountsEntry, UiKeyedProfileResult,
23 types::{AccountUpdate, SetSomeAccount, SupplyUpdate, TokenAccountUpdate, UuidOrSignature},
24};
25
26use super::{RunloopContext, SurfnetRpcContext};
27use crate::{
28 error::SurfpoolError,
29 rpc::{
30 State,
31 utils::{decode_and_deserialize, verify_pubkey, verify_pubkeys},
32 },
33 surfnet::{GetAccountResult, locker::SvmAccessContext},
34 types::{TimeTravelConfig, TokenAccount},
35};
36
37pub trait AccountUpdateExt {
38 fn is_full_account_data_ext(&self) -> bool;
39 fn to_account_ext(&self) -> Result<Option<Account>>;
40 fn apply_ext(self, account: &mut GetAccountResult) -> Result<()>;
41 fn expect_hex_data_ext(&self) -> Result<Vec<u8>>;
42}
43
44impl AccountUpdateExt for AccountUpdate {
45 fn is_full_account_data_ext(&self) -> bool {
46 self.lamports.is_some()
47 && self.owner.is_some()
48 && self.executable.is_some()
49 && self.rent_epoch.is_some()
50 && self.data.is_some()
51 }
52
53 fn to_account_ext(&self) -> Result<Option<Account>> {
54 if self.is_full_account_data_ext() {
55 Ok(Some(Account {
56 lamports: self.lamports.unwrap(),
57 owner: verify_pubkey(&self.owner.clone().unwrap())?,
58 executable: self.executable.unwrap(),
59 rent_epoch: self.rent_epoch.unwrap(),
60 data: self.expect_hex_data_ext()?,
61 }))
62 } else {
63 Ok(None)
64 }
65 }
66
67 fn apply_ext(self, account_result: &mut GetAccountResult) -> Result<()> {
68 account_result.apply_update(|account| {
69 if let Some(lamports) = self.lamports {
70 account.lamports = lamports;
71 }
72 if let Some(owner) = &self.owner {
73 account.owner = verify_pubkey(owner)?;
74 }
75 if let Some(executable) = self.executable {
76 account.executable = executable;
77 }
78 if let Some(rent_epoch) = self.rent_epoch {
79 account.rent_epoch = rent_epoch;
80 }
81 if self.data.is_some() {
82 account.data = self.expect_hex_data_ext()?;
83 }
84 Ok(())
85 })?;
86 Ok(())
87 }
88
89 fn expect_hex_data_ext(&self) -> Result<Vec<u8>> {
90 let data = self.data.as_ref().expect("missing expected data field");
91 hex::decode(data)
92 .map_err(|e| Error::invalid_params(format!("Invalid hex data provided: {}", e)))
93 }
94}
95
96pub trait TokenAccountUpdateExt {
97 fn apply(self, token_account: &mut TokenAccount) -> Result<()>;
98}
99
100impl TokenAccountUpdateExt for TokenAccountUpdate {
101 fn apply(self, token_account: &mut TokenAccount) -> Result<()> {
103 if let Some(amount) = self.amount {
104 token_account.set_amount(amount);
105 }
106 if let Some(delegate) = self.delegate {
107 match delegate {
108 SetSomeAccount::Account(pubkey) => {
109 token_account.set_delegate(COption::Some(verify_pubkey(&pubkey)?));
110 }
111 SetSomeAccount::NoAccount => {
112 token_account.set_delegate(COption::None);
113 }
114 }
115 }
116 if let Some(state) = self.state {
117 token_account.set_state_from_str(state.as_str())?;
118 }
119 if let Some(delegated_amount) = self.delegated_amount {
120 token_account.set_delegated_amount(delegated_amount);
121 }
122 if let Some(close_authority) = self.close_authority {
123 match close_authority {
124 SetSomeAccount::Account(pubkey) => {
125 token_account.set_close_authority(COption::Some(verify_pubkey(&pubkey)?));
126 }
127 SetSomeAccount::NoAccount => {
128 token_account.set_close_authority(COption::None);
129 }
130 }
131 }
132 Ok(())
133 }
134}
135
136#[rpc]
137pub trait SurfnetCheatcodes {
138 type Metadata;
139
140 #[rpc(meta, name = "surfnet_setAccount")]
178 fn set_account(
179 &self,
180 meta: Self::Metadata,
181 pubkey: String,
182 update: AccountUpdate,
183 ) -> BoxFuture<Result<RpcResponse<()>>>;
184
185 #[rpc(meta, name = "surfnet_enableCheatcode")]
221 fn enable_cheatcode(
222 &self,
223 meta: Self::Metadata,
224 cheatcodes_filter: CheatcodeFilter,
225 ) -> Result<RpcResponse<()>>;
226
227 #[rpc(meta, name = "surfnet_disableCheatcode")]
264 fn disable_cheatcode(
265 &self,
266 meta: Self::Metadata,
267 cheatcodes_filter: CheatcodeFilter,
268 lockout: Option<CheatcodeControlConfig>,
269 ) -> Result<RpcResponse<()>>;
270
271 #[rpc(meta, name = "surfnet_setTokenAccount")]
311 fn set_token_account(
312 &self,
313 meta: Self::Metadata,
314 owner: String,
315 mint: String,
316 update: TokenAccountUpdate,
317 token_program: Option<String>,
318 ) -> BoxFuture<Result<RpcResponse<()>>>;
319
320 #[rpc(meta, name = "surfnet_cloneProgramAccount")]
321 fn clone_program_account(
322 &self,
323 meta: Self::Metadata,
324 source_program_id: String,
325 destination_program_id: String,
326 ) -> BoxFuture<Result<RpcResponse<()>>>;
327
328 #[rpc(meta, name = "surfnet_profileTransaction")]
352 fn profile_transaction(
353 &self,
354 meta: Self::Metadata,
355 transaction_data: String, tag: Option<String>, config: Option<RpcProfileResultConfig>,
358 ) -> BoxFuture<Result<RpcResponse<UiKeyedProfileResult>>>;
359
360 #[rpc(meta, name = "surfnet_getProfileResultsByTag")]
368 fn get_profile_results_by_tag(
369 &self,
370 meta: Self::Metadata,
371 tag: String,
372 config: Option<RpcProfileResultConfig>,
373 ) -> Result<RpcResponse<Option<Vec<UiKeyedProfileResult>>>>;
374
375 #[rpc(meta, name = "surfnet_setSupply")]
423 fn set_supply(
424 &self,
425 meta: Self::Metadata,
426 update: SupplyUpdate,
427 ) -> BoxFuture<Result<RpcResponse<()>>>;
428
429 #[rpc(meta, name = "surfnet_setProgramAuthority")]
462 fn set_program_authority(
463 &self,
464 meta: Self::Metadata,
465 program_id_str: String,
466 new_authority_str: Option<String>,
467 ) -> BoxFuture<Result<RpcResponse<()>>>;
468
469 #[rpc(meta, name = "surfnet_getTransactionProfile")]
502 fn get_transaction_profile(
503 &self,
504 meta: Self::Metadata,
505 signature_or_uuid: UuidOrSignature,
506 config: Option<RpcProfileResultConfig>,
507 ) -> Result<RpcResponse<Option<UiKeyedProfileResult>>>;
508
509 #[rpc(meta, name = "surfnet_registerIdl")]
599 fn register_idl(
600 &self,
601 meta: Self::Metadata,
602 idl: Idl,
603 slot: Option<Slot>,
604 ) -> Result<RpcResponse<()>>;
605
606 #[rpc(meta, name = "surfnet_getActiveIdl")]
652 fn get_idl(
653 &self,
654 meta: Self::Metadata,
655 program_id: String,
656 slot: Option<Slot>,
657 ) -> Result<RpcResponse<Option<Idl>>>;
658
659 #[rpc(meta, name = "surfnet_getLocalSignatures")]
681 fn get_local_signatures(
682 &self,
683 meta: Self::Metadata,
684 limit: Option<u64>,
685 ) -> BoxFuture<Result<RpcResponse<Vec<RpcLogsResponse>>>>;
686
687 #[rpc(meta, name = "surfnet_timeTravel")]
724 fn time_travel(
725 &self,
726 meta: Self::Metadata,
727 config: Option<TimeTravelConfig>,
728 ) -> Result<EpochInfo>;
729
730 #[rpc(meta, name = "surfnet_pauseClock")]
762 fn pause_clock(&self, meta: Self::Metadata) -> Result<EpochInfo>;
763
764 #[rpc(meta, name = "surfnet_resumeClock")]
798 fn resume_clock(&self, meta: Self::Metadata) -> Result<EpochInfo>;
799
800 #[rpc(meta, name = "surfnet_resetAccount")]
834 fn reset_account(
835 &self,
836 meta: Self::Metadata,
837 pubkey_str: String,
838 config: Option<ResetAccountConfig>,
839 ) -> Result<RpcResponse<()>>;
840
841 #[rpc(meta, name = "surfnet_resetNetwork")]
870 fn reset_network(&self, meta: Self::Metadata) -> BoxFuture<Result<RpcResponse<()>>>;
871
872 #[rpc(meta, name = "surfnet_offlineAccount")]
907 fn offline_account(
908 &self,
909 meta: Self::Metadata,
910 pubkey_str: String,
911 config: Option<OfflineAccountConfig>,
912 ) -> BoxFuture<Result<RpcResponse<()>>>;
913
914 #[rpc(meta, name = "surfnet_exportSnapshot")]
973 fn export_snapshot(
974 &self,
975 meta: Self::Metadata,
976 config: Option<ExportSnapshotConfig>,
977 ) -> Result<RpcResponse<BTreeMap<String, AccountSnapshot>>>;
978
979 #[rpc(meta, name = "surfnet_streamAccount")]
1015 fn stream_account(
1016 &self,
1017 meta: Self::Metadata,
1018 pubkey_str: String,
1019 config: Option<StreamAccountConfig>,
1020 ) -> Result<RpcResponse<()>>;
1021
1022 #[rpc(meta, name = "surfnet_streamAccounts")]
1062 fn stream_accounts(
1063 &self,
1064 meta: Self::Metadata,
1065 accounts: Vec<StreamAccountsEntry>,
1066 ) -> Result<RpcResponse<()>>;
1067
1068 #[rpc(meta, name = "surfnet_getStreamedAccounts")]
1104 fn get_streamed_accounts(
1105 &self,
1106 meta: Self::Metadata,
1107 ) -> Result<RpcResponse<GetStreamedAccountsResponse>>;
1108
1109 #[rpc(meta, name = "surfnet_getSurfnetInfo")]
1147 fn get_surfnet_info(&self, meta: Self::Metadata)
1148 -> Result<RpcResponse<GetSurfnetInfoResponse>>;
1149
1150 #[rpc(meta, name = "surfnet_writeProgram")]
1192 fn write_program(
1193 &self,
1194 meta: Self::Metadata,
1195 program_id: String,
1196 data: String,
1197 offset: usize,
1198 authority: Option<String>,
1199 ) -> BoxFuture<Result<RpcResponse<()>>>;
1200
1201 #[rpc(meta, name = "surfnet_registerScenario")]
1304 fn register_scenario(
1305 &self,
1306 meta: Self::Metadata,
1307 scenario: Scenario,
1308 slot: Option<Slot>,
1309 ) -> BoxFuture<Result<RpcResponse<()>>>;
1310}
1311
1312#[derive(Clone)]
1313pub struct SurfnetCheatcodesRpc {
1314 pub registered_methods: Arc<RwLock<Vec<String>>>,
1315}
1316impl SurfnetCheatcodesRpc {
1317 pub fn empty() -> Self {
1318 Self {
1319 registered_methods: Arc::new(RwLock::new(vec![])),
1320 }
1321 }
1322 pub fn is_available_cheatcode(&self, cheatcode: &String) -> bool {
1323 let Ok(methods) = self.registered_methods.read() else {
1324 return false;
1325 };
1326 methods.contains(cheatcode)
1327 }
1328}
1329
1330impl SurfnetCheatcodes for SurfnetCheatcodesRpc {
1331 type Metadata = Option<RunloopContext>;
1332
1333 fn set_account(
1334 &self,
1335 meta: Self::Metadata,
1336 pubkey_str: String,
1337 update: AccountUpdate,
1338 ) -> BoxFuture<Result<RpcResponse<()>>> {
1339 let pubkey = match verify_pubkey(&pubkey_str) {
1340 Ok(res) => res,
1341 Err(e) => return e.into(),
1342 };
1343 let account_update_opt = match update.to_account_ext() {
1344 Err(e) => return Box::pin(future::err(e)),
1345 Ok(res) => res,
1346 };
1347
1348 let SurfnetRpcContext {
1349 svm_locker,
1350 remote_ctx,
1351 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1352 Ok(res) => res,
1353 Err(e) => return e.into(),
1354 };
1355
1356 Box::pin(async move {
1357 let (account_to_set, latest_absolute_slot) = if let Some(account) = account_update_opt {
1358 (
1359 GetAccountResult::FoundAccount(pubkey, account, true),
1360 svm_locker.get_latest_absolute_slot(),
1361 )
1362 } else {
1363 let SvmAccessContext {
1365 slot, inner: mut account_result_to_update,
1366 ..
1367 } = svm_locker.get_account(&remote_ctx, &pubkey, Some(Box::new(move |svm_locker| {
1368
1369 let _ = svm_locker.simnet_events_tx().send(SimnetEvent::info(format!(
1371 "Account {pubkey} not found, creating a new account from default values"
1372 )));
1373 GetAccountResult::FoundAccount(
1374 pubkey,
1375 solana_account::Account {
1376 lamports: 0,
1377 owner: system_program::id(),
1378 executable: false,
1379 rent_epoch: 0,
1380 data: vec![],
1381 },
1382 true, )
1384 }))).await?;
1385
1386 update.apply_ext(&mut account_result_to_update)?;
1387 (account_result_to_update, slot)
1388 };
1389
1390 svm_locker.write_account_update(account_to_set);
1391
1392 Ok(RpcResponse {
1393 context: RpcResponseContext::new(latest_absolute_slot),
1394 value: (),
1395 })
1396 })
1397 }
1398
1399 fn disable_cheatcode(
1400 &self,
1401 meta: Self::Metadata,
1402 cheatcodes_filter: CheatcodeFilter,
1403 control_config: Option<CheatcodeControlConfig>,
1404 ) -> Result<RpcResponse<()>> {
1405 let svm_locker = match meta.get_svm_locker() {
1406 Ok(locker) => locker,
1407 Err(e) => return Err(e.into()),
1408 };
1409
1410 let CheatcodeControlConfig { lockout } = control_config.unwrap_or_default();
1411 let lockout = lockout.unwrap_or_default();
1412
1413 if let Some(runloop_ctx) = meta {
1414 let Ok(mut cheatcode_ctx) = runloop_ctx.cheatcode_config.lock() else {
1415 return Err(jsonrpc_core::Error::internal_error());
1416 };
1417
1418 if lockout {
1419 cheatcode_ctx.lockout();
1420 }
1421
1422 match cheatcodes_filter {
1423 CheatcodeFilter::All(all) => {
1424 if all.ne("all") {
1425 return Err(SurfpoolError::disable_cheatcode(
1426 "Invalid option provided for disabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
1427 )
1428 .into());
1429 }
1430
1431 let Ok(available_cheatcodes) = self.registered_methods.read() else {
1432 return Err(jsonrpc_core::Error::internal_error());
1433 };
1434 cheatcode_ctx.disable_all(lockout, (*available_cheatcodes).clone());
1435 }
1436 CheatcodeFilter::List(cheatcodes) => {
1437 for cheatcode in &cheatcodes {
1439 if !lockout
1440 && (cheatcode.eq("surfnet_enableCheatcode")
1441 || cheatcode.eq("surfnet_disableCheatcode"))
1442 {
1443 return Err(SurfpoolError::disable_cheatcode(
1444 "Cannot disable surfnet_enableCheatcode or surfnet_disableCheatcode rpc method when lockout is not enabled".to_string(),
1445 )
1446 .into());
1447 }
1448 if !self.is_available_cheatcode(cheatcode) {
1449 return Err(SurfpoolError::disable_cheatcode(
1450 "Invalid cheatcode rpc method".to_string(),
1451 )
1452 .into());
1453 }
1454 }
1455 for cheatcode in cheatcodes {
1457 if let Err(e) = cheatcode_ctx.disable_cheatcode(&cheatcode) {
1458 return Err(SurfpoolError::disable_cheatcode(e).into());
1459 }
1460 }
1461 }
1462 }
1463 }
1464
1465 Ok(RpcResponse {
1466 value: (),
1467 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1468 })
1469 }
1470
1471 fn enable_cheatcode(
1472 &self,
1473 meta: Self::Metadata,
1474 cheatcodes_filter: CheatcodeFilter,
1475 ) -> Result<RpcResponse<()>> {
1476 let svm_locker = match meta.get_svm_locker() {
1477 Ok(locker) => locker,
1478 Err(e) => return Err(e.into()),
1479 };
1480 if let Some(runloop_ctx) = meta {
1481 let Ok(mut cheatcode_ctx) = runloop_ctx.cheatcode_config.lock() else {
1482 return Err(jsonrpc_core::Error::internal_error());
1483 };
1484 match cheatcodes_filter {
1485 CheatcodeFilter::All(all) => {
1486 if all.ne("all") {
1487 return Err(SurfpoolError::enable_cheatcode(
1488 "Invalid option provided for enabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
1489 )
1490 .into());
1491 }
1492
1493 cheatcode_ctx.filter = CheatcodeFilter::List(vec![]);
1495 }
1496 CheatcodeFilter::List(cheatcodes) => {
1497 for ref cheatcode in cheatcodes {
1498 debug!("enabling cheatcode: {cheatcode}");
1499 if !self.is_available_cheatcode(cheatcode) {
1500 return Err(SurfpoolError::enable_cheatcode(
1501 "Invalid cheatcode rpc method".to_string(),
1502 )
1503 .into());
1504 }
1505
1506 if let Err(e) = cheatcode_ctx.enable_cheatcode(cheatcode) {
1507 return Err(SurfpoolError::enable_cheatcode(e).into());
1508 }
1509 }
1510 }
1511 }
1512 }
1513
1514 Ok(RpcResponse {
1515 value: (),
1516 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1517 })
1518 }
1519
1520 fn set_token_account(
1521 &self,
1522 meta: Self::Metadata,
1523 owner_str: String,
1524 mint_str: String,
1525 update: TokenAccountUpdate,
1526 some_token_program_str: Option<String>,
1527 ) -> BoxFuture<Result<RpcResponse<()>>> {
1528 let owner = match verify_pubkey(&owner_str) {
1529 Ok(res) => res,
1530 Err(e) => return e.into(),
1531 };
1532
1533 let mint = match verify_pubkey(&mint_str) {
1534 Ok(res) => res,
1535 Err(e) => return e.into(),
1536 };
1537
1538 let is_native_mint = mint == spl_token_interface::native_mint::id();
1539 let token_amount = update.amount.unwrap_or(0);
1540
1541 let token_program_id = match some_token_program_str {
1542 Some(token_program_str) => match verify_pubkey(&token_program_str) {
1543 Ok(res) => res,
1544 Err(e) => return e.into(),
1545 },
1546 None => spl_token_interface::id(),
1547 };
1548
1549 let associated_token_account =
1550 get_associated_token_address_with_program_id(&owner, &mint, &token_program_id);
1551
1552 let SurfnetRpcContext {
1553 svm_locker,
1554 remote_ctx,
1555 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1556 Ok(res) => res,
1557 Err(e) => return e.into(),
1558 };
1559
1560 Box::pin(async move {
1561 let get_mint_result = svm_locker
1562 .get_account(&remote_ctx, &mint, None)
1563 .await?
1564 .inner;
1565 svm_locker.write_account_update(get_mint_result);
1566
1567 let minimum_rent = svm_locker.with_svm_reader(|svm_reader| {
1568 svm_reader.inner.minimum_balance_for_rent_exemption(
1569 TokenAccount::get_packed_len_for_token_program_id(&token_program_id),
1570 )
1571 });
1572
1573 let (rent_exempt_reserve, initial_lamports) = if is_native_mint {
1574 (Some(minimum_rent), minimum_rent + token_amount) } else {
1577 (None, minimum_rent)
1578 };
1579
1580 let SvmAccessContext {
1581 slot,
1582 inner: mut token_account,
1583 ..
1584 } = svm_locker
1585 .get_account(
1586 &remote_ctx,
1587 &associated_token_account,
1588 Some(Box::new(move |_| {
1589 let default =
1590 TokenAccount::new(&token_program_id, owner, mint, rent_exempt_reserve);
1591 let data = default.pack_into_vec();
1592 GetAccountResult::FoundAccount(
1593 associated_token_account,
1594 Account {
1595 lamports: initial_lamports,
1596 owner: token_program_id,
1597 executable: false,
1598 rent_epoch: 0,
1599 data,
1600 },
1601 true, )
1603 })),
1604 )
1605 .await?;
1606
1607 let mut token_account_data = TokenAccount::unpack(token_account.expected_data())
1608 .map_err(|e| {
1609 Error::invalid_params(format!("Failed to unpack token account data: {}", e))
1610 })?;
1611
1612 update.apply(&mut token_account_data)?;
1613
1614 let final_account_bytes = token_account_data.pack_into_vec();
1615 token_account.apply_update(|account| {
1616 account.lamports = initial_lamports;
1618 account.data = final_account_bytes.clone();
1619 Ok(())
1620 })?;
1621 svm_locker.write_account_update(token_account);
1622
1623 Ok(RpcResponse {
1624 context: RpcResponseContext::new(slot),
1625 value: (),
1626 })
1627 })
1628 }
1629
1630 fn clone_program_account(
1641 &self,
1642 meta: Self::Metadata,
1643 source_program_id: String,
1644 destination_program_id: String,
1645 ) -> BoxFuture<Result<RpcResponse<()>>> {
1646 let source_program_id = match verify_pubkey(&source_program_id) {
1647 Ok(res) => res,
1648 Err(e) => return e.into(),
1649 };
1650 let destination_program_id = match verify_pubkey(&destination_program_id) {
1651 Ok(res) => res,
1652 Err(e) => return e.into(),
1653 };
1654
1655 let SurfnetRpcContext {
1656 svm_locker,
1657 remote_ctx,
1658 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1659 Ok(res) => res,
1660 Err(e) => return e.into(),
1661 };
1662
1663 Box::pin(async move {
1664 let SvmAccessContext { slot, .. } = svm_locker
1665 .clone_program_account(&remote_ctx, &source_program_id, &destination_program_id)
1666 .await?;
1667
1668 Ok(RpcResponse {
1669 context: RpcResponseContext::new(slot),
1670 value: (),
1671 })
1672 })
1673 }
1674
1675 fn profile_transaction(
1676 &self,
1677 meta: Self::Metadata,
1678 transaction_data_b64: String,
1679 tag: Option<String>,
1680 config: Option<RpcProfileResultConfig>,
1681 ) -> BoxFuture<Result<RpcResponse<UiKeyedProfileResult>>> {
1682 Box::pin(async move {
1683 let (_, transaction) = decode_and_deserialize::<VersionedTransaction>(
1684 transaction_data_b64,
1685 solana_transaction_status::TransactionBinaryEncoding::Base64,
1686 )?;
1687
1688 let SurfnetRpcContext {
1689 svm_locker,
1690 remote_ctx,
1691 } = meta.get_rpc_context(CommitmentConfig::confirmed())?;
1692
1693 let SvmAccessContext {
1694 slot, inner: uuid, ..
1695 } = svm_locker
1696 .profile_transaction(&remote_ctx, transaction, tag.clone())
1697 .await?;
1698
1699 let key = UuidOrSignature::Uuid(uuid);
1700
1701 let config = config.unwrap_or_default();
1702 let ui_result = svm_locker
1703 .get_profile_result(key, &config)?
1704 .ok_or(SurfpoolError::expected_profile_not_found(&key))?;
1705
1706 Ok(RpcResponse {
1707 context: RpcResponseContext::new(slot),
1708 value: ui_result,
1709 })
1710 })
1711 }
1712
1713 fn get_profile_results_by_tag(
1714 &self,
1715 meta: Self::Metadata,
1716 tag: String,
1717 config: Option<RpcProfileResultConfig>,
1718 ) -> Result<RpcResponse<Option<Vec<UiKeyedProfileResult>>>> {
1719 let config = config.unwrap_or_default();
1720 let svm_locker = meta.get_svm_locker()?;
1721 let profiles = svm_locker.get_profile_results_by_tag(tag, &config)?;
1722 let slot = svm_locker.get_latest_absolute_slot();
1723 Ok(RpcResponse {
1724 context: RpcResponseContext::new(slot),
1725 value: profiles,
1726 })
1727 }
1728
1729 fn set_supply(
1730 &self,
1731 meta: Self::Metadata,
1732 update: SupplyUpdate,
1733 ) -> BoxFuture<Result<RpcResponse<()>>> {
1734 let svm_locker = match meta.get_svm_locker() {
1735 Ok(locker) => locker,
1736 Err(e) => return e.into(),
1737 };
1738
1739 if let Some(ref non_circulating_accounts) = update.non_circulating_accounts {
1741 if let Err(e) = verify_pubkeys(non_circulating_accounts) {
1742 return e.into();
1743 }
1744 }
1745
1746 Box::pin(async move {
1747 let latest_absolute_slot = svm_locker.with_svm_writer(|svm_writer| {
1748 if let Some(total) = update.total {
1750 svm_writer.total_supply = total;
1751 }
1752
1753 if let Some(circulating) = update.circulating {
1754 svm_writer.circulating_supply = circulating;
1755 }
1756
1757 if let Some(non_circulating) = update.non_circulating {
1758 svm_writer.non_circulating_supply = non_circulating;
1759 }
1760
1761 if let Some(ref accounts) = update.non_circulating_accounts {
1762 svm_writer.non_circulating_accounts = accounts.clone();
1763 }
1764
1765 svm_writer.get_latest_absolute_slot()
1766 });
1767
1768 Ok(RpcResponse {
1769 context: RpcResponseContext::new(latest_absolute_slot),
1770 value: (),
1771 })
1772 })
1773 }
1774
1775 fn set_program_authority(
1776 &self,
1777 meta: Self::Metadata,
1778 program_id_str: String,
1779 new_authority_str: Option<String>,
1780 ) -> BoxFuture<Result<RpcResponse<()>>> {
1781 let program_id = match verify_pubkey(&program_id_str) {
1782 Ok(res) => res,
1783 Err(e) => return e.into(),
1784 };
1785 let new_authority = if let Some(ref new_authority_str) = new_authority_str {
1786 match verify_pubkey(new_authority_str) {
1787 Ok(res) => Some(res),
1788 Err(e) => return e.into(),
1789 }
1790 } else {
1791 None
1792 };
1793
1794 let SurfnetRpcContext {
1795 svm_locker,
1796 remote_ctx,
1797 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1798 Ok(res) => res,
1799 Err(e) => return e.into(),
1800 };
1801 Box::pin(async move {
1802 let SvmAccessContext { slot, .. } = svm_locker
1803 .set_program_authority(&remote_ctx, program_id, new_authority)
1804 .await?;
1805
1806 Ok(RpcResponse {
1807 context: RpcResponseContext::new(slot),
1808 value: (),
1809 })
1810 })
1811 }
1812
1813 fn get_transaction_profile(
1814 &self,
1815 meta: Self::Metadata,
1816 signature_or_uuid: UuidOrSignature,
1817 config: Option<RpcProfileResultConfig>,
1818 ) -> Result<RpcResponse<Option<UiKeyedProfileResult>>> {
1819 let config = config.unwrap_or_default();
1820 let svm_locker = meta.get_svm_locker()?;
1821 let profile_result = svm_locker.get_profile_result(signature_or_uuid, &config)?;
1822 let context_slot = profile_result
1823 .as_ref()
1824 .map(|pr| pr.slot)
1825 .unwrap_or_else(|| svm_locker.get_latest_absolute_slot());
1826 Ok(RpcResponse {
1827 context: RpcResponseContext::new(context_slot),
1828 value: profile_result,
1829 })
1830 }
1831
1832 fn register_idl(
1833 &self,
1834 meta: Self::Metadata,
1835 idl: Idl,
1836 slot: Option<Slot>,
1837 ) -> Result<RpcResponse<()>> {
1838 let svm_locker = match meta.get_svm_locker() {
1839 Ok(locker) => locker,
1840 Err(e) => return Err(e.into()),
1841 };
1842 svm_locker.register_idl(idl, slot)?;
1843 Ok(RpcResponse {
1844 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1845 value: (),
1846 })
1847 }
1848
1849 fn get_idl(
1850 &self,
1851 meta: Self::Metadata,
1852 program_id: String,
1853 slot: Option<Slot>,
1854 ) -> Result<RpcResponse<Option<Idl>>> {
1855 let svm_locker = match meta.get_svm_locker() {
1856 Ok(locker) => locker,
1857 Err(e) => return Err(e.into()),
1858 };
1859 let program_id = match verify_pubkey(&program_id) {
1860 Ok(pk) => pk,
1861 Err(e) => return Err(e.into()),
1862 };
1863 let idl = svm_locker.get_idl(&program_id, slot);
1864 let slot = slot.unwrap_or_else(|| svm_locker.get_latest_absolute_slot());
1865 Ok(RpcResponse {
1866 context: RpcResponseContext::new(slot),
1867 value: idl,
1868 })
1869 }
1870
1871 fn get_local_signatures(
1872 &self,
1873 meta: Self::Metadata,
1874 limit: Option<u64>,
1875 ) -> BoxFuture<Result<RpcResponse<Vec<RpcLogsResponse>>>> {
1876 let svm_locker = match meta.get_svm_locker() {
1877 Ok(locker) => locker,
1878 Err(e) => return e.into(),
1879 };
1880
1881 let limit = limit.unwrap_or(50);
1882 let latest = svm_locker.get_latest_absolute_slot();
1883 if limit == 0 {
1884 return Box::pin(async move {
1885 Ok(RpcResponse {
1886 context: RpcResponseContext::new(latest),
1887 value: Vec::new(),
1888 })
1889 });
1890 }
1891
1892 let mut items: Vec<(
1893 String,
1894 Slot,
1895 Option<solana_transaction_error::TransactionError>,
1896 Vec<String>,
1897 )> = svm_locker.with_svm_reader(|svm_reader| {
1898 svm_reader
1899 .transactions
1900 .into_iter()
1901 .map(|iter| {
1902 iter.map(|(sig, status)| {
1903 let (transaction_with_status_meta, _) = status.expect_processed();
1904 (
1905 sig,
1906 transaction_with_status_meta.slot,
1907 transaction_with_status_meta.meta.status.clone().err(),
1908 transaction_with_status_meta
1909 .meta
1910 .log_messages
1911 .clone()
1912 .unwrap_or_default(),
1913 )
1914 })
1915 .collect()
1916 })
1917 .unwrap_or_default()
1918 });
1919
1920 items.sort_by(|a, b| b.1.cmp(&a.1));
1921 items.truncate(limit as usize);
1922
1923 let value: Vec<RpcLogsResponse> = items
1924 .into_iter()
1925 .map(|(signature, _slot, err, logs)| RpcLogsResponse {
1926 signature,
1927 err: err.map(|e| e.into()),
1928 logs,
1929 })
1930 .collect();
1931
1932 Box::pin(async move {
1933 Ok(RpcResponse {
1934 context: RpcResponseContext::new(latest),
1935 value,
1936 })
1937 })
1938 }
1939
1940 fn pause_clock(&self, meta: Self::Metadata) -> Result<EpochInfo> {
1941 let key = meta.as_ref().map(|ctx| ctx.id.clone()).unwrap_or_default();
1942 let surfnet_command_tx: crossbeam_channel::Sender<SimnetCommand> =
1943 meta.get_surfnet_command_tx()?;
1944
1945 let (response_tx, response_rx) = crossbeam_channel::bounded(1);
1947
1948 let _ = surfnet_command_tx.send(SimnetCommand::CommandClock(
1950 key,
1951 ClockCommand::PauseWithConfirmation(response_tx),
1952 ));
1953
1954 response_rx
1956 .recv_timeout(std::time::Duration::from_secs(2))
1957 .map_err(|e| jsonrpc_core::Error {
1958 code: jsonrpc_core::ErrorCode::InternalError,
1959 message: format!("Failed to confirm clock pause: {}", e),
1960 data: None,
1961 })
1962 }
1963
1964 fn resume_clock(&self, meta: Self::Metadata) -> Result<EpochInfo> {
1965 let key = meta.as_ref().map(|ctx| ctx.id.clone()).unwrap_or_default();
1966 let surfnet_command_tx: crossbeam_channel::Sender<SimnetCommand> =
1967 meta.get_surfnet_command_tx()?;
1968 let _ = surfnet_command_tx.send(SimnetCommand::CommandClock(key, ClockCommand::Resume));
1969 meta.with_svm_reader(|svm_reader| svm_reader.latest_epoch_info.clone())
1970 .map_err(Into::into)
1971 }
1972
1973 fn time_travel(
1974 &self,
1975 meta: Self::Metadata,
1976 config: Option<TimeTravelConfig>,
1977 ) -> Result<EpochInfo> {
1978 let key = meta.as_ref().map(|ctx| ctx.id.clone()).unwrap_or_default();
1979 let time_travel_config = config.unwrap_or_default();
1980 let simnet_command_tx = meta.get_surfnet_command_tx()?;
1981 let svm_locker = meta.get_svm_locker()?;
1982
1983 let epoch_info = svm_locker.time_travel(key, simnet_command_tx, time_travel_config)?;
1984
1985 Ok(epoch_info)
1986 }
1987
1988 fn reset_account(
1989 &self,
1990 meta: Self::Metadata,
1991 pubkey: String,
1992 config: Option<ResetAccountConfig>,
1993 ) -> Result<RpcResponse<()>> {
1994 let svm_locker = meta.get_svm_locker()?;
1995 let pubkey = verify_pubkey(&pubkey)?;
1996 let config = config.unwrap_or_default();
1997 let include_owned_accounts = config.include_owned_accounts.unwrap_or_default();
1998 svm_locker.reset_account(pubkey, include_owned_accounts)?;
1999 Ok(RpcResponse {
2000 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2001 value: (),
2002 })
2003 }
2004
2005 fn reset_network(&self, meta: Self::Metadata) -> BoxFuture<Result<RpcResponse<()>>> {
2006 let SurfnetRpcContext {
2007 svm_locker,
2008 remote_ctx,
2009 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2010 Ok(res) => res,
2011 Err(e) => return e.into(),
2012 };
2013
2014 let remote_client = remote_ctx.as_ref().map(|(client, _)| client.clone());
2016
2017 Box::pin(async move {
2018 svm_locker.reset_network(&remote_client).await?;
2019 Ok(RpcResponse {
2020 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2021 value: (),
2022 })
2023 })
2024 }
2025
2026 fn offline_account(
2027 &self,
2028 meta: Self::Metadata,
2029 pubkey_str: String,
2030 config: Option<OfflineAccountConfig>,
2031 ) -> BoxFuture<Result<RpcResponse<()>>> {
2032 let SurfnetRpcContext { svm_locker, .. } =
2033 match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2034 Ok(res) => res,
2035 Err(e) => return e.into(),
2036 };
2037 let pubkey = match verify_pubkey(&pubkey_str) {
2038 Ok(res) => res,
2039 Err(e) => return e.into(),
2040 };
2041 let config = config.unwrap_or_default();
2042 let include_owned_accounts = config.include_owned_accounts.unwrap_or_default();
2043
2044 Box::pin(async move {
2045 svm_locker
2046 .insert_offline_account(pubkey, include_owned_accounts)
2047 .await?;
2048 Ok(RpcResponse {
2049 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2050 value: (),
2051 })
2052 })
2053 }
2054
2055 fn stream_account(
2056 &self,
2057 meta: Self::Metadata,
2058 pubkey_str: String,
2059 config: Option<StreamAccountConfig>,
2060 ) -> Result<RpcResponse<()>> {
2061 let svm_locker = meta.get_svm_locker()?;
2062 let pubkey = verify_pubkey(&pubkey_str)?;
2063 let config = config.unwrap_or_default();
2064 let include_owned_accounts = config.include_owned_accounts.unwrap_or_default();
2065 svm_locker.stream_account(pubkey, include_owned_accounts)?;
2066 Ok(RpcResponse {
2067 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2068 value: (),
2069 })
2070 }
2071
2072 fn stream_accounts(
2073 &self,
2074 meta: Self::Metadata,
2075 accounts: Vec<StreamAccountsEntry>,
2076 ) -> Result<RpcResponse<()>> {
2077 let svm_locker = meta.get_svm_locker()?;
2078 for entry in accounts {
2079 let pubkey = verify_pubkey(&entry.pubkey)?;
2080 let include_owned_accounts = entry.include_owned_accounts.unwrap_or_default();
2081 svm_locker.stream_account(pubkey, include_owned_accounts)?;
2082 }
2083 Ok(RpcResponse {
2084 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2085 value: (),
2086 })
2087 }
2088
2089 fn get_streamed_accounts(
2090 &self,
2091 meta: Self::Metadata,
2092 ) -> Result<RpcResponse<GetStreamedAccountsResponse>> {
2093 let svm_locker = meta.get_svm_locker()?;
2094
2095 let value = svm_locker.with_svm_reader(|svm_reader| {
2096 let accounts: Vec<_> = svm_reader
2097 .streamed_accounts
2098 .into_iter()
2099 .map(|iter| iter.collect())
2100 .unwrap_or_default();
2101 GetStreamedAccountsResponse::from_iter(accounts)
2102 });
2103
2104 Ok(RpcResponse {
2105 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2106 value,
2107 })
2108 }
2109
2110 fn get_surfnet_info(
2111 &self,
2112 meta: Self::Metadata,
2113 ) -> Result<RpcResponse<GetSurfnetInfoResponse>> {
2114 let svm_locker = meta.get_svm_locker()?;
2115 let runbook_executions = svm_locker.runbook_executions();
2116 Ok(RpcResponse {
2117 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2118 value: GetSurfnetInfoResponse::new(runbook_executions),
2119 })
2120 }
2121
2122 fn write_program(
2123 &self,
2124 meta: Self::Metadata,
2125 program_id_str: String,
2126 data_hex: String,
2127 offset: usize,
2128 authority: Option<String>,
2129 ) -> BoxFuture<Result<RpcResponse<()>>> {
2130 let program_id = match verify_pubkey(&program_id_str) {
2132 Ok(res) => res,
2133 Err(e) => return e.into(),
2134 };
2135
2136 let authority = if let Some(ref authority_str) = authority {
2137 match verify_pubkey(authority_str) {
2138 Ok(res) => Some(res),
2139 Err(e) => return e.into(),
2140 }
2141 } else {
2142 None
2143 };
2144
2145 let data = match hex::decode(&data_hex) {
2147 Ok(data) => data,
2148 Err(e) => {
2149 return Box::pin(future::err(Error::invalid_params(format!(
2150 "Invalid hex data provided: {}",
2151 e
2152 ))));
2153 }
2154 };
2155
2156 if offset.checked_add(data.len()).is_none() {
2158 return Box::pin(future::err(Error::invalid_params(
2159 "Offset + data length causes integer overflow",
2160 )));
2161 }
2162
2163 let SurfnetRpcContext {
2164 svm_locker,
2165 remote_ctx,
2166 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2167 Ok(res) => res,
2168 Err(e) => return e.into(),
2169 };
2170
2171 Box::pin(async move {
2172 let slot = svm_locker.get_latest_absolute_slot();
2173
2174 svm_locker
2175 .write_program(program_id, authority, offset, &data, &remote_ctx)
2176 .await?;
2177
2178 Ok(RpcResponse {
2179 context: RpcResponseContext::new(slot),
2180 value: (),
2181 })
2182 })
2183 }
2184
2185 fn export_snapshot(
2186 &self,
2187 meta: Self::Metadata,
2188 config: Option<ExportSnapshotConfig>,
2189 ) -> Result<RpcResponse<BTreeMap<String, AccountSnapshot>>> {
2190 let config = config.unwrap_or_default();
2191 let svm_locker = meta.get_svm_locker()?;
2192 let snapshot = svm_locker.export_snapshot(config)?;
2193 Ok(RpcResponse {
2194 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2195 value: snapshot,
2196 })
2197 }
2198
2199 fn register_scenario(
2200 &self,
2201 meta: Self::Metadata,
2202 scenario: Scenario,
2203 slot: Option<Slot>,
2204 ) -> BoxFuture<Result<RpcResponse<()>>> {
2205 let SurfnetRpcContext {
2206 svm_locker,
2207 remote_ctx,
2208 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
2209 Ok(res) => res,
2210 Err(e) => return e.into(),
2211 };
2212
2213 Box::pin(async move {
2214 let base_slot = slot.unwrap_or_else(|| svm_locker.get_latest_absolute_slot());
2216
2217 svm_locker
2219 .register_scenario(scenario, Some(base_slot))
2220 .map_err(|e| jsonrpc_core::Error {
2221 code: jsonrpc_core::ErrorCode::InternalError,
2222 message: format!("Failed to register scenario: {}", e),
2223 data: None,
2224 })?;
2225
2226 svm_locker
2229 .materialize_overrides_for_slot(&remote_ctx, base_slot)
2230 .await
2231 .map_err(|e| jsonrpc_core::Error {
2232 code: jsonrpc_core::ErrorCode::InternalError,
2233 message: format!("Failed to materialize initial overrides: {}", e),
2234 data: None,
2235 })?;
2236
2237 Ok(RpcResponse {
2238 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2239 value: (),
2240 })
2241 })
2242 }
2243}
2244
2245#[cfg(test)]
2246mod tests {
2247 use base64::Engine as _;
2248 use solana_account_decoder::{
2249 UiAccountData, UiAccountEncoding, parse_account_data::ParsedAccount,
2250 };
2251 use solana_keypair::Keypair;
2252 use solana_program_pack::Pack;
2253 use solana_pubkey::Pubkey;
2254 use solana_signer::Signer;
2255 use solana_system_interface::instruction::create_account;
2256 use solana_transaction::{Transaction, versioned::VersionedTransaction};
2257 use spl_associated_token_account_interface::{
2258 address::get_associated_token_address_with_program_id,
2259 instruction::create_associated_token_account,
2260 };
2261 use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
2262 use spl_token_interface::state::Mint;
2263 use surfpool_types::{
2264 ExportSnapshotFilter, ExportSnapshotScope, RpcProfileDepth, UiAccountChange,
2265 UiAccountProfileState,
2266 };
2267
2268 use super::*;
2269 use crate::{rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc, tests::helpers::TestSetup};
2270
2271 #[tokio::test(flavor = "multi_thread")]
2272 async fn test_get_transaction_profile() {
2273 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
2275 let recent_blockhash = client
2276 .context
2277 .svm_locker
2278 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2279
2280 let payer = Keypair::new();
2282
2283 let owner = Keypair::new();
2284
2285 let recipient = Keypair::new();
2287
2288 client
2290 .context
2291 .svm_locker
2292 .airdrop(&payer.pubkey(), 1_000_000_000)
2293 .unwrap()
2294 .unwrap();
2295
2296 client
2298 .context
2299 .svm_locker
2300 .airdrop(&recipient.pubkey(), 1_000_000_000)
2301 .unwrap()
2302 .unwrap();
2303
2304 let mint = Keypair::new();
2306
2307 let mint_space = Mint::LEN;
2309 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
2310 svm_reader
2311 .inner
2312 .minimum_balance_for_rent_exemption(mint_space)
2313 });
2314
2315 let create_account_instruction = create_account(
2317 &payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022_interface::id(), );
2323
2324 let initialize_mint_instruction = initialize_mint2(
2326 &spl_token_2022_interface::id(),
2327 &mint.pubkey(), &payer.pubkey(), Some(&payer.pubkey()), 2, )
2332 .unwrap();
2333
2334 let source_ata = get_associated_token_address_with_program_id(
2336 &owner.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2340
2341 let create_source_ata_instruction = create_associated_token_account(
2343 &payer.pubkey(), &owner.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2348
2349 let destination_ata = get_associated_token_address_with_program_id(
2351 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2355
2356 let create_destination_ata_instruction = create_associated_token_account(
2358 &payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2363
2364 let amount = 10_000;
2366
2367 let mint_to_instruction = mint_to(
2369 &spl_token_2022_interface::id(),
2370 &mint.pubkey(), &source_ata, &payer.pubkey(), &[&payer.pubkey()], amount, )
2376 .unwrap();
2377
2378 let transaction = Transaction::new_signed_with_payer(
2380 &[
2381 create_account_instruction,
2382 initialize_mint_instruction,
2383 create_source_ata_instruction,
2384 create_destination_ata_instruction,
2385 mint_to_instruction,
2386 ],
2387 Some(&payer.pubkey()),
2388 &[&payer, &mint],
2389 recent_blockhash,
2390 );
2391
2392 let signature = transaction.signatures[0];
2393
2394 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
2395 client
2396 .context
2397 .svm_locker
2398 .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
2399 .await
2400 .unwrap();
2401
2402 {
2404 let ui_profile_result = client
2405 .rpc
2406 .get_transaction_profile(
2407 Some(client.context.clone()),
2408 UuidOrSignature::Signature(signature),
2409 Some(RpcProfileResultConfig {
2410 depth: Some(RpcProfileDepth::Instruction),
2411 ..Default::default()
2412 }),
2413 )
2414 .unwrap()
2415 .value
2416 .expect("missing profile result for processed transaction");
2417
2418 {
2420 let ix_profile = ui_profile_result
2421 .instruction_profiles
2422 .as_ref()
2423 .unwrap()
2424 .first()
2425 .expect("instruction profile should exist");
2426 assert!(
2427 ix_profile.error_message.is_none(),
2428 "Profile should succeed, found error: {}",
2429 ix_profile.error_message.as_ref().unwrap()
2430 );
2431 assert_eq!(ix_profile.compute_units_consumed, 150);
2432 assert!(ix_profile.error_message.is_none());
2433 let account_states = &ix_profile.account_states;
2434
2435 let UiAccountProfileState::Writable(sender_account_change) = account_states
2436 .get(&payer.pubkey())
2437 .expect("Payer account state should be present")
2438 else {
2439 panic!("Expected account state to be Writable");
2440 };
2441
2442 match sender_account_change {
2443 UiAccountChange::Update(before, after) => {
2444 assert_eq!(
2445 after.lamports,
2446 before.lamports - mint_rent - (2 * 5000), "Payer account should be original balance minus rent"
2448 );
2449 }
2450 other => {
2451 panic!("Expected account state to be an Update, got: {:?}", other);
2452 }
2453 }
2454
2455 let UiAccountProfileState::Writable(mint_account_change) = account_states
2456 .get(&mint.pubkey())
2457 .expect("Mint account state should be present")
2458 else {
2459 panic!("Expected mint account state to be Writable");
2460 };
2461 match mint_account_change {
2462 UiAccountChange::Create(mint_account) => {
2463 assert_eq!(
2464 mint_account.lamports, mint_rent,
2465 "Mint account should have the correct rent amount"
2466 );
2467 assert_eq!(
2468 mint_account.owner,
2469 spl_token_2022_interface::id().to_string(),
2470 "Mint account should be owned by the SPL Token program"
2471 );
2472 assert_eq!(
2474 mint_account.data,
2475 UiAccountData::Binary(
2476 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".into(),
2477 UiAccountEncoding::Base64
2478 ),
2479 );
2480 }
2481 other => {
2482 panic!("Expected account state to be an Update, got: {:?}", other);
2483 }
2484 }
2485 }
2486
2487 {
2489 let ix_profile = ui_profile_result
2490 .instruction_profiles
2491 .as_ref()
2492 .unwrap()
2493 .get(1)
2494 .expect("instruction profile should exist");
2495
2496 assert!(
2497 ix_profile.error_message.is_none(),
2498 "Profile should succeed, found error: {}",
2499 ix_profile.error_message.as_ref().unwrap()
2500 );
2501 assert_eq!(ix_profile.compute_units_consumed, 1278);
2502 let account_states = &ix_profile.account_states;
2503
2504 assert!(account_states.get(&payer.pubkey()).is_none());
2505
2506 let UiAccountProfileState::Writable(mint_account_change) = account_states
2507 .get(&mint.pubkey())
2508 .expect("Mint account state should be present")
2509 else {
2510 panic!("Expected mint account state to be Writable");
2511 };
2512 match mint_account_change {
2513 UiAccountChange::Update(_before, after) => {
2514 assert_eq!(
2515 after.lamports, mint_rent,
2516 "Mint account should have the correct rent amount"
2517 );
2518 assert_eq!(
2519 after.owner,
2520 spl_token_2022_interface::id().to_string(),
2521 "Mint account should be owned by the SPL Token program"
2522 );
2523 assert_eq!(
2525 after.data,
2526 UiAccountData::Json(ParsedAccount {
2527 program: "spl-token-2022".to_string(),
2528 parsed: json!({
2529 "info": {
2530 "decimals": 2,
2531 "freezeAuthority": payer.pubkey().to_string(),
2532 "mintAuthority": payer.pubkey().to_string(),
2533 "isInitialized": true,
2534 "supply": "0",
2535 },
2536 "type": "mint"
2537 }),
2538 space: 82,
2539 }),
2540 );
2541 }
2542 other => {
2543 panic!("Expected account state to be an Update, got: {:?}", other);
2544 }
2545 }
2546 }
2547
2548 {
2550 let ix_profile = ui_profile_result
2551 .instruction_profiles
2552 .as_ref()
2553 .unwrap()
2554 .get(2)
2555 .expect("instruction profile should exist");
2556 assert!(
2557 ix_profile.error_message.is_none(),
2558 "Profile should succeed, found error: {}",
2559 ix_profile.error_message.as_ref().unwrap()
2560 );
2561
2562 let account_states = &ix_profile.account_states;
2563
2564 let UiAccountProfileState::Writable(sender_account_change) = account_states
2565 .get(&payer.pubkey())
2566 .expect("Payer account state should be present")
2567 else {
2568 panic!("Expected account state to be Writable");
2569 };
2570
2571 match sender_account_change {
2572 UiAccountChange::Update(before, after) => {
2573 assert_eq!(
2574 after.lamports,
2575 before.lamports - 2074080,
2576 "Payer account should be original balance minus rent"
2577 );
2578 }
2579 other => {
2580 panic!("Expected account state to be an Update, got: {:?}", other);
2581 }
2582 }
2583
2584 let UiAccountProfileState::Writable(mint_account_change) = account_states
2585 .get(&mint.pubkey())
2586 .expect("Mint account state should be present")
2587 else {
2588 panic!("Expected mint account state to be Writable");
2589 };
2590 match mint_account_change {
2591 UiAccountChange::Unchanged(mint_account) => {
2592 assert!(mint_account.is_some())
2593 }
2594 other => {
2595 panic!("Expected account state to be Unchanged, got: {:?}", other);
2596 }
2597 }
2598
2599 let UiAccountProfileState::Writable(source_ata_change) = account_states
2600 .get(&source_ata)
2601 .expect("account state should be present")
2602 else {
2603 panic!("Expected account state to be Writable");
2604 };
2605
2606 match source_ata_change {
2607 UiAccountChange::Create(new) => {
2608 assert_eq!(
2609 new.lamports, 2074080,
2610 "Source ATA should have the correct lamports after creation"
2611 );
2612 assert_eq!(
2613 new.owner,
2614 spl_token_2022_interface::id().to_string(),
2615 "Source ATA should be owned by the SPL Token program"
2616 );
2617
2618 match &new.data {
2619 UiAccountData::Json(parsed) => {
2620 assert_eq!(
2621 parsed,
2622 &ParsedAccount {
2623 program: "spl-token-2022".into(),
2624 parsed: json!({
2625 "info": {
2626 "extensions": [
2627 {
2628 "extension": "immutableOwner"
2629 }
2630 ],
2631 "isNative": false,
2632 "mint": mint.pubkey().to_string(),
2633 "owner": owner.pubkey().to_string(),
2634 "state": "initialized",
2635 "tokenAmount": {
2636 "amount": "0",
2637 "decimals": 2,
2638 "uiAmount": 0.0,
2639 "uiAmountString": "0"
2640 }
2641 },
2642 "type": "account"
2643 }),
2644 space: 170
2645 }
2646 );
2647 }
2648 _ => panic!("Expected source ATA data to be JSON"),
2649 }
2650 }
2651 other => {
2652 panic!("Expected account state to be Create, got: {:?}", other);
2653 }
2654 }
2655 }
2656
2657 {
2659 let ix_profile = ui_profile_result
2660 .instruction_profiles
2661 .as_ref()
2662 .unwrap()
2663 .get(3)
2664 .expect("instruction profile should exist");
2665 assert!(
2666 ix_profile.error_message.is_none(),
2667 "Profile should succeed, found error: {}",
2668 ix_profile.error_message.as_ref().unwrap()
2669 );
2670
2671 let account_states = &ix_profile.account_states;
2672
2673 let UiAccountProfileState::Writable(sender_account_change) = account_states
2674 .get(&payer.pubkey())
2675 .expect("Payer account state should be present")
2676 else {
2677 panic!("Expected account state to be Writable");
2678 };
2679
2680 match sender_account_change {
2681 UiAccountChange::Update(before, after) => {
2682 assert_eq!(
2683 after.lamports,
2684 before.lamports - 2074080,
2685 "Payer account should be original balance minus rent"
2686 );
2687 }
2688 other => {
2689 panic!("Expected account state to be an Update, got: {:?}", other);
2690 }
2691 }
2692
2693 let UiAccountProfileState::Writable(mint_account_change) = account_states
2694 .get(&mint.pubkey())
2695 .expect("Mint account state should be present")
2696 else {
2697 panic!("Expected mint account state to be Writable");
2698 };
2699 match mint_account_change {
2700 UiAccountChange::Unchanged(mint_account) => {
2701 assert!(mint_account.is_some())
2702 }
2703 other => {
2704 panic!("Expected account state to be Unchanged, got: {:?}", other);
2705 }
2706 }
2707
2708 let UiAccountProfileState::Writable(destination_ata_change) = account_states
2709 .get(&destination_ata)
2710 .expect("account state should be present")
2711 else {
2712 panic!("Expected account state to be Writable");
2713 };
2714
2715 match destination_ata_change {
2716 UiAccountChange::Create(new) => {
2717 assert_eq!(
2718 new.lamports, 2074080,
2719 "Source ATA should have the correct lamports after creation"
2720 );
2721 assert_eq!(
2722 new.owner,
2723 spl_token_2022_interface::id().to_string(),
2724 "Source ATA should be owned by the SPL Token program"
2725 );
2726 match &new.data {
2727 UiAccountData::Json(parsed) => {
2728 assert_eq!(
2729 parsed,
2730 &ParsedAccount {
2731 program: "spl-token-2022".into(),
2732 parsed: json!({
2733 "info": {
2734 "extensions": [
2735 {
2736 "extension": "immutableOwner"
2737 }
2738 ],
2739 "isNative": false,
2740 "mint": mint.pubkey().to_string(),
2741 "owner": recipient.pubkey().to_string(),
2742 "state": "initialized",
2743 "tokenAmount": {
2744 "amount": "0",
2745 "decimals": 2,
2746 "uiAmount": 0.0,
2747 "uiAmountString": "0"
2748 }
2749 },
2750 "type": "account"
2751 }),
2752 space: 170
2753 }
2754 );
2755 }
2756 _ => panic!("Expected source ATA data to be JSON"),
2757 }
2758 }
2759 other => {
2760 panic!("Expected account state to be Create, got: {:?}", other);
2761 }
2762 }
2763 }
2764
2765 {
2767 let ix_profile = ui_profile_result
2768 .instruction_profiles
2769 .as_ref()
2770 .unwrap()
2771 .get(4)
2772 .expect("instruction profile should exist");
2773 assert!(
2774 ix_profile.error_message.is_none(),
2775 "Profile should succeed, found error: {}",
2776 ix_profile.error_message.as_ref().unwrap()
2777 );
2778
2779 let account_states = &ix_profile.account_states;
2780
2781 let UiAccountProfileState::Writable(sender_account_change) = account_states
2782 .get(&payer.pubkey())
2783 .expect("Payer account state should be present")
2784 else {
2785 panic!("Expected account state to be Writable");
2786 };
2787
2788 match sender_account_change {
2789 UiAccountChange::Unchanged(unchanged) => {
2790 assert!(unchanged.is_some(), "Payer account should remain unchanged");
2791 }
2792 other => {
2793 panic!("Expected account state to be Unchanged, got: {:?}", other);
2794 }
2795 }
2796
2797 let UiAccountProfileState::Writable(mint_account_change) = account_states
2798 .get(&mint.pubkey())
2799 .expect("Mint account state should be present")
2800 else {
2801 panic!("Expected mint account state to be Writable");
2802 };
2803 match mint_account_change {
2804 UiAccountChange::Update(before, after) => {
2805 assert_eq!(
2806 after.lamports, before.lamports,
2807 "Lamports should stay the same for mint account"
2808 );
2809 assert_eq!(
2810 after.data,
2811 UiAccountData::Json(ParsedAccount {
2812 program: "spl-token-2022".into(),
2813 parsed: json!({
2814 "info": {
2815 "decimals": 2,
2816 "freezeAuthority": payer.pubkey().to_string(),
2817 "isInitialized": true,
2818 "mintAuthority": payer.pubkey().to_string(),
2819 "supply": "10000",
2820 },
2821 "type": "mint"
2822 }),
2823 space: 82
2824 }),
2825 "Data should stay the same for mint account"
2826 );
2827 }
2828 other => {
2829 panic!("Expected account state to be Update, got: {:?}", other);
2830 }
2831 }
2832 }
2833
2834 assert_eq!(
2835 ui_profile_result.transaction_profile.compute_units_consumed,
2836 ui_profile_result
2837 .instruction_profiles
2838 .as_ref()
2839 .unwrap()
2840 .iter()
2841 .map(|ix| ix.compute_units_consumed)
2842 .sum::<u64>(),
2843 )
2844 }
2845 let recent_blockhash = client
2847 .context
2848 .svm_locker
2849 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2850
2851 let transfer_amount = 50;
2853
2854 let transfer_instruction = transfer_checked(
2856 &spl_token_2022_interface::id(), &source_ata, &mint.pubkey(), &destination_ata, &owner.pubkey(), &[&payer.pubkey(), &owner.pubkey()], transfer_amount, 2, )
2865 .unwrap();
2866
2867 let transaction = Transaction::new_signed_with_payer(
2869 &[transfer_instruction],
2870 Some(&payer.pubkey()),
2871 &[&payer, &owner],
2872 recent_blockhash,
2873 );
2874 let signature = transaction.signatures[0];
2875 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
2876 client
2878 .context
2879 .svm_locker
2880 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
2881 .await
2882 .unwrap();
2883
2884 {
2885 let profile_result = client
2886 .rpc
2887 .get_transaction_profile(
2888 Some(client.context.clone()),
2889 UuidOrSignature::Signature(signature),
2890 Some(RpcProfileResultConfig {
2891 depth: Some(RpcProfileDepth::Instruction),
2892 ..Default::default()
2893 }),
2894 )
2895 .unwrap()
2896 .value
2897 .expect("missing profile result for processed transaction");
2898
2899 assert!(
2900 profile_result.transaction_profile.error_message.is_none(),
2901 "Transaction should succeed, found error: {}",
2902 profile_result
2903 .transaction_profile
2904 .error_message
2905 .as_ref()
2906 .unwrap()
2907 );
2908
2909 assert_eq!(
2910 profile_result.instruction_profiles.as_ref().unwrap().len(),
2911 1
2912 );
2913
2914 let ix_profile = profile_result
2915 .instruction_profiles
2916 .as_ref()
2917 .unwrap()
2918 .first()
2919 .expect("instruction profile should exist");
2920 assert!(
2921 ix_profile.error_message.is_none(),
2922 "Profile should succeed, found error: {}",
2923 ix_profile.error_message.as_ref().unwrap()
2924 );
2925
2926 let mut account_states = ix_profile.account_states.clone();
2927
2928 let UiAccountProfileState::Writable(owner_account_change) = account_states
2929 .swap_remove(&owner.pubkey())
2930 .expect("account state should be present")
2931 else {
2932 panic!("Expected account state to be Writable");
2933 };
2934
2935 let UiAccountChange::Unchanged(unchanged) = owner_account_change else {
2936 panic!(
2937 "Expected account state to be Unchanged, got: {:?}",
2938 owner_account_change
2939 );
2940 };
2941 assert!(unchanged.is_none(), "Owner account shouldn't exist");
2942
2943 let UiAccountProfileState::Writable(sender_account_change) = account_states
2944 .swap_remove(&payer.pubkey())
2945 .expect("Payer account state should be present")
2946 else {
2947 panic!("Expected account state to be Writable");
2948 };
2949
2950 match sender_account_change {
2951 UiAccountChange::Update(before, after) => {
2952 assert_eq!(after.lamports, before.lamports - 10000);
2953 }
2954 other => {
2955 panic!("Expected account state to be an Update, got: {:?}", other);
2956 }
2957 }
2958
2959 let UiAccountProfileState::Readonly = account_states
2960 .swap_remove(&mint.pubkey())
2961 .expect("Mint account state should be present")
2962 else {
2963 panic!("Expected mint account state to be Readonly");
2964 };
2965 let UiAccountProfileState::Readonly = account_states
2966 .swap_remove(&spl_token_2022_interface::ID)
2967 .expect("account state should be present")
2968 else {
2969 panic!("Expected account state to be Readonly");
2970 };
2971
2972 let UiAccountProfileState::Writable(source_ata_change) = account_states
2973 .swap_remove(&source_ata)
2974 .expect("account state should be present")
2975 else {
2976 panic!("Expected account state to be Writable");
2977 };
2978
2979 match source_ata_change {
2980 UiAccountChange::Update(before, after) => {
2981 assert_eq!(
2982 after.lamports, before.lamports,
2983 "Source ATA lamports should remain unchanged"
2984 );
2985 assert_eq!(
2986 after.data,
2987 UiAccountData::Json(ParsedAccount {
2988 program: "spl-token-2022".into(),
2989 parsed: json!({
2990 "info": {
2991 "extensions": [
2992 {
2993 "extension": "immutableOwner"
2994 }
2995 ],
2996 "isNative": false,
2997 "mint": mint.pubkey().to_string(),
2998 "owner": owner.pubkey().to_string(),
2999 "state": "initialized",
3000 "tokenAmount": {
3001 "amount": "9950",
3002 "decimals": 2,
3003 "uiAmount": 99.5,
3004 "uiAmountString": "99.5"
3005 }
3006 },
3007 "type": "account"
3008 }),
3009 space: 170
3010 }),
3011 "Source ATA data should be updated after transfer"
3012 );
3013 }
3014 other => {
3015 panic!("Expected account state to be Update, got: {:?}", other);
3016 }
3017 }
3018
3019 let UiAccountProfileState::Writable(destination_ata_change) = account_states
3020 .swap_remove(&destination_ata)
3021 .expect("account state should be present")
3022 else {
3023 panic!("Expected account state to be Writable");
3024 };
3025
3026 match destination_ata_change {
3027 UiAccountChange::Update(before, after) => {
3028 assert_eq!(
3029 after.lamports, before.lamports,
3030 "Destination ATA lamports should remain unchanged"
3031 );
3032 assert_eq!(
3033 after.data,
3034 UiAccountData::Json(ParsedAccount {
3035 program: "spl-token-2022".into(),
3036 parsed: json!({
3037 "info": {
3038 "extensions": [
3039 {
3040 "extension": "immutableOwner"
3041 }
3042 ],
3043 "isNative": false,
3044 "mint": mint.pubkey().to_string(),
3045 "owner": recipient.pubkey().to_string(),
3046 "state": "initialized",
3047 "tokenAmount": {
3048 "amount": transfer_amount.to_string(),
3049 "decimals": 2,
3050 "uiAmount": 0.5,
3051 "uiAmountString": "0.5"
3052 }
3053 },
3054 "type": "account"
3055 }),
3056 space: 170
3057 }),
3058 "Destination ATA data should be updated after transfer"
3059 );
3060 }
3061 other => {
3062 panic!("Expected account state to be Update, got: {:?}", other);
3063 }
3064 }
3065
3066 assert!(
3067 account_states.is_empty(),
3068 "All account states should have been processed, found: {:?}",
3069 account_states
3070 );
3071 }
3072 }
3073
3074 fn set_account(client: &TestSetup<SurfnetCheatcodesRpc>, pubkey: &Pubkey, account: &Account) {
3075 client
3076 .context
3077 .svm_locker
3078 .with_svm_writer(|svm| svm.inner.set_account(*pubkey, account.clone()))
3079 .expect("Failed to set account");
3080 }
3081
3082 fn verify_snapshot_account(
3083 snapshot: &BTreeMap<String, AccountSnapshot>,
3084 expected_account_pubkey: &Pubkey,
3085 expected_account: &Account,
3086 ) {
3087 let account = snapshot
3088 .get(&expected_account_pubkey.to_string())
3089 .unwrap_or_else(|| {
3090 panic!(
3091 "Account fixture not found for pubkey {}",
3092 expected_account_pubkey
3093 )
3094 });
3095 assert_eq!(expected_account.lamports, account.lamports);
3096 assert_eq!(
3097 base64::engine::general_purpose::STANDARD.encode(&expected_account.data),
3098 account.data
3099 );
3100 assert_eq!(expected_account.owner.to_string(), account.owner);
3101 assert_eq!(expected_account.executable, account.executable);
3102 assert_eq!(expected_account.rent_epoch, account.rent_epoch);
3103 }
3104
3105 #[test]
3106 fn test_export_snapshot() {
3107 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3108
3109 let pubkey1 = Pubkey::new_unique();
3110 let account1 = Account {
3111 lamports: 1_000_000,
3112 data: vec![1, 2, 3, 4],
3113 owner: system_program::id(),
3114 executable: false,
3115 rent_epoch: 0,
3116 };
3117
3118 set_account(&client, &pubkey1, &account1);
3119
3120 let pubkey2 = Pubkey::new_unique();
3121 let account2 = Account {
3122 lamports: 2_000_000,
3123 data: vec![5, 6, 7, 8, 9],
3124 owner: system_program::id(),
3125 executable: false,
3126 rent_epoch: 0,
3127 };
3128
3129 set_account(&client, &pubkey2, &account2);
3130
3131 let snapshot = client
3132 .rpc
3133 .export_snapshot(Some(client.context.clone()), None)
3134 .expect("Failed to export snapshot")
3135 .value;
3136
3137 verify_snapshot_account(&snapshot, &pubkey1, &account1);
3138 verify_snapshot_account(&snapshot, &pubkey2, &account2);
3139 }
3140
3141 #[test]
3142 fn test_export_snapshot_json_parsed() {
3143 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3144
3145 let pubkey1 = Pubkey::new_unique();
3146 println!("Pubkey1: {}", pubkey1);
3147 let account1 = Account {
3148 lamports: 1_000_000,
3149 data: vec![1, 2, 3, 4],
3150 owner: system_program::id(),
3151 executable: false,
3152 rent_epoch: 0,
3153 };
3154
3155 set_account(&client, &pubkey1, &account1);
3156
3157 let mint_pubkey = Pubkey::new_unique();
3158 println!("Mint Pubkey: {}", mint_pubkey);
3159 let mint_authority = Pubkey::new_unique();
3160
3161 let mut mint_data = [0u8; Mint::LEN];
3162 let mint = Mint {
3163 mint_authority: COption::Some(mint_authority),
3164 supply: 1000,
3165 decimals: 6,
3166 is_initialized: true,
3167 freeze_authority: COption::None,
3168 };
3169 mint.pack_into_slice(&mut mint_data);
3170
3171 let mint_account = Account {
3172 lamports: 1_000_000,
3173 data: mint_data.to_vec(),
3174 owner: spl_token_interface::id(),
3175 executable: false,
3176 rent_epoch: 0,
3177 };
3178
3179 set_account(&client, &mint_pubkey, &mint_account);
3180
3181 let snapshot = client
3182 .rpc
3183 .export_snapshot(
3184 Some(client.context.clone()),
3185 Some(ExportSnapshotConfig {
3186 include_parsed_accounts: Some(true),
3187 filter: None,
3188 scope: ExportSnapshotScope::Network,
3189 }),
3190 )
3191 .expect("Failed to export snapshot")
3192 .value;
3193
3194 verify_snapshot_account(&snapshot, &pubkey1, &account1);
3195 let actual_account1 = snapshot
3196 .get(&pubkey1.to_string())
3197 .expect("Account fixture not found");
3198 assert!(
3199 actual_account1.parsed_data.is_none(),
3200 "Account1 should not have parsed data"
3201 );
3202
3203 verify_snapshot_account(&snapshot, &mint_pubkey, &mint_account);
3204 let mint_snapshot = snapshot
3205 .get(&mint_pubkey.to_string())
3206 .expect("Mint account snapshot not found");
3207 let parsed = mint_snapshot
3208 .parsed_data
3209 .as_ref()
3210 .expect("Parsed data should be present");
3211
3212 assert_eq!(parsed.program, "spl-token");
3213 assert_eq!(parsed.space, Mint::LEN as u64);
3214
3215 let parsed_info = parsed
3216 .parsed
3217 .as_object()
3218 .expect("Parsed data should be an object");
3219 let info = parsed_info
3220 .get("info")
3221 .expect("Parsed data should have info field")
3222 .as_object()
3223 .expect("Info field should be an object");
3224 assert_eq!(
3225 info.get("mintAuthority")
3226 .and_then(|v| v.as_str())
3227 .expect("mintAuthority should be a string"),
3228 mint_authority.to_string()
3229 );
3230 }
3231
3232 #[test]
3233 fn test_export_snapshot_pre_transaction() {
3234 use std::collections::HashMap;
3235
3236 use solana_signature::Signature;
3237 use surfpool_types::{ProfileResult, types::KeyedProfileResult};
3238
3239 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3240
3241 let account1_pubkey = Pubkey::new_unique();
3243 let account1 = Account {
3244 lamports: 1_000_000,
3245 data: vec![1, 2, 3, 4],
3246 owner: system_program::id(),
3247 executable: false,
3248 rent_epoch: 0,
3249 };
3250 set_account(&client, &account1_pubkey, &account1);
3251
3252 let account2_pubkey = Pubkey::new_unique();
3253 let account2 = Account {
3254 lamports: 2_000_000,
3255 data: vec![5, 6, 7, 8],
3256 owner: system_program::id(),
3257 executable: false,
3258 rent_epoch: 0,
3259 };
3260 set_account(&client, &account2_pubkey, &account2);
3261
3262 let account3_pubkey = Pubkey::new_unique();
3263 let account3 = Account {
3264 lamports: 3_000_000,
3265 data: vec![9, 10, 11, 12],
3266 owner: system_program::id(),
3267 executable: false,
3268 rent_epoch: 0,
3269 };
3270 set_account(&client, &account3_pubkey, &account3);
3271
3272 let signature = Signature::new_unique();
3274 let mut pre_execution_capture = BTreeMap::new();
3275 pre_execution_capture.insert(account1_pubkey, Some(account1.clone()));
3276 pre_execution_capture.insert(account2_pubkey, Some(account2.clone()));
3277
3278 let mut post_execution_capture = BTreeMap::new();
3279 let mut modified_account1 = account1.clone();
3280 modified_account1.lamports = 500_000;
3281 post_execution_capture.insert(account1_pubkey, Some(modified_account1.clone()));
3282 let mut modified_account2 = account2.clone();
3283 modified_account2.lamports = 2_500_000;
3284 post_execution_capture.insert(account2_pubkey, Some(modified_account2.clone()));
3285
3286 let profile = ProfileResult {
3287 pre_execution_capture,
3288 post_execution_capture,
3289 compute_units_consumed: 1000,
3290 log_messages: None,
3291 error_message: None,
3292 };
3293
3294 let keyed_profile = KeyedProfileResult::new(
3295 1,
3296 UuidOrSignature::Signature(signature),
3297 None,
3298 profile,
3299 HashMap::new(),
3300 );
3301
3302 client.context.svm_locker.with_svm_writer(|svm| {
3304 svm.executed_transaction_profiles
3305 .store(signature.to_string(), keyed_profile)
3306 .unwrap();
3307 });
3308
3309 let snapshot = client
3311 .rpc
3312 .export_snapshot(
3313 Some(client.context.clone()),
3314 Some(ExportSnapshotConfig {
3315 include_parsed_accounts: Some(false),
3316 filter: None,
3317 scope: ExportSnapshotScope::PreTransaction(signature.to_string()),
3318 }),
3319 )
3320 .expect("Failed to export snapshot")
3321 .value;
3322
3323 assert!(
3325 snapshot.contains_key(&account1_pubkey.to_string()),
3326 "Snapshot should contain account1 (touched by transaction)"
3327 );
3328 assert!(
3329 snapshot.contains_key(&account2_pubkey.to_string()),
3330 "Snapshot should contain account2 (touched by transaction)"
3331 );
3332 assert!(
3333 !snapshot.contains_key(&account3_pubkey.to_string()),
3334 "Snapshot should NOT contain account3 (not touched by transaction)"
3335 );
3336
3337 verify_snapshot_account(&snapshot, &account1_pubkey, &account1);
3339 verify_snapshot_account(&snapshot, &account2_pubkey, &account2);
3340
3341 let snapshot_account1 = snapshot
3343 .get(&account1_pubkey.to_string())
3344 .expect("Account1 should be in snapshot");
3345 assert_eq!(
3346 snapshot_account1.lamports, 1_000_000,
3347 "Account1 should have pre-execution lamports (1M), not post-execution (500K)"
3348 );
3349
3350 let snapshot_account2 = snapshot
3351 .get(&account2_pubkey.to_string())
3352 .expect("Account2 should be in snapshot");
3353 assert_eq!(
3354 snapshot_account2.lamports, 2_000_000,
3355 "Account2 should have pre-execution lamports (2M), not post-execution (2.5M)"
3356 );
3357
3358 println!(
3362 "Snapshot contains {} accounts (expected at least 2)",
3363 snapshot.len()
3364 );
3365 }
3366
3367 #[test]
3368 fn test_export_snapshot_filtering() {
3369 let system_account_pubkey = Pubkey::new_unique();
3370 println!("System Account Pubkey: {}", system_account_pubkey);
3371 let excluded_system_account_pubkey = Pubkey::new_unique();
3372 println!(
3373 "Excluded System Account Pubkey: {}",
3374 excluded_system_account_pubkey
3375 );
3376 let program_account_pubkey = Pubkey::new_unique();
3377 println!("Program Account Pubkey: {}", program_account_pubkey);
3378 let included_program_account_pubkey = Pubkey::new_unique();
3379 println!(
3380 "Included Program Account Pubkey: {}",
3381 included_program_account_pubkey
3382 );
3383
3384 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3385
3386 let system_account = Account {
3387 lamports: 1_000_000,
3388 data: vec![1, 2, 3, 4],
3389 owner: system_program::id(),
3390 executable: false,
3391 rent_epoch: 0,
3392 };
3393 set_account(&client, &system_account_pubkey, &system_account);
3394 set_account(&client, &excluded_system_account_pubkey, &system_account);
3395
3396 let program_account = Account {
3397 lamports: 2_000_000,
3398 data: vec![5, 6, 7, 8, 9],
3399 owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
3400 executable: false,
3401 rent_epoch: 0,
3402 };
3403 set_account(&client, &program_account_pubkey, &program_account);
3404 set_account(&client, &included_program_account_pubkey, &program_account);
3405
3406 let snapshot = client
3407 .rpc
3408 .export_snapshot(Some(client.context.clone()), None)
3409 .expect("Failed to export snapshot")
3410 .value;
3411 assert!(
3412 !snapshot.contains_key(&program_account_pubkey.to_string()),
3413 "Program account should be excluded by default"
3414 );
3415 assert!(
3416 !snapshot.contains_key(&included_program_account_pubkey.to_string()),
3417 "Program account should be excluded by default"
3418 );
3419 let snapshot = client
3420 .rpc
3421 .export_snapshot(
3422 Some(client.context.clone()),
3423 Some(ExportSnapshotConfig {
3424 filter: Some(ExportSnapshotFilter {
3425 include_accounts: Some(vec![included_program_account_pubkey.to_string()]),
3426 ..Default::default()
3427 }),
3428 ..Default::default()
3429 }),
3430 )
3431 .expect("Failed to export snapshot")
3432 .value;
3433 assert!(
3434 !snapshot.contains_key(&program_account_pubkey.to_string()),
3435 "Program account should be excluded by default"
3436 );
3437 assert!(
3438 snapshot.contains_key(&included_program_account_pubkey.to_string()),
3439 "Program account should be included when explicitly listed"
3440 );
3441
3442 let snapshot = client
3443 .rpc
3444 .export_snapshot(
3445 Some(client.context.clone()),
3446 Some(ExportSnapshotConfig {
3447 filter: Some(ExportSnapshotFilter {
3448 include_program_accounts: Some(true),
3449 exclude_accounts: Some(vec![excluded_system_account_pubkey.to_string()]),
3450 ..Default::default()
3451 }),
3452 ..Default::default()
3453 }),
3454 )
3455 .expect("Failed to export snapshot")
3456 .value;
3457
3458 assert!(
3459 snapshot.contains_key(&program_account_pubkey.to_string()),
3460 "Program account should be included when filter is set"
3461 );
3462 assert!(
3463 snapshot.contains_key(&included_program_account_pubkey.to_string()),
3464 "Included program account should be present"
3465 );
3466 assert!(
3467 snapshot.contains_key(&system_account_pubkey.to_string()),
3468 "System account should be present"
3469 );
3470 assert!(
3471 !snapshot.contains_key(&excluded_system_account_pubkey.to_string()),
3472 "Excluded system account should not be present"
3473 );
3474 }
3475
3476 #[tokio::test(flavor = "multi_thread")]
3477 async fn test_write_program_creates_accounts_automatically() {
3478 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3480 let program_id = Keypair::new();
3481
3482 let program_data_address =
3484 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3485
3486 let program_account_before = client.context.svm_locker.with_svm_reader(|svm_reader| {
3487 svm_reader.inner.get_account(&program_id.pubkey()).unwrap()
3488 });
3489 assert!(
3490 program_account_before.is_none(),
3491 "Program account should not exist initially"
3492 );
3493
3494 let program_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
3496 let result = client
3497 .rpc
3498 .write_program(
3499 Some(client.context.clone()),
3500 program_id.pubkey().to_string(),
3501 hex::encode(&program_data),
3502 0,
3503 None,
3504 )
3505 .await;
3506
3507 assert!(
3508 result.is_ok(),
3509 "Failed to write program: {:?}",
3510 result.err()
3511 );
3512
3513 let program_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3515 svm_reader.inner.get_account(&program_id.pubkey()).unwrap()
3516 });
3517 assert!(
3518 program_account.is_some(),
3519 "Program account should be created"
3520 );
3521
3522 let program_account = program_account.unwrap();
3523 assert_eq!(
3524 program_account.owner,
3525 solana_sdk_ids::bpf_loader_upgradeable::id()
3526 );
3527 assert!(
3528 program_account.executable,
3529 "Program account should be executable"
3530 );
3531
3532 let program_data_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3534 svm_reader.inner.get_account(&program_data_address).unwrap()
3535 });
3536 assert!(
3537 program_data_account.is_some(),
3538 "Program data account should be created"
3539 );
3540
3541 let program_data_account = program_data_account.unwrap();
3542 assert_eq!(
3543 program_data_account.owner,
3544 solana_sdk_ids::bpf_loader_upgradeable::id()
3545 );
3546 assert!(
3547 !program_data_account.executable,
3548 "Program data account should not be executable"
3549 );
3550
3551 println!("✅ Both accounts created successfully");
3552 }
3553
3554 #[tokio::test(flavor = "multi_thread")]
3555 async fn test_write_program_single_chunk_small() {
3556 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3558 let program_id = Keypair::new();
3559
3560 let program_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
3561 let data_hex = hex::encode(&program_data);
3562
3563 let result = client
3564 .rpc
3565 .write_program(
3566 Some(client.context.clone()),
3567 program_id.pubkey().to_string(),
3568 data_hex,
3569 0,
3570 None,
3571 )
3572 .await;
3573
3574 assert!(
3575 result.is_ok(),
3576 "Failed to write program: {:?}",
3577 result.err()
3578 );
3579
3580 let program_data_address =
3582 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3583 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3584 svm_reader
3585 .inner
3586 .get_account(&program_data_address)
3587 .unwrap()
3588 .unwrap()
3589 });
3590
3591 let metadata_size =
3592 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3593 );
3594 let written_data = &account.data[metadata_size..metadata_size + program_data.len()];
3595 assert_eq!(
3596 written_data, &program_data,
3597 "Written data should match input"
3598 );
3599
3600 println!("✅ Small program written correctly");
3601 }
3602
3603 #[tokio::test(flavor = "multi_thread")]
3604 async fn test_write_program_single_chunk_large() {
3605 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3607 let program_id = Keypair::new();
3608
3609 let program_data = vec![0xAB; 1024 * 1024]; let data_hex = hex::encode(&program_data);
3611
3612 let result = client
3613 .rpc
3614 .write_program(
3615 Some(client.context.clone()),
3616 program_id.pubkey().to_string(),
3617 data_hex,
3618 0,
3619 None,
3620 )
3621 .await;
3622
3623 assert!(
3624 result.is_ok(),
3625 "Failed to write large program: {:?}",
3626 result.err()
3627 );
3628
3629 let program_data_address =
3631 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3632 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3633 svm_reader
3634 .inner
3635 .get_account(&program_data_address)
3636 .unwrap()
3637 .unwrap()
3638 });
3639
3640 let metadata_size =
3641 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3642 );
3643 assert_eq!(
3644 account.data.len(),
3645 metadata_size + program_data.len(),
3646 "Account should have correct size"
3647 );
3648
3649 println!("✅ Large program (1MB) written successfully");
3650 }
3651
3652 #[tokio::test(flavor = "multi_thread")]
3653 async fn test_write_program_multiple_sequential_chunks() {
3654 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3656 let program_id = Keypair::new();
3657
3658 let chunks = vec![
3659 (vec![0x01; 1024], 0), (vec![0x02; 1024], 1024), (vec![0x03; 1024], 2048), (vec![0x04; 512], 3072), ];
3664
3665 for (i, (chunk_data, offset)) in chunks.iter().enumerate() {
3666 let result = client
3667 .rpc
3668 .write_program(
3669 Some(client.context.clone()),
3670 program_id.pubkey().to_string(),
3671 hex::encode(chunk_data),
3672 *offset,
3673 None,
3674 )
3675 .await;
3676
3677 assert!(
3678 result.is_ok(),
3679 "Failed to write chunk {} at offset {}: {:?}",
3680 i,
3681 offset,
3682 result.err()
3683 );
3684 }
3685
3686 let program_data_address =
3688 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3689 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3690 svm_reader
3691 .inner
3692 .get_account(&program_data_address)
3693 .unwrap()
3694 .unwrap()
3695 });
3696
3697 let metadata_size =
3698 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3699 );
3700
3701 for (chunk_data, offset) in chunks {
3703 let start = metadata_size + offset as usize;
3704 let end = start + chunk_data.len();
3705 let written = &account.data[start..end];
3706 assert_eq!(
3707 written,
3708 &chunk_data[..],
3709 "Chunk at offset {} should match",
3710 offset
3711 );
3712 }
3713
3714 println!("✅ Multiple sequential chunks written correctly");
3715 }
3716
3717 #[tokio::test(flavor = "multi_thread")]
3718 async fn test_write_program_non_sequential_chunks() {
3719 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3721 let program_id = Keypair::new();
3722
3723 let chunks = vec![
3724 (vec![0x03; 512], 2048), (vec![0x01; 1024], 0), (vec![0x02; 1024], 1024), ];
3728
3729 for (chunk_data, offset) in chunks.iter() {
3730 let result = client
3731 .rpc
3732 .write_program(
3733 Some(client.context.clone()),
3734 program_id.pubkey().to_string(),
3735 hex::encode(chunk_data),
3736 *offset,
3737 None,
3738 )
3739 .await;
3740
3741 assert!(
3742 result.is_ok(),
3743 "Failed at offset {}: {:?}",
3744 offset,
3745 result.err()
3746 );
3747 }
3748
3749 let program_data_address =
3751 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3752 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3753 svm_reader
3754 .inner
3755 .get_account(&program_data_address)
3756 .unwrap()
3757 .unwrap()
3758 });
3759
3760 let metadata_size =
3761 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3762 );
3763
3764 assert_eq!(
3766 &account.data[metadata_size..metadata_size + 1024],
3767 &vec![0x01; 1024][..]
3768 );
3769 assert_eq!(
3771 &account.data[metadata_size + 1024..metadata_size + 2048],
3772 &vec![0x02; 1024][..]
3773 );
3774 assert_eq!(
3776 &account.data[metadata_size + 2048..metadata_size + 2560],
3777 &vec![0x03; 512][..]
3778 );
3779
3780 println!("✅ Non-sequential chunks written correctly");
3781 }
3782
3783 #[tokio::test(flavor = "multi_thread")]
3784 async fn test_write_program_overlapping_writes() {
3785 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3787 let program_id = Keypair::new();
3788
3789 let initial_data = vec![0xFF; 1024];
3791 client
3792 .rpc
3793 .write_program(
3794 Some(client.context.clone()),
3795 program_id.pubkey().to_string(),
3796 hex::encode(&initial_data),
3797 0,
3798 None,
3799 )
3800 .await
3801 .unwrap();
3802
3803 let overwrite_data = vec![0xAA; 512];
3805 client
3806 .rpc
3807 .write_program(
3808 Some(client.context.clone()),
3809 program_id.pubkey().to_string(),
3810 hex::encode(&overwrite_data),
3811 256, None,
3813 )
3814 .await
3815 .unwrap();
3816
3817 let program_data_address =
3819 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3820 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3821 svm_reader
3822 .inner
3823 .get_account(&program_data_address)
3824 .unwrap()
3825 .unwrap()
3826 });
3827
3828 let metadata_size =
3829 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3830 );
3831
3832 assert_eq!(
3834 &account.data[metadata_size..metadata_size + 256],
3835 &vec![0xFF; 256][..]
3836 );
3837 assert_eq!(
3839 &account.data[metadata_size + 256..metadata_size + 768],
3840 &vec![0xAA; 512][..]
3841 );
3842 assert_eq!(
3844 &account.data[metadata_size + 768..metadata_size + 1024],
3845 &vec![0xFF; 256][..]
3846 );
3847
3848 println!("✅ Overlapping writes handled correctly");
3849 }
3850
3851 #[tokio::test(flavor = "multi_thread")]
3852 async fn test_write_program_zero_offset() {
3853 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3855 let program_id = Keypair::new();
3856
3857 let data = vec![0x42; 128];
3858 let result = client
3859 .rpc
3860 .write_program(
3861 Some(client.context.clone()),
3862 program_id.pubkey().to_string(),
3863 hex::encode(&data),
3864 0,
3865 None,
3866 )
3867 .await;
3868
3869 assert!(result.is_ok());
3870
3871 let program_data_address =
3872 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3873 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3874 svm_reader
3875 .inner
3876 .get_account(&program_data_address)
3877 .unwrap()
3878 .unwrap()
3879 });
3880
3881 let metadata_size =
3882 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3883 );
3884 assert_eq!(&account.data[metadata_size..metadata_size + 128], &data[..]);
3885
3886 println!("✅ Write at offset 0 works correctly");
3887 }
3888
3889 #[tokio::test(flavor = "multi_thread")]
3890 async fn test_write_program_large_offset() {
3891 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3893 let program_id = Keypair::new();
3894
3895 let large_offset = 1024 * 1024; let data = vec![0x99; 256];
3897
3898 let result = client
3899 .rpc
3900 .write_program(
3901 Some(client.context.clone()),
3902 program_id.pubkey().to_string(),
3903 hex::encode(&data),
3904 large_offset,
3905 None,
3906 )
3907 .await;
3908
3909 assert!(result.is_ok(), "Should handle large offset");
3910
3911 let program_data_address =
3912 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3913 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3914 svm_reader
3915 .inner
3916 .get_account(&program_data_address)
3917 .unwrap()
3918 .unwrap()
3919 });
3920
3921 let metadata_size =
3922 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3923 );
3924 let expected_size = metadata_size + large_offset as usize + data.len();
3925 assert_eq!(
3926 account.data.len(),
3927 expected_size,
3928 "Account should expand to fit data"
3929 );
3930
3931 let start = metadata_size + large_offset as usize;
3933 assert_eq!(&account.data[start..start + 256], &data[..]);
3934
3935 println!("✅ Large offset handled with account expansion");
3936 }
3937
3938 #[tokio::test(flavor = "multi_thread")]
3939 async fn test_write_program_empty_data() {
3940 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3942 let program_id = Keypair::new();
3943
3944 let result = client
3945 .rpc
3946 .write_program(
3947 Some(client.context.clone()),
3948 program_id.pubkey().to_string(),
3949 hex::encode(&[]),
3950 0,
3951 None,
3952 )
3953 .await;
3954
3955 assert!(result.is_ok(), "Should handle empty data");
3956
3957 println!("✅ Empty data handled correctly");
3958 }
3959
3960 #[tokio::test(flavor = "multi_thread")]
3961 async fn test_write_program_single_byte() {
3962 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3964 let program_id = Keypair::new();
3965
3966 let data = vec![0x42];
3967 let result = client
3968 .rpc
3969 .write_program(
3970 Some(client.context.clone()),
3971 program_id.pubkey().to_string(),
3972 hex::encode(&data),
3973 0,
3974 None,
3975 )
3976 .await;
3977
3978 assert!(result.is_ok());
3979
3980 let program_data_address =
3981 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3982 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3983 svm_reader
3984 .inner
3985 .get_account(&program_data_address)
3986 .unwrap()
3987 .unwrap()
3988 });
3989
3990 let metadata_size =
3991 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3992 );
3993 assert_eq!(account.data[metadata_size], 0x42);
3994
3995 println!("✅ Single byte write works");
3996 }
3997
3998 #[tokio::test(flavor = "multi_thread")]
3999 async fn test_write_program_invalid_program_id() {
4000 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4002
4003 let result = client
4004 .rpc
4005 .write_program(
4006 Some(client.context.clone()),
4007 "not_a_valid_pubkey".to_string(),
4008 "deadbeef".to_string(),
4009 0,
4010 None,
4011 )
4012 .await;
4013
4014 assert!(result.is_err(), "Should fail with invalid program ID");
4015 assert!(
4016 result.unwrap_err().to_string().contains("Invalid pubkey"),
4017 "Error should mention invalid pubkey"
4018 );
4019
4020 println!("✅ Invalid program ID rejected");
4021 }
4022
4023 #[tokio::test(flavor = "multi_thread")]
4024 async fn test_write_program_invalid_hex_data() {
4025 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4027 let program_id = Keypair::new();
4028
4029 let invalid_hex_strings = vec![
4030 "not_hex_at_all",
4031 "GHIJKLMN",
4032 "0x123", "12 34", ];
4035
4036 for invalid_hex in invalid_hex_strings {
4037 let result = client
4038 .rpc
4039 .write_program(
4040 Some(client.context.clone()),
4041 program_id.pubkey().to_string(),
4042 invalid_hex.to_string(),
4043 0,
4044 None,
4045 )
4046 .await;
4047
4048 assert!(
4049 result.is_err(),
4050 "Should fail with invalid hex: {}",
4051 invalid_hex
4052 );
4053 assert!(
4054 result.unwrap_err().to_string().contains("Invalid hex"),
4055 "Error should mention invalid hex"
4056 );
4057 }
4058
4059 println!("✅ Invalid hex data rejected");
4060 }
4061
4062 #[tokio::test(flavor = "multi_thread")]
4063 async fn test_write_program_rent_exemption() {
4064 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4066 let program_id = Keypair::new();
4067
4068 let small_data = vec![0x01; 128];
4070 client
4071 .rpc
4072 .write_program(
4073 Some(client.context.clone()),
4074 program_id.pubkey().to_string(),
4075 hex::encode(&small_data),
4076 0,
4077 None,
4078 )
4079 .await
4080 .unwrap();
4081
4082 let program_data_address =
4083 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4084
4085 let initial_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4086 svm_reader
4087 .inner
4088 .get_account(&program_data_address)
4089 .unwrap()
4090 .unwrap()
4091 .lamports
4092 });
4093
4094 let large_data = vec![0x02; 1024];
4096 client
4097 .rpc
4098 .write_program(
4099 Some(client.context.clone()),
4100 program_id.pubkey().to_string(),
4101 hex::encode(&large_data),
4102 10240, None,
4104 )
4105 .await
4106 .unwrap();
4107
4108 let final_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4109 svm_reader
4110 .inner
4111 .get_account(&program_data_address)
4112 .unwrap()
4113 .unwrap()
4114 .lamports
4115 });
4116
4117 assert!(
4118 final_lamports > initial_lamports,
4119 "Lamports should increase to maintain rent exemption"
4120 );
4121
4122 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4124 svm_reader
4125 .inner
4126 .get_account(&program_data_address)
4127 .unwrap()
4128 .unwrap()
4129 });
4130
4131 let required_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4132 svm_reader
4133 .inner
4134 .minimum_balance_for_rent_exemption(account.data.len())
4135 });
4136
4137 assert_eq!(
4138 account.lamports, required_lamports,
4139 "Account should have exact rent-exempt lamports"
4140 );
4141
4142 println!("✅ Rent exemption maintained during expansion");
4143 }
4144
4145 #[tokio::test(flavor = "multi_thread")]
4146 async fn test_write_program_account_ownership() {
4147 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4149 let program_id = Keypair::new();
4150 let authority = Keypair::new();
4151
4152 let data = vec![0xAB; 64];
4153 client
4154 .rpc
4155 .write_program(
4156 Some(client.context.clone()),
4157 program_id.pubkey().to_string(),
4158 hex::encode(&data),
4159 0,
4160 Some(authority.pubkey().to_string()),
4161 )
4162 .await
4163 .unwrap();
4164
4165 let program_data_address =
4166 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4167
4168 let program_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4170 svm_reader
4171 .inner
4172 .get_account(&program_id.pubkey())
4173 .unwrap()
4174 .unwrap()
4175 });
4176 assert_eq!(
4177 program_account.owner,
4178 solana_sdk_ids::bpf_loader_upgradeable::id(),
4179 "Program account should be owned by upgradeable loader"
4180 );
4181 assert!(
4182 program_account.executable,
4183 "Program account should be executable"
4184 );
4185
4186 let program_data_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4188 svm_reader
4189 .inner
4190 .get_account(&program_data_address)
4191 .unwrap()
4192 .unwrap()
4193 });
4194 assert_eq!(
4195 program_data_account.owner,
4196 solana_sdk_ids::bpf_loader_upgradeable::id(),
4197 "Program data account should be owned by upgradeable loader"
4198 );
4199 assert!(
4200 !program_data_account.executable,
4201 "Program data account should not be executable"
4202 );
4203
4204 let Ok(solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4207 slot: _,
4208 upgrade_authority_address,
4209 }) = bincode::deserialize::<solana_loader_v3_interface::state::UpgradeableLoaderState>(
4210 &program_data_account.data,
4211 )
4212 else {
4213 panic!("Program data account has incorrect state");
4214 };
4215
4216 assert_eq!(
4217 upgrade_authority_address,
4218 Some(authority.pubkey()),
4219 "Upgrade authority should match"
4220 );
4221
4222 println!("✅ Account ownership is correct");
4223 }
4224
4225 #[tokio::test(flavor = "multi_thread")]
4226 async fn test_write_program_metadata_preservation() {
4227 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4229 let program_id = Keypair::new();
4230
4231 client
4233 .rpc
4234 .write_program(
4235 Some(client.context.clone()),
4236 program_id.pubkey().to_string(),
4237 hex::encode(&vec![0x01; 128]),
4238 0,
4239 None,
4240 )
4241 .await
4242 .unwrap();
4243
4244 let program_data_address =
4245 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4246
4247 let initial_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4249 svm_reader
4250 .inner
4251 .get_account(&program_data_address)
4252 .unwrap()
4253 .unwrap()
4254 });
4255
4256 let metadata_size =
4257 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4258 );
4259 let initial_metadata = initial_account.data[..metadata_size].to_vec();
4260
4261 client
4263 .rpc
4264 .write_program(
4265 Some(client.context.clone()),
4266 program_id.pubkey().to_string(),
4267 hex::encode(&vec![0x02; 256]),
4268 128,
4269 None,
4270 )
4271 .await
4272 .unwrap();
4273
4274 let final_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4276 svm_reader
4277 .inner
4278 .get_account(&program_data_address)
4279 .unwrap()
4280 .unwrap()
4281 });
4282
4283 let final_metadata = final_account.data[..metadata_size].to_vec();
4284 assert_eq!(
4285 initial_metadata, final_metadata,
4286 "Metadata should be preserved"
4287 );
4288
4289 println!("✅ Metadata preserved across writes");
4290 }
4291
4292 #[tokio::test(flavor = "multi_thread")]
4293 async fn test_write_program_idempotent() {
4294 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4296 let program_id = Keypair::new();
4297
4298 let data = vec![0x55; 512];
4299 let offset = 100;
4300
4301 client
4303 .rpc
4304 .write_program(
4305 Some(client.context.clone()),
4306 program_id.pubkey().to_string(),
4307 hex::encode(&data),
4308 offset,
4309 None,
4310 )
4311 .await
4312 .unwrap();
4313
4314 let program_data_address =
4315 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4316
4317 let first_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4318 svm_reader
4319 .inner
4320 .get_account(&program_data_address)
4321 .unwrap()
4322 .unwrap()
4323 });
4324
4325 client
4327 .rpc
4328 .write_program(
4329 Some(client.context.clone()),
4330 program_id.pubkey().to_string(),
4331 hex::encode(&data),
4332 offset,
4333 None,
4334 )
4335 .await
4336 .unwrap();
4337
4338 let second_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4339 svm_reader
4340 .inner
4341 .get_account(&program_data_address)
4342 .unwrap()
4343 .unwrap()
4344 });
4345
4346 assert_eq!(
4347 first_account.data, second_account.data,
4348 "Data should be identical"
4349 );
4350 assert_eq!(
4351 first_account.lamports, second_account.lamports,
4352 "Lamports should be identical"
4353 );
4354
4355 println!("✅ Writes are idempotent");
4356 }
4357
4358 #[tokio::test(flavor = "multi_thread")]
4359 async fn test_write_program_context_slot() {
4360 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4362 let program_id = Keypair::new();
4363
4364 let result = client
4365 .rpc
4366 .write_program(
4367 Some(client.context.clone()),
4368 program_id.pubkey().to_string(),
4369 hex::encode(&vec![0x42; 64]),
4370 0,
4371 None,
4372 )
4373 .await
4374 .unwrap();
4375
4376 assert!(result.context.slot > 0, "Context slot should be valid");
4377
4378 println!("✅ Response context is valid");
4379 }
4380
4381 #[tokio::test(flavor = "multi_thread")]
4382 async fn test_write_program_small_no_minimum_program_artifacts() {
4383 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4386 let program_id = Keypair::new();
4387
4388 let program_data = vec![0xAB; 100];
4389 let result = client
4390 .rpc
4391 .write_program(
4392 Some(client.context.clone()),
4393 program_id.pubkey().to_string(),
4394 hex::encode(&program_data),
4395 0,
4396 None,
4397 )
4398 .await;
4399
4400 assert!(
4401 result.is_ok(),
4402 "Failed to write program: {:?}",
4403 result.err()
4404 );
4405
4406 let program_data_address =
4407 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4408 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4409 svm_reader
4410 .inner
4411 .get_account(&program_data_address)
4412 .unwrap()
4413 .unwrap()
4414 });
4415
4416 let metadata_size =
4417 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4418 );
4419
4420 assert_eq!(
4422 account.data.len(),
4423 metadata_size + 100,
4424 "Account data length should be metadata_size ({}) + 100 = {}, but was {}",
4425 metadata_size,
4426 metadata_size + 100,
4427 account.data.len()
4428 );
4429
4430 let written_data = &account.data[metadata_size..];
4432 assert_eq!(
4433 written_data, &program_data,
4434 "Written data should match exactly"
4435 );
4436
4437 assert!(
4439 account.data[metadata_size + 100..].is_empty(),
4440 "There should be no trailing bytes beyond the written data"
4441 );
4442
4443 println!("✅ Small program has no minimum_program.so artifacts");
4444 }
4445
4446 #[tokio::test(flavor = "multi_thread")]
4447 async fn test_write_program_exact_account_size() {
4448 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4451
4452 let metadata_size =
4453 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4454 );
4455
4456 for data_len in [1usize, 100, 3311, 3312, 3313, 5000] {
4457 let program_id = Keypair::new();
4458 let program_data: Vec<u8> = (0..data_len).map(|i| (i % 256) as u8).collect();
4459
4460 let result = client
4461 .rpc
4462 .write_program(
4463 Some(client.context.clone()),
4464 program_id.pubkey().to_string(),
4465 hex::encode(&program_data),
4466 0,
4467 None,
4468 )
4469 .await;
4470
4471 assert!(
4472 result.is_ok(),
4473 "Failed to write program of size {}: {:?}",
4474 data_len,
4475 result.err()
4476 );
4477
4478 let program_data_address =
4479 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4480 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4481 svm_reader
4482 .inner
4483 .get_account(&program_data_address)
4484 .unwrap()
4485 .unwrap()
4486 });
4487
4488 assert_eq!(
4489 account.data.len(),
4490 metadata_size + data_len,
4491 "For data_len={}, account data length should be {} but was {}",
4492 data_len,
4493 metadata_size + data_len,
4494 account.data.len()
4495 );
4496
4497 let written_data = &account.data[metadata_size..metadata_size + data_len];
4498 assert_eq!(
4499 written_data, &program_data,
4500 "For data_len={}, written content should match exactly",
4501 data_len
4502 );
4503 }
4504
4505 println!("✅ All program sizes produce exact account sizes");
4506 }
4507
4508 #[tokio::test(flavor = "multi_thread")]
4509 async fn test_write_program_execution_uses_written_bytes_not_noop() {
4510 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4513 let program_id = Keypair::new();
4514
4515 let mut error_program_elf = crate::surfnet::noop_program::NOOP_PROGRAM_ELF.to_vec();
4518 error_program_elf[124] = 0x01;
4521
4522 let result = client
4524 .rpc
4525 .write_program(
4526 Some(client.context.clone()),
4527 program_id.pubkey().to_string(),
4528 hex::encode(&error_program_elf),
4529 0,
4530 None,
4531 )
4532 .await;
4533 assert!(
4534 result.is_ok(),
4535 "Failed to write program: {:?}",
4536 result.err()
4537 );
4538
4539 let payer = Keypair::new();
4541 client
4542 .context
4543 .svm_locker
4544 .airdrop(&payer.pubkey(), 1_000_000_000)
4545 .unwrap()
4546 .unwrap();
4547
4548 let recent_blockhash = client
4550 .context
4551 .svm_locker
4552 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
4553 let invoke_ix = solana_instruction::Instruction {
4555 program_id: program_id.pubkey(),
4556 accounts: vec![],
4557 data: vec![],
4558 };
4559 let message = solana_message::Message::new_with_blockhash(
4560 &[invoke_ix],
4561 Some(&payer.pubkey()),
4562 &recent_blockhash,
4563 );
4564 let tx = VersionedTransaction::try_new(
4565 solana_message::VersionedMessage::Legacy(message),
4566 &[&payer],
4567 )
4568 .unwrap();
4569
4570 let sim_result = client.context.svm_locker.simulate_transaction(tx, false);
4572
4573 assert!(
4576 sim_result.is_err(),
4577 "Transaction should fail because the written program returns error (r0=1). \
4578 If it succeeded, the noop placeholder is still being executed instead of \
4579 the written program bytes."
4580 );
4581 }
4582
4583 #[test]
4584 fn test_stream_accounts_registers_multiple() {
4585 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4586
4587 let pubkey1 = Pubkey::new_unique();
4588 let pubkey2 = Pubkey::new_unique();
4589 let pubkey3 = Pubkey::new_unique();
4590
4591 let entries = vec![
4592 StreamAccountsEntry {
4593 pubkey: pubkey1.to_string(),
4594 include_owned_accounts: Some(true),
4595 },
4596 StreamAccountsEntry {
4597 pubkey: pubkey2.to_string(),
4598 include_owned_accounts: Some(false),
4599 },
4600 StreamAccountsEntry {
4601 pubkey: pubkey3.to_string(),
4602 include_owned_accounts: None,
4603 },
4604 ];
4605
4606 let result = client
4607 .rpc
4608 .stream_accounts(Some(client.context.clone()), entries);
4609 assert!(result.is_ok(), "stream_accounts should succeed");
4610
4611 let streamed = client
4613 .rpc
4614 .get_streamed_accounts(Some(client.context.clone()))
4615 .expect("Failed to get streamed accounts")
4616 .value;
4617
4618 let accounts = serde_json::to_value(&streamed).unwrap();
4619 let accounts_arr = accounts["accounts"].as_array().unwrap();
4620 assert_eq!(accounts_arr.len(), 3, "Should have 3 streamed accounts");
4621
4622 let find = |pk: &str| {
4624 accounts_arr
4625 .iter()
4626 .find(|a| a["pubkey"].as_str().unwrap() == pk)
4627 .unwrap()
4628 .clone()
4629 };
4630 assert_eq!(find(&pubkey1.to_string())["includeOwnedAccounts"], true);
4631 assert_eq!(find(&pubkey2.to_string())["includeOwnedAccounts"], false);
4632 assert_eq!(find(&pubkey3.to_string())["includeOwnedAccounts"], false);
4633 }
4634
4635 #[test]
4636 fn test_stream_accounts_empty_list() {
4637 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4638
4639 let result = client
4640 .rpc
4641 .stream_accounts(Some(client.context.clone()), vec![]);
4642 assert!(
4643 result.is_ok(),
4644 "stream_accounts with empty list should succeed"
4645 );
4646
4647 let streamed = client
4648 .rpc
4649 .get_streamed_accounts(Some(client.context.clone()))
4650 .expect("Failed to get streamed accounts")
4651 .value;
4652
4653 let accounts = serde_json::to_value(&streamed).unwrap();
4654 let accounts_arr = accounts["accounts"].as_array().unwrap();
4655 assert_eq!(
4656 accounts_arr.len(),
4657 0,
4658 "Should have no streamed accounts after empty call"
4659 );
4660 }
4661
4662 #[test]
4663 fn test_stream_accounts_invalid_pubkey() {
4664 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4665
4666 let entries = vec![StreamAccountsEntry {
4667 pubkey: "not-a-valid-pubkey".to_string(),
4668 include_owned_accounts: None,
4669 }];
4670
4671 let result = client
4672 .rpc
4673 .stream_accounts(Some(client.context.clone()), entries);
4674 assert!(
4675 result.is_err(),
4676 "stream_accounts with invalid pubkey should fail"
4677 );
4678 }
4679}