1use std::{
2 collections::BTreeMap,
3 sync::{Arc, RwLock},
4};
5
6use base64::{Engine as _, engine::general_purpose::STANDARD};
7use jsonrpc_core::{BoxFuture, Error, Result, futures::future};
8use jsonrpc_derive::rpc;
9use solana_account::Account;
10use solana_client::rpc_response::{RpcLogsResponse, RpcResponseContext};
11use solana_clock::Slot;
12use solana_commitment_config::CommitmentConfig;
13use solana_epoch_info::EpochInfo;
14use solana_program_option::COption;
15use solana_rpc_client_api::response::Response as RpcResponse;
16use solana_system_interface::program as system_program;
17use solana_transaction::versioned::VersionedTransaction;
18use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
19use surfpool_types::{
20 AccountSnapshot, CheatcodeControlConfig, CheatcodeFilter, ClockCommand, ExportSnapshotConfig,
21 GetStreamedAccountsResponse, GetSurfnetInfoResponse, Idl, OfflineAccountConfig,
22 ResetAccountConfig, RpcProfileResultConfig, Scenario, SimnetCommand, SimnetEvent,
23 StreamAccountConfig, StreamAccountsEntry, UiKeyedProfileResult,
24 types::{AccountUpdate, SetSomeAccount, SupplyUpdate, TokenAccountUpdate, UuidOrSignature},
25};
26
27use super::{RunloopContext, SurfnetRpcContext};
28use crate::{
29 error::SurfpoolError,
30 rpc::{
31 State,
32 utils::{verify_pubkey, verify_pubkeys},
33 },
34 surfnet::{GetAccountResult, locker::SvmAccessContext},
35 types::{TimeTravelConfig, TokenAccount},
36};
37
38pub trait AccountUpdateExt {
39 fn is_full_account_data_ext(&self) -> bool;
40 fn to_account_ext(&self) -> Result<Option<Account>>;
41 fn apply_ext(self, account: &mut GetAccountResult) -> Result<()>;
42 fn expect_hex_data_ext(&self) -> Result<Vec<u8>>;
43}
44
45impl AccountUpdateExt for AccountUpdate {
46 fn is_full_account_data_ext(&self) -> bool {
47 self.lamports.is_some()
48 && self.owner.is_some()
49 && self.executable.is_some()
50 && self.rent_epoch.is_some()
51 && self.data.is_some()
52 }
53
54 fn to_account_ext(&self) -> Result<Option<Account>> {
55 if self.is_full_account_data_ext() {
56 Ok(Some(Account {
57 lamports: self.lamports.unwrap(),
58 owner: verify_pubkey(&self.owner.clone().unwrap())?,
59 executable: self.executable.unwrap(),
60 rent_epoch: self.rent_epoch.unwrap(),
61 data: self.expect_hex_data_ext()?,
62 }))
63 } else {
64 Ok(None)
65 }
66 }
67
68 fn apply_ext(self, account_result: &mut GetAccountResult) -> Result<()> {
69 account_result.apply_update(|account| {
70 if let Some(lamports) = self.lamports {
71 account.lamports = lamports;
72 }
73 if let Some(owner) = &self.owner {
74 account.owner = verify_pubkey(owner)?;
75 }
76 if let Some(executable) = self.executable {
77 account.executable = executable;
78 }
79 if let Some(rent_epoch) = self.rent_epoch {
80 account.rent_epoch = rent_epoch;
81 }
82 if self.data.is_some() {
83 account.data = self.expect_hex_data_ext()?;
84 }
85 Ok(())
86 })?;
87 Ok(())
88 }
89
90 fn expect_hex_data_ext(&self) -> Result<Vec<u8>> {
91 let data = self.data.as_ref().expect("missing expected data field");
92 hex::decode(data)
93 .map_err(|e| Error::invalid_params(format!("Invalid hex data provided: {}", e)))
94 }
95}
96
97pub trait TokenAccountUpdateExt {
98 fn apply(self, token_account: &mut TokenAccount) -> Result<()>;
99}
100
101impl TokenAccountUpdateExt for TokenAccountUpdate {
102 fn apply(self, token_account: &mut TokenAccount) -> Result<()> {
104 if let Some(amount) = self.amount {
105 token_account.set_amount(amount);
106 }
107 if let Some(delegate) = self.delegate {
108 match delegate {
109 SetSomeAccount::Account(pubkey) => {
110 token_account.set_delegate(COption::Some(verify_pubkey(&pubkey)?));
111 }
112 SetSomeAccount::NoAccount => {
113 token_account.set_delegate(COption::None);
114 }
115 }
116 }
117 if let Some(state) = self.state {
118 token_account.set_state_from_str(state.as_str())?;
119 }
120 if let Some(delegated_amount) = self.delegated_amount {
121 token_account.set_delegated_amount(delegated_amount);
122 }
123 if let Some(close_authority) = self.close_authority {
124 match close_authority {
125 SetSomeAccount::Account(pubkey) => {
126 token_account.set_close_authority(COption::Some(verify_pubkey(&pubkey)?));
127 }
128 SetSomeAccount::NoAccount => {
129 token_account.set_close_authority(COption::None);
130 }
131 }
132 }
133 Ok(())
134 }
135}
136
137#[rpc]
138pub trait SurfnetCheatcodes {
139 type Metadata;
140
141 #[rpc(meta, name = "surfnet_setAccount")]
179 fn set_account(
180 &self,
181 meta: Self::Metadata,
182 pubkey: String,
183 update: AccountUpdate,
184 ) -> BoxFuture<Result<RpcResponse<()>>>;
185
186 #[rpc(meta, name = "surfnet_enableCheatcode")]
222 fn enable_cheatcode(
223 &self,
224 meta: Self::Metadata,
225 cheatcodes_filter: CheatcodeFilter,
226 ) -> Result<RpcResponse<()>>;
227
228 #[rpc(meta, name = "surfnet_disableCheatcode")]
265 fn disable_cheatcode(
266 &self,
267 meta: Self::Metadata,
268 cheatcodes_filter: CheatcodeFilter,
269 lockout: Option<CheatcodeControlConfig>,
270 ) -> Result<RpcResponse<()>>;
271
272 #[rpc(meta, name = "surfnet_setTokenAccount")]
312 fn set_token_account(
313 &self,
314 meta: Self::Metadata,
315 owner: String,
316 mint: String,
317 update: TokenAccountUpdate,
318 token_program: Option<String>,
319 ) -> BoxFuture<Result<RpcResponse<()>>>;
320
321 #[rpc(meta, name = "surfnet_cloneProgramAccount")]
322 fn clone_program_account(
323 &self,
324 meta: Self::Metadata,
325 source_program_id: String,
326 destination_program_id: String,
327 ) -> BoxFuture<Result<RpcResponse<()>>>;
328
329 #[rpc(meta, name = "surfnet_profileTransaction")]
353 fn profile_transaction(
354 &self,
355 meta: Self::Metadata,
356 transaction_data: String, tag: Option<String>, config: Option<RpcProfileResultConfig>,
359 ) -> BoxFuture<Result<RpcResponse<UiKeyedProfileResult>>>;
360
361 #[rpc(meta, name = "surfnet_getProfileResultsByTag")]
369 fn get_profile_results_by_tag(
370 &self,
371 meta: Self::Metadata,
372 tag: String,
373 config: Option<RpcProfileResultConfig>,
374 ) -> Result<RpcResponse<Option<Vec<UiKeyedProfileResult>>>>;
375
376 #[rpc(meta, name = "surfnet_setSupply")]
424 fn set_supply(
425 &self,
426 meta: Self::Metadata,
427 update: SupplyUpdate,
428 ) -> BoxFuture<Result<RpcResponse<()>>>;
429
430 #[rpc(meta, name = "surfnet_setProgramAuthority")]
463 fn set_program_authority(
464 &self,
465 meta: Self::Metadata,
466 program_id_str: String,
467 new_authority_str: Option<String>,
468 ) -> BoxFuture<Result<RpcResponse<()>>>;
469
470 #[rpc(meta, name = "surfnet_getTransactionProfile")]
503 fn get_transaction_profile(
504 &self,
505 meta: Self::Metadata,
506 signature_or_uuid: UuidOrSignature,
507 config: Option<RpcProfileResultConfig>,
508 ) -> Result<RpcResponse<Option<UiKeyedProfileResult>>>;
509
510 #[rpc(meta, name = "surfnet_registerIdl")]
600 fn register_idl(
601 &self,
602 meta: Self::Metadata,
603 idl: Idl,
604 slot: Option<Slot>,
605 ) -> Result<RpcResponse<()>>;
606
607 #[rpc(meta, name = "surfnet_getActiveIdl")]
653 fn get_idl(
654 &self,
655 meta: Self::Metadata,
656 program_id: String,
657 slot: Option<Slot>,
658 ) -> Result<RpcResponse<Option<Idl>>>;
659
660 #[rpc(meta, name = "surfnet_getLocalSignatures")]
682 fn get_local_signatures(
683 &self,
684 meta: Self::Metadata,
685 limit: Option<u64>,
686 ) -> BoxFuture<Result<RpcResponse<Vec<RpcLogsResponse>>>>;
687
688 #[rpc(meta, name = "surfnet_timeTravel")]
725 fn time_travel(
726 &self,
727 meta: Self::Metadata,
728 config: Option<TimeTravelConfig>,
729 ) -> Result<EpochInfo>;
730
731 #[rpc(meta, name = "surfnet_pauseClock")]
763 fn pause_clock(&self, meta: Self::Metadata) -> Result<EpochInfo>;
764
765 #[rpc(meta, name = "surfnet_resumeClock")]
799 fn resume_clock(&self, meta: Self::Metadata) -> Result<EpochInfo>;
800
801 #[rpc(meta, name = "surfnet_resetAccount")]
835 fn reset_account(
836 &self,
837 meta: Self::Metadata,
838 pubkey_str: String,
839 config: Option<ResetAccountConfig>,
840 ) -> Result<RpcResponse<()>>;
841
842 #[rpc(meta, name = "surfnet_resetNetwork")]
871 fn reset_network(&self, meta: Self::Metadata) -> BoxFuture<Result<RpcResponse<()>>>;
872
873 #[rpc(meta, name = "surfnet_offlineAccount")]
908 fn offline_account(
909 &self,
910 meta: Self::Metadata,
911 pubkey_str: String,
912 config: Option<OfflineAccountConfig>,
913 ) -> BoxFuture<Result<RpcResponse<()>>>;
914
915 #[rpc(meta, name = "surfnet_exportSnapshot")]
972 fn export_snapshot(
973 &self,
974 meta: Self::Metadata,
975 config: Option<ExportSnapshotConfig>,
976 ) -> Result<RpcResponse<BTreeMap<String, AccountSnapshot>>>;
977
978 #[rpc(meta, name = "surfnet_streamAccount")]
1014 fn stream_account(
1015 &self,
1016 meta: Self::Metadata,
1017 pubkey_str: String,
1018 config: Option<StreamAccountConfig>,
1019 ) -> Result<RpcResponse<()>>;
1020
1021 #[rpc(meta, name = "surfnet_streamAccounts")]
1061 fn stream_accounts(
1062 &self,
1063 meta: Self::Metadata,
1064 accounts: Vec<StreamAccountsEntry>,
1065 ) -> Result<RpcResponse<()>>;
1066
1067 #[rpc(meta, name = "surfnet_getStreamedAccounts")]
1103 fn get_streamed_accounts(
1104 &self,
1105 meta: Self::Metadata,
1106 ) -> Result<RpcResponse<GetStreamedAccountsResponse>>;
1107
1108 #[rpc(meta, name = "surfnet_getSurfnetInfo")]
1146 fn get_surfnet_info(&self, meta: Self::Metadata)
1147 -> Result<RpcResponse<GetSurfnetInfoResponse>>;
1148
1149 #[rpc(meta, name = "surfnet_writeProgram")]
1191 fn write_program(
1192 &self,
1193 meta: Self::Metadata,
1194 program_id: String,
1195 data: String,
1196 offset: usize,
1197 authority: Option<String>,
1198 ) -> BoxFuture<Result<RpcResponse<()>>>;
1199
1200 #[rpc(meta, name = "surfnet_registerScenario")]
1303 fn register_scenario(
1304 &self,
1305 meta: Self::Metadata,
1306 scenario: Scenario,
1307 slot: Option<Slot>,
1308 ) -> Result<RpcResponse<()>>;
1309}
1310
1311#[derive(Clone)]
1312pub struct SurfnetCheatcodesRpc {
1313 pub registered_methods: Arc<RwLock<Vec<String>>>,
1314}
1315impl SurfnetCheatcodesRpc {
1316 pub fn empty() -> Self {
1317 Self {
1318 registered_methods: Arc::new(RwLock::new(vec![])),
1319 }
1320 }
1321 pub fn is_available_cheatcode(&self, cheatcode: &String) -> bool {
1322 let Ok(methods) = self.registered_methods.read() else {
1323 return false;
1324 };
1325 methods.contains(cheatcode)
1326 }
1327}
1328
1329impl SurfnetCheatcodes for SurfnetCheatcodesRpc {
1330 type Metadata = Option<RunloopContext>;
1331
1332 fn set_account(
1333 &self,
1334 meta: Self::Metadata,
1335 pubkey_str: String,
1336 update: AccountUpdate,
1337 ) -> BoxFuture<Result<RpcResponse<()>>> {
1338 let pubkey = match verify_pubkey(&pubkey_str) {
1339 Ok(res) => res,
1340 Err(e) => return e.into(),
1341 };
1342 let account_update_opt = match update.to_account_ext() {
1343 Err(e) => return Box::pin(future::err(e)),
1344 Ok(res) => res,
1345 };
1346
1347 let SurfnetRpcContext {
1348 svm_locker,
1349 remote_ctx,
1350 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1351 Ok(res) => res,
1352 Err(e) => return e.into(),
1353 };
1354
1355 Box::pin(async move {
1356 let (account_to_set, latest_absolute_slot) = if let Some(account) = account_update_opt {
1357 (
1358 GetAccountResult::FoundAccount(pubkey, account, true),
1359 svm_locker.get_latest_absolute_slot(),
1360 )
1361 } else {
1362 let SvmAccessContext {
1364 slot, inner: mut account_result_to_update,
1365 ..
1366 } = svm_locker.get_account(&remote_ctx, &pubkey, Some(Box::new(move |svm_locker| {
1367
1368 let _ = svm_locker.simnet_events_tx().send(SimnetEvent::info(format!(
1370 "Account {pubkey} not found, creating a new account from default values"
1371 )));
1372 GetAccountResult::FoundAccount(
1373 pubkey,
1374 solana_account::Account {
1375 lamports: 0,
1376 owner: system_program::id(),
1377 executable: false,
1378 rent_epoch: 0,
1379 data: vec![],
1380 },
1381 true, )
1383 }))).await?;
1384
1385 update.apply_ext(&mut account_result_to_update)?;
1386 (account_result_to_update, slot)
1387 };
1388
1389 svm_locker.write_account_update(account_to_set);
1390
1391 Ok(RpcResponse {
1392 context: RpcResponseContext::new(latest_absolute_slot),
1393 value: (),
1394 })
1395 })
1396 }
1397
1398 fn disable_cheatcode(
1399 &self,
1400 meta: Self::Metadata,
1401 cheatcodes_filter: CheatcodeFilter,
1402 control_config: Option<CheatcodeControlConfig>,
1403 ) -> Result<RpcResponse<()>> {
1404 let svm_locker = match meta.get_svm_locker() {
1405 Ok(locker) => locker,
1406 Err(e) => return Err(e.into()),
1407 };
1408
1409 let CheatcodeControlConfig { lockout } = control_config.unwrap_or_default();
1410 let lockout = lockout.unwrap_or_default();
1411
1412 if let Some(runloop_ctx) = meta {
1413 let Ok(mut cheatcode_ctx) = runloop_ctx.cheatcode_config.lock() else {
1414 return Err(jsonrpc_core::Error::internal_error());
1415 };
1416
1417 if lockout {
1418 cheatcode_ctx.lockout();
1419 }
1420
1421 match cheatcodes_filter {
1422 CheatcodeFilter::All(all) => {
1423 if all.ne("all") {
1424 return Err(SurfpoolError::disable_cheatcode(
1425 "Invalid option provided for disabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
1426 )
1427 .into());
1428 }
1429
1430 let Ok(available_cheatcodes) = self.registered_methods.read() else {
1431 return Err(jsonrpc_core::Error::internal_error());
1432 };
1433 cheatcode_ctx.disable_all(lockout, (*available_cheatcodes).clone());
1434 }
1435 CheatcodeFilter::List(cheatcodes) => {
1436 for cheatcode in &cheatcodes {
1438 if !lockout
1439 && (cheatcode.eq("surfnet_enableCheatcode")
1440 || cheatcode.eq("surfnet_disableCheatcode"))
1441 {
1442 return Err(SurfpoolError::disable_cheatcode(
1443 "Cannot disable surfnet_enableCheatcode or surfnet_disableCheatcode rpc method when lockout is not enabled".to_string(),
1444 )
1445 .into());
1446 }
1447 if !self.is_available_cheatcode(cheatcode) {
1448 return Err(SurfpoolError::disable_cheatcode(
1449 "Invalid cheatcode rpc method".to_string(),
1450 )
1451 .into());
1452 }
1453 }
1454 for cheatcode in cheatcodes {
1456 if let Err(e) = cheatcode_ctx.disable_cheatcode(&cheatcode) {
1457 return Err(SurfpoolError::disable_cheatcode(e).into());
1458 }
1459 }
1460 }
1461 }
1462 }
1463
1464 Ok(RpcResponse {
1465 value: (),
1466 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1467 })
1468 }
1469
1470 fn enable_cheatcode(
1471 &self,
1472 meta: Self::Metadata,
1473 cheatcodes_filter: CheatcodeFilter,
1474 ) -> Result<RpcResponse<()>> {
1475 let svm_locker = match meta.get_svm_locker() {
1476 Ok(locker) => locker,
1477 Err(e) => return Err(e.into()),
1478 };
1479 if let Some(runloop_ctx) = meta {
1480 let Ok(mut cheatcode_ctx) = runloop_ctx.cheatcode_config.lock() else {
1481 return Err(jsonrpc_core::Error::internal_error());
1482 };
1483 match cheatcodes_filter {
1484 CheatcodeFilter::All(all) => {
1485 if all.ne("all") {
1486 return Err(SurfpoolError::enable_cheatcode(
1487 "Invalid option provided for enabling all cheatcodes. Try using 'all' or providing an array of specific cheatcodes".to_string(),
1488 )
1489 .into());
1490 }
1491
1492 cheatcode_ctx.filter = CheatcodeFilter::List(vec![]);
1494 }
1495 CheatcodeFilter::List(cheatcodes) => {
1496 for ref cheatcode in cheatcodes {
1497 debug!("enabling cheatcode: {cheatcode}");
1498 if !self.is_available_cheatcode(cheatcode) {
1499 return Err(SurfpoolError::enable_cheatcode(
1500 "Invalid cheatcode rpc method".to_string(),
1501 )
1502 .into());
1503 }
1504
1505 if let Err(e) = cheatcode_ctx.enable_cheatcode(cheatcode) {
1506 return Err(SurfpoolError::enable_cheatcode(e).into());
1507 }
1508 }
1509 }
1510 }
1511 }
1512
1513 Ok(RpcResponse {
1514 value: (),
1515 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
1516 })
1517 }
1518
1519 fn set_token_account(
1520 &self,
1521 meta: Self::Metadata,
1522 owner_str: String,
1523 mint_str: String,
1524 update: TokenAccountUpdate,
1525 some_token_program_str: Option<String>,
1526 ) -> BoxFuture<Result<RpcResponse<()>>> {
1527 let owner = match verify_pubkey(&owner_str) {
1528 Ok(res) => res,
1529 Err(e) => return e.into(),
1530 };
1531
1532 let mint = match verify_pubkey(&mint_str) {
1533 Ok(res) => res,
1534 Err(e) => return e.into(),
1535 };
1536
1537 let is_native_mint = mint == spl_token_interface::native_mint::id();
1538 let token_amount = update.amount.unwrap_or(0);
1539
1540 let token_program_id = match some_token_program_str {
1541 Some(token_program_str) => match verify_pubkey(&token_program_str) {
1542 Ok(res) => res,
1543 Err(e) => return e.into(),
1544 },
1545 None => spl_token_interface::id(),
1546 };
1547
1548 let associated_token_account =
1549 get_associated_token_address_with_program_id(&owner, &mint, &token_program_id);
1550
1551 let SurfnetRpcContext {
1552 svm_locker,
1553 remote_ctx,
1554 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1555 Ok(res) => res,
1556 Err(e) => return e.into(),
1557 };
1558
1559 Box::pin(async move {
1560 let get_mint_result = svm_locker
1561 .get_account(&remote_ctx, &mint, None)
1562 .await?
1563 .inner;
1564 svm_locker.write_account_update(get_mint_result);
1565
1566 let minimum_rent = svm_locker.with_svm_reader(|svm_reader| {
1567 svm_reader.inner.minimum_balance_for_rent_exemption(
1568 TokenAccount::get_packed_len_for_token_program_id(&token_program_id),
1569 )
1570 });
1571
1572 let (rent_exempt_reserve, initial_lamports) = if is_native_mint {
1573 (Some(minimum_rent), minimum_rent + token_amount) } else {
1576 (None, minimum_rent)
1577 };
1578
1579 let SvmAccessContext {
1580 slot,
1581 inner: mut token_account,
1582 ..
1583 } = svm_locker
1584 .get_account(
1585 &remote_ctx,
1586 &associated_token_account,
1587 Some(Box::new(move |_| {
1588 let default =
1589 TokenAccount::new(&token_program_id, owner, mint, rent_exempt_reserve);
1590 let data = default.pack_into_vec();
1591 GetAccountResult::FoundAccount(
1592 associated_token_account,
1593 Account {
1594 lamports: initial_lamports,
1595 owner: token_program_id,
1596 executable: false,
1597 rent_epoch: 0,
1598 data,
1599 },
1600 true, )
1602 })),
1603 )
1604 .await?;
1605
1606 let mut token_account_data = TokenAccount::unpack(token_account.expected_data())
1607 .map_err(|e| {
1608 Error::invalid_params(format!("Failed to unpack token account data: {}", e))
1609 })?;
1610
1611 update.apply(&mut token_account_data)?;
1612
1613 let final_account_bytes = token_account_data.pack_into_vec();
1614 token_account.apply_update(|account| {
1615 account.lamports = initial_lamports;
1617 account.data = final_account_bytes.clone();
1618 Ok(())
1619 })?;
1620 svm_locker.write_account_update(token_account);
1621
1622 Ok(RpcResponse {
1623 context: RpcResponseContext::new(slot),
1624 value: (),
1625 })
1626 })
1627 }
1628
1629 fn clone_program_account(
1640 &self,
1641 meta: Self::Metadata,
1642 source_program_id: String,
1643 destination_program_id: String,
1644 ) -> BoxFuture<Result<RpcResponse<()>>> {
1645 let source_program_id = match verify_pubkey(&source_program_id) {
1646 Ok(res) => res,
1647 Err(e) => return e.into(),
1648 };
1649 let destination_program_id = match verify_pubkey(&destination_program_id) {
1650 Ok(res) => res,
1651 Err(e) => return e.into(),
1652 };
1653
1654 let SurfnetRpcContext {
1655 svm_locker,
1656 remote_ctx,
1657 } = match meta.get_rpc_context(CommitmentConfig::confirmed()) {
1658 Ok(res) => res,
1659 Err(e) => return e.into(),
1660 };
1661
1662 Box::pin(async move {
1663 let SvmAccessContext { slot, .. } = svm_locker
1664 .clone_program_account(&remote_ctx, &source_program_id, &destination_program_id)
1665 .await?;
1666
1667 Ok(RpcResponse {
1668 context: RpcResponseContext::new(slot),
1669 value: (),
1670 })
1671 })
1672 }
1673
1674 fn profile_transaction(
1675 &self,
1676 meta: Self::Metadata,
1677 transaction_data_b64: String,
1678 tag: Option<String>,
1679 config: Option<RpcProfileResultConfig>,
1680 ) -> BoxFuture<Result<RpcResponse<UiKeyedProfileResult>>> {
1681 Box::pin(async move {
1682 let transaction_bytes = STANDARD
1683 .decode(&transaction_data_b64)
1684 .map_err(|e| SurfpoolError::invalid_base64_data("transaction", e))?;
1685 let transaction: VersionedTransaction = bincode::deserialize(&transaction_bytes)
1686 .map_err(|e| SurfpoolError::deserialize_error("transaction", e))?;
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 ) -> Result<RpcResponse<()>> {
2205 let svm_locker = meta.get_svm_locker()?;
2206 svm_locker.register_scenario(scenario, slot)?;
2207 Ok(RpcResponse {
2208 context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()),
2209 value: (),
2210 })
2211 }
2212}
2213
2214#[cfg(test)]
2215mod tests {
2216 use solana_account_decoder::{
2217 UiAccountData, UiAccountEncoding, parse_account_data::ParsedAccount,
2218 };
2219 use solana_keypair::Keypair;
2220 use solana_program_pack::Pack;
2221 use solana_pubkey::Pubkey;
2222 use solana_signer::Signer;
2223 use solana_system_interface::instruction::create_account;
2224 use solana_transaction::{Transaction, versioned::VersionedTransaction};
2225 use spl_associated_token_account_interface::{
2226 address::get_associated_token_address_with_program_id,
2227 instruction::create_associated_token_account,
2228 };
2229 use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
2230 use spl_token_interface::state::Mint;
2231 use surfpool_types::{
2232 ExportSnapshotFilter, ExportSnapshotScope, RpcProfileDepth, UiAccountChange,
2233 UiAccountProfileState,
2234 };
2235
2236 use super::*;
2237 use crate::{rpc::surfnet_cheatcodes::SurfnetCheatcodesRpc, tests::helpers::TestSetup};
2238
2239 #[tokio::test(flavor = "multi_thread")]
2240 async fn test_get_transaction_profile() {
2241 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
2243 let recent_blockhash = client
2244 .context
2245 .svm_locker
2246 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2247
2248 let payer = Keypair::new();
2250
2251 let owner = Keypair::new();
2252
2253 let recipient = Keypair::new();
2255
2256 client
2258 .context
2259 .svm_locker
2260 .airdrop(&payer.pubkey(), 1_000_000_000)
2261 .unwrap()
2262 .unwrap();
2263
2264 client
2266 .context
2267 .svm_locker
2268 .airdrop(&recipient.pubkey(), 1_000_000_000)
2269 .unwrap()
2270 .unwrap();
2271
2272 let mint = Keypair::new();
2274
2275 let mint_space = Mint::LEN;
2277 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
2278 svm_reader
2279 .inner
2280 .minimum_balance_for_rent_exemption(mint_space)
2281 });
2282
2283 let create_account_instruction = create_account(
2285 &payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022_interface::id(), );
2291
2292 let initialize_mint_instruction = initialize_mint2(
2294 &spl_token_2022_interface::id(),
2295 &mint.pubkey(), &payer.pubkey(), Some(&payer.pubkey()), 2, )
2300 .unwrap();
2301
2302 let source_ata = get_associated_token_address_with_program_id(
2304 &owner.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2308
2309 let create_source_ata_instruction = create_associated_token_account(
2311 &payer.pubkey(), &owner.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2316
2317 let destination_ata = get_associated_token_address_with_program_id(
2319 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2323
2324 let create_destination_ata_instruction = create_associated_token_account(
2326 &payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
2331
2332 let amount = 10_000;
2334
2335 let mint_to_instruction = mint_to(
2337 &spl_token_2022_interface::id(),
2338 &mint.pubkey(), &source_ata, &payer.pubkey(), &[&payer.pubkey()], amount, )
2344 .unwrap();
2345
2346 let transaction = Transaction::new_signed_with_payer(
2348 &[
2349 create_account_instruction,
2350 initialize_mint_instruction,
2351 create_source_ata_instruction,
2352 create_destination_ata_instruction,
2353 mint_to_instruction,
2354 ],
2355 Some(&payer.pubkey()),
2356 &[&payer, &mint],
2357 recent_blockhash,
2358 );
2359
2360 let signature = transaction.signatures[0];
2361
2362 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
2363 client
2364 .context
2365 .svm_locker
2366 .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
2367 .await
2368 .unwrap();
2369
2370 {
2372 let ui_profile_result = client
2373 .rpc
2374 .get_transaction_profile(
2375 Some(client.context.clone()),
2376 UuidOrSignature::Signature(signature),
2377 Some(RpcProfileResultConfig {
2378 depth: Some(RpcProfileDepth::Instruction),
2379 ..Default::default()
2380 }),
2381 )
2382 .unwrap()
2383 .value
2384 .expect("missing profile result for processed transaction");
2385
2386 {
2388 let ix_profile = ui_profile_result
2389 .instruction_profiles
2390 .as_ref()
2391 .unwrap()
2392 .first()
2393 .expect("instruction profile should exist");
2394 assert!(
2395 ix_profile.error_message.is_none(),
2396 "Profile should succeed, found error: {}",
2397 ix_profile.error_message.as_ref().unwrap()
2398 );
2399 assert_eq!(ix_profile.compute_units_consumed, 150);
2400 assert!(ix_profile.error_message.is_none());
2401 let account_states = &ix_profile.account_states;
2402
2403 let UiAccountProfileState::Writable(sender_account_change) = account_states
2404 .get(&payer.pubkey())
2405 .expect("Payer account state should be present")
2406 else {
2407 panic!("Expected account state to be Writable");
2408 };
2409
2410 match sender_account_change {
2411 UiAccountChange::Update(before, after) => {
2412 assert_eq!(
2413 after.lamports,
2414 before.lamports - mint_rent - (2 * 5000), "Payer account should be original balance minus rent"
2416 );
2417 }
2418 other => {
2419 panic!("Expected account state to be an Update, got: {:?}", other);
2420 }
2421 }
2422
2423 let UiAccountProfileState::Writable(mint_account_change) = account_states
2424 .get(&mint.pubkey())
2425 .expect("Mint account state should be present")
2426 else {
2427 panic!("Expected mint account state to be Writable");
2428 };
2429 match mint_account_change {
2430 UiAccountChange::Create(mint_account) => {
2431 assert_eq!(
2432 mint_account.lamports, mint_rent,
2433 "Mint account should have the correct rent amount"
2434 );
2435 assert_eq!(
2436 mint_account.owner,
2437 spl_token_2022_interface::id().to_string(),
2438 "Mint account should be owned by the SPL Token program"
2439 );
2440 assert_eq!(
2442 mint_account.data,
2443 UiAccountData::Binary(
2444 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".into(),
2445 UiAccountEncoding::Base64
2446 ),
2447 );
2448 }
2449 other => {
2450 panic!("Expected account state to be an Update, got: {:?}", other);
2451 }
2452 }
2453 }
2454
2455 {
2457 let ix_profile = ui_profile_result
2458 .instruction_profiles
2459 .as_ref()
2460 .unwrap()
2461 .get(1)
2462 .expect("instruction profile should exist");
2463
2464 assert!(
2465 ix_profile.error_message.is_none(),
2466 "Profile should succeed, found error: {}",
2467 ix_profile.error_message.as_ref().unwrap()
2468 );
2469 assert_eq!(ix_profile.compute_units_consumed, 1230);
2470 let account_states = &ix_profile.account_states;
2471
2472 assert!(account_states.get(&payer.pubkey()).is_none());
2473
2474 let UiAccountProfileState::Writable(mint_account_change) = account_states
2475 .get(&mint.pubkey())
2476 .expect("Mint account state should be present")
2477 else {
2478 panic!("Expected mint account state to be Writable");
2479 };
2480 match mint_account_change {
2481 UiAccountChange::Update(_before, after) => {
2482 assert_eq!(
2483 after.lamports, mint_rent,
2484 "Mint account should have the correct rent amount"
2485 );
2486 assert_eq!(
2487 after.owner,
2488 spl_token_2022_interface::id().to_string(),
2489 "Mint account should be owned by the SPL Token program"
2490 );
2491 assert_eq!(
2493 after.data,
2494 UiAccountData::Json(ParsedAccount {
2495 program: "spl-token-2022".to_string(),
2496 parsed: json!({
2497 "info": {
2498 "decimals": 2,
2499 "freezeAuthority": payer.pubkey().to_string(),
2500 "mintAuthority": payer.pubkey().to_string(),
2501 "isInitialized": true,
2502 "supply": "0",
2503 },
2504 "type": "mint"
2505 }),
2506 space: 82,
2507 }),
2508 );
2509 }
2510 other => {
2511 panic!("Expected account state to be an Update, got: {:?}", other);
2512 }
2513 }
2514 }
2515
2516 {
2518 let ix_profile = ui_profile_result
2519 .instruction_profiles
2520 .as_ref()
2521 .unwrap()
2522 .get(2)
2523 .expect("instruction profile should exist");
2524 assert!(
2525 ix_profile.error_message.is_none(),
2526 "Profile should succeed, found error: {}",
2527 ix_profile.error_message.as_ref().unwrap()
2528 );
2529
2530 let account_states = &ix_profile.account_states;
2531
2532 let UiAccountProfileState::Writable(sender_account_change) = account_states
2533 .get(&payer.pubkey())
2534 .expect("Payer account state should be present")
2535 else {
2536 panic!("Expected account state to be Writable");
2537 };
2538
2539 match sender_account_change {
2540 UiAccountChange::Update(before, after) => {
2541 assert_eq!(
2542 after.lamports,
2543 before.lamports - 2074080,
2544 "Payer account should be original balance minus rent"
2545 );
2546 }
2547 other => {
2548 panic!("Expected account state to be an Update, got: {:?}", other);
2549 }
2550 }
2551
2552 let UiAccountProfileState::Writable(mint_account_change) = account_states
2553 .get(&mint.pubkey())
2554 .expect("Mint account state should be present")
2555 else {
2556 panic!("Expected mint account state to be Writable");
2557 };
2558 match mint_account_change {
2559 UiAccountChange::Unchanged(mint_account) => {
2560 assert!(mint_account.is_some())
2561 }
2562 other => {
2563 panic!("Expected account state to be Unchanged, got: {:?}", other);
2564 }
2565 }
2566
2567 let UiAccountProfileState::Writable(source_ata_change) = account_states
2568 .get(&source_ata)
2569 .expect("account state should be present")
2570 else {
2571 panic!("Expected account state to be Writable");
2572 };
2573
2574 match source_ata_change {
2575 UiAccountChange::Create(new) => {
2576 assert_eq!(
2577 new.lamports, 2074080,
2578 "Source ATA should have the correct lamports after creation"
2579 );
2580 assert_eq!(
2581 new.owner,
2582 spl_token_2022_interface::id().to_string(),
2583 "Source ATA should be owned by the SPL Token program"
2584 );
2585
2586 match &new.data {
2587 UiAccountData::Json(parsed) => {
2588 assert_eq!(
2589 parsed,
2590 &ParsedAccount {
2591 program: "spl-token-2022".into(),
2592 parsed: json!({
2593 "info": {
2594 "extensions": [
2595 {
2596 "extension": "immutableOwner"
2597 }
2598 ],
2599 "isNative": false,
2600 "mint": mint.pubkey().to_string(),
2601 "owner": owner.pubkey().to_string(),
2602 "state": "initialized",
2603 "tokenAmount": {
2604 "amount": "0",
2605 "decimals": 2,
2606 "uiAmount": 0.0,
2607 "uiAmountString": "0"
2608 }
2609 },
2610 "type": "account"
2611 }),
2612 space: 170
2613 }
2614 );
2615 }
2616 _ => panic!("Expected source ATA data to be JSON"),
2617 }
2618 }
2619 other => {
2620 panic!("Expected account state to be Create, got: {:?}", other);
2621 }
2622 }
2623 }
2624
2625 {
2627 let ix_profile = ui_profile_result
2628 .instruction_profiles
2629 .as_ref()
2630 .unwrap()
2631 .get(3)
2632 .expect("instruction profile should exist");
2633 assert!(
2634 ix_profile.error_message.is_none(),
2635 "Profile should succeed, found error: {}",
2636 ix_profile.error_message.as_ref().unwrap()
2637 );
2638
2639 let account_states = &ix_profile.account_states;
2640
2641 let UiAccountProfileState::Writable(sender_account_change) = account_states
2642 .get(&payer.pubkey())
2643 .expect("Payer account state should be present")
2644 else {
2645 panic!("Expected account state to be Writable");
2646 };
2647
2648 match sender_account_change {
2649 UiAccountChange::Update(before, after) => {
2650 assert_eq!(
2651 after.lamports,
2652 before.lamports - 2074080,
2653 "Payer account should be original balance minus rent"
2654 );
2655 }
2656 other => {
2657 panic!("Expected account state to be an Update, got: {:?}", other);
2658 }
2659 }
2660
2661 let UiAccountProfileState::Writable(mint_account_change) = account_states
2662 .get(&mint.pubkey())
2663 .expect("Mint account state should be present")
2664 else {
2665 panic!("Expected mint account state to be Writable");
2666 };
2667 match mint_account_change {
2668 UiAccountChange::Unchanged(mint_account) => {
2669 assert!(mint_account.is_some())
2670 }
2671 other => {
2672 panic!("Expected account state to be Unchanged, got: {:?}", other);
2673 }
2674 }
2675
2676 let UiAccountProfileState::Writable(destination_ata_change) = account_states
2677 .get(&destination_ata)
2678 .expect("account state should be present")
2679 else {
2680 panic!("Expected account state to be Writable");
2681 };
2682
2683 match destination_ata_change {
2684 UiAccountChange::Create(new) => {
2685 assert_eq!(
2686 new.lamports, 2074080,
2687 "Source ATA should have the correct lamports after creation"
2688 );
2689 assert_eq!(
2690 new.owner,
2691 spl_token_2022_interface::id().to_string(),
2692 "Source ATA should be owned by the SPL Token program"
2693 );
2694 match &new.data {
2695 UiAccountData::Json(parsed) => {
2696 assert_eq!(
2697 parsed,
2698 &ParsedAccount {
2699 program: "spl-token-2022".into(),
2700 parsed: json!({
2701 "info": {
2702 "extensions": [
2703 {
2704 "extension": "immutableOwner"
2705 }
2706 ],
2707 "isNative": false,
2708 "mint": mint.pubkey().to_string(),
2709 "owner": recipient.pubkey().to_string(),
2710 "state": "initialized",
2711 "tokenAmount": {
2712 "amount": "0",
2713 "decimals": 2,
2714 "uiAmount": 0.0,
2715 "uiAmountString": "0"
2716 }
2717 },
2718 "type": "account"
2719 }),
2720 space: 170
2721 }
2722 );
2723 }
2724 _ => panic!("Expected source ATA data to be JSON"),
2725 }
2726 }
2727 other => {
2728 panic!("Expected account state to be Create, got: {:?}", other);
2729 }
2730 }
2731 }
2732
2733 {
2735 let ix_profile = ui_profile_result
2736 .instruction_profiles
2737 .as_ref()
2738 .unwrap()
2739 .get(4)
2740 .expect("instruction profile should exist");
2741 assert!(
2742 ix_profile.error_message.is_none(),
2743 "Profile should succeed, found error: {}",
2744 ix_profile.error_message.as_ref().unwrap()
2745 );
2746
2747 let account_states = &ix_profile.account_states;
2748
2749 let UiAccountProfileState::Writable(sender_account_change) = account_states
2750 .get(&payer.pubkey())
2751 .expect("Payer account state should be present")
2752 else {
2753 panic!("Expected account state to be Writable");
2754 };
2755
2756 match sender_account_change {
2757 UiAccountChange::Unchanged(unchanged) => {
2758 assert!(unchanged.is_some(), "Payer account should remain unchanged");
2759 }
2760 other => {
2761 panic!("Expected account state to be Unchanged, got: {:?}", other);
2762 }
2763 }
2764
2765 let UiAccountProfileState::Writable(mint_account_change) = account_states
2766 .get(&mint.pubkey())
2767 .expect("Mint account state should be present")
2768 else {
2769 panic!("Expected mint account state to be Writable");
2770 };
2771 match mint_account_change {
2772 UiAccountChange::Update(before, after) => {
2773 assert_eq!(
2774 after.lamports, before.lamports,
2775 "Lamports should stay the same for mint account"
2776 );
2777 assert_eq!(
2778 after.data,
2779 UiAccountData::Json(ParsedAccount {
2780 program: "spl-token-2022".into(),
2781 parsed: json!({
2782 "info": {
2783 "decimals": 2,
2784 "freezeAuthority": payer.pubkey().to_string(),
2785 "isInitialized": true,
2786 "mintAuthority": payer.pubkey().to_string(),
2787 "supply": "10000",
2788 },
2789 "type": "mint"
2790 }),
2791 space: 82
2792 }),
2793 "Data should stay the same for mint account"
2794 );
2795 }
2796 other => {
2797 panic!("Expected account state to be Update, got: {:?}", other);
2798 }
2799 }
2800 }
2801
2802 assert_eq!(
2803 ui_profile_result.transaction_profile.compute_units_consumed,
2804 ui_profile_result
2805 .instruction_profiles
2806 .as_ref()
2807 .unwrap()
2808 .iter()
2809 .map(|ix| ix.compute_units_consumed)
2810 .sum::<u64>(),
2811 )
2812 }
2813 let recent_blockhash = client
2815 .context
2816 .svm_locker
2817 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
2818
2819 let transfer_amount = 50;
2821
2822 let transfer_instruction = transfer_checked(
2824 &spl_token_2022_interface::id(), &source_ata, &mint.pubkey(), &destination_ata, &owner.pubkey(), &[&payer.pubkey(), &owner.pubkey()], transfer_amount, 2, )
2833 .unwrap();
2834
2835 let transaction = Transaction::new_signed_with_payer(
2837 &[transfer_instruction],
2838 Some(&payer.pubkey()),
2839 &[&payer, &owner],
2840 recent_blockhash,
2841 );
2842 let signature = transaction.signatures[0];
2843 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
2844 client
2846 .context
2847 .svm_locker
2848 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
2849 .await
2850 .unwrap();
2851
2852 {
2853 let profile_result = client
2854 .rpc
2855 .get_transaction_profile(
2856 Some(client.context.clone()),
2857 UuidOrSignature::Signature(signature),
2858 Some(RpcProfileResultConfig {
2859 depth: Some(RpcProfileDepth::Instruction),
2860 ..Default::default()
2861 }),
2862 )
2863 .unwrap()
2864 .value
2865 .expect("missing profile result for processed transaction");
2866
2867 assert!(
2868 profile_result.transaction_profile.error_message.is_none(),
2869 "Transaction should succeed, found error: {}",
2870 profile_result
2871 .transaction_profile
2872 .error_message
2873 .as_ref()
2874 .unwrap()
2875 );
2876
2877 assert_eq!(
2878 profile_result.instruction_profiles.as_ref().unwrap().len(),
2879 1
2880 );
2881
2882 let ix_profile = profile_result
2883 .instruction_profiles
2884 .as_ref()
2885 .unwrap()
2886 .first()
2887 .expect("instruction profile should exist");
2888 assert!(
2889 ix_profile.error_message.is_none(),
2890 "Profile should succeed, found error: {}",
2891 ix_profile.error_message.as_ref().unwrap()
2892 );
2893
2894 let mut account_states = ix_profile.account_states.clone();
2895
2896 let UiAccountProfileState::Writable(owner_account_change) = account_states
2897 .swap_remove(&owner.pubkey())
2898 .expect("account state should be present")
2899 else {
2900 panic!("Expected account state to be Writable");
2901 };
2902
2903 let UiAccountChange::Unchanged(unchanged) = owner_account_change else {
2904 panic!(
2905 "Expected account state to be Unchanged, got: {:?}",
2906 owner_account_change
2907 );
2908 };
2909 assert!(unchanged.is_none(), "Owner account shouldn't exist");
2910
2911 let UiAccountProfileState::Writable(sender_account_change) = account_states
2912 .swap_remove(&payer.pubkey())
2913 .expect("Payer account state should be present")
2914 else {
2915 panic!("Expected account state to be Writable");
2916 };
2917
2918 match sender_account_change {
2919 UiAccountChange::Update(before, after) => {
2920 assert_eq!(after.lamports, before.lamports - 10000);
2921 }
2922 other => {
2923 panic!("Expected account state to be an Update, got: {:?}", other);
2924 }
2925 }
2926
2927 let UiAccountProfileState::Readonly = account_states
2928 .swap_remove(&mint.pubkey())
2929 .expect("Mint account state should be present")
2930 else {
2931 panic!("Expected mint account state to be Readonly");
2932 };
2933 let UiAccountProfileState::Readonly = account_states
2934 .swap_remove(&spl_token_2022_interface::ID)
2935 .expect("account state should be present")
2936 else {
2937 panic!("Expected account state to be Readonly");
2938 };
2939
2940 let UiAccountProfileState::Writable(source_ata_change) = account_states
2941 .swap_remove(&source_ata)
2942 .expect("account state should be present")
2943 else {
2944 panic!("Expected account state to be Writable");
2945 };
2946
2947 match source_ata_change {
2948 UiAccountChange::Update(before, after) => {
2949 assert_eq!(
2950 after.lamports, before.lamports,
2951 "Source ATA lamports should remain unchanged"
2952 );
2953 assert_eq!(
2954 after.data,
2955 UiAccountData::Json(ParsedAccount {
2956 program: "spl-token-2022".into(),
2957 parsed: json!({
2958 "info": {
2959 "extensions": [
2960 {
2961 "extension": "immutableOwner"
2962 }
2963 ],
2964 "isNative": false,
2965 "mint": mint.pubkey().to_string(),
2966 "owner": owner.pubkey().to_string(),
2967 "state": "initialized",
2968 "tokenAmount": {
2969 "amount": "9950",
2970 "decimals": 2,
2971 "uiAmount": 99.5,
2972 "uiAmountString": "99.5"
2973 }
2974 },
2975 "type": "account"
2976 }),
2977 space: 170
2978 }),
2979 "Source ATA data should be updated after transfer"
2980 );
2981 }
2982 other => {
2983 panic!("Expected account state to be Update, got: {:?}", other);
2984 }
2985 }
2986
2987 let UiAccountProfileState::Writable(destination_ata_change) = account_states
2988 .swap_remove(&destination_ata)
2989 .expect("account state should be present")
2990 else {
2991 panic!("Expected account state to be Writable");
2992 };
2993
2994 match destination_ata_change {
2995 UiAccountChange::Update(before, after) => {
2996 assert_eq!(
2997 after.lamports, before.lamports,
2998 "Destination ATA lamports should remain unchanged"
2999 );
3000 assert_eq!(
3001 after.data,
3002 UiAccountData::Json(ParsedAccount {
3003 program: "spl-token-2022".into(),
3004 parsed: json!({
3005 "info": {
3006 "extensions": [
3007 {
3008 "extension": "immutableOwner"
3009 }
3010 ],
3011 "isNative": false,
3012 "mint": mint.pubkey().to_string(),
3013 "owner": recipient.pubkey().to_string(),
3014 "state": "initialized",
3015 "tokenAmount": {
3016 "amount": transfer_amount.to_string(),
3017 "decimals": 2,
3018 "uiAmount": 0.5,
3019 "uiAmountString": "0.5"
3020 }
3021 },
3022 "type": "account"
3023 }),
3024 space: 170
3025 }),
3026 "Destination ATA data should be updated after transfer"
3027 );
3028 }
3029 other => {
3030 panic!("Expected account state to be Update, got: {:?}", other);
3031 }
3032 }
3033
3034 assert!(
3035 account_states.is_empty(),
3036 "All account states should have been processed, found: {:?}",
3037 account_states
3038 );
3039 }
3040 }
3041
3042 fn set_account(client: &TestSetup<SurfnetCheatcodesRpc>, pubkey: &Pubkey, account: &Account) {
3043 client
3044 .context
3045 .svm_locker
3046 .with_svm_writer(|svm| svm.inner.set_account(*pubkey, account.clone()))
3047 .expect("Failed to set account");
3048 }
3049
3050 fn verify_snapshot_account(
3051 snapshot: &BTreeMap<String, AccountSnapshot>,
3052 expected_account_pubkey: &Pubkey,
3053 expected_account: &Account,
3054 ) {
3055 let account = snapshot
3056 .get(&expected_account_pubkey.to_string())
3057 .unwrap_or_else(|| {
3058 panic!(
3059 "Account fixture not found for pubkey {}",
3060 expected_account_pubkey
3061 )
3062 });
3063 assert_eq!(expected_account.lamports, account.lamports);
3064 assert_eq!(
3065 base64::engine::general_purpose::STANDARD.encode(&expected_account.data),
3066 account.data
3067 );
3068 assert_eq!(expected_account.owner.to_string(), account.owner);
3069 assert_eq!(expected_account.executable, account.executable);
3070 assert_eq!(expected_account.rent_epoch, account.rent_epoch);
3071 }
3072
3073 #[test]
3074 fn test_export_snapshot() {
3075 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3076
3077 let pubkey1 = Pubkey::new_unique();
3078 let account1 = Account {
3079 lamports: 1_000_000,
3080 data: vec![1, 2, 3, 4],
3081 owner: system_program::id(),
3082 executable: false,
3083 rent_epoch: 0,
3084 };
3085
3086 set_account(&client, &pubkey1, &account1);
3087
3088 let pubkey2 = Pubkey::new_unique();
3089 let account2 = Account {
3090 lamports: 2_000_000,
3091 data: vec![5, 6, 7, 8, 9],
3092 owner: system_program::id(),
3093 executable: false,
3094 rent_epoch: 0,
3095 };
3096
3097 set_account(&client, &pubkey2, &account2);
3098
3099 let snapshot = client
3100 .rpc
3101 .export_snapshot(Some(client.context.clone()), None)
3102 .expect("Failed to export snapshot")
3103 .value;
3104
3105 verify_snapshot_account(&snapshot, &pubkey1, &account1);
3106 verify_snapshot_account(&snapshot, &pubkey2, &account2);
3107 }
3108
3109 #[test]
3110 fn test_export_snapshot_json_parsed() {
3111 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3112
3113 let pubkey1 = Pubkey::new_unique();
3114 println!("Pubkey1: {}", pubkey1);
3115 let account1 = Account {
3116 lamports: 1_000_000,
3117 data: vec![1, 2, 3, 4],
3118 owner: system_program::id(),
3119 executable: false,
3120 rent_epoch: 0,
3121 };
3122
3123 set_account(&client, &pubkey1, &account1);
3124
3125 let mint_pubkey = Pubkey::new_unique();
3126 println!("Mint Pubkey: {}", mint_pubkey);
3127 let mint_authority = Pubkey::new_unique();
3128
3129 let mut mint_data = [0u8; Mint::LEN];
3130 let mint = Mint {
3131 mint_authority: COption::Some(mint_authority),
3132 supply: 1000,
3133 decimals: 6,
3134 is_initialized: true,
3135 freeze_authority: COption::None,
3136 };
3137 mint.pack_into_slice(&mut mint_data);
3138
3139 let mint_account = Account {
3140 lamports: 1_000_000,
3141 data: mint_data.to_vec(),
3142 owner: spl_token_interface::id(),
3143 executable: false,
3144 rent_epoch: 0,
3145 };
3146
3147 set_account(&client, &mint_pubkey, &mint_account);
3148
3149 let snapshot = client
3150 .rpc
3151 .export_snapshot(
3152 Some(client.context.clone()),
3153 Some(ExportSnapshotConfig {
3154 include_parsed_accounts: Some(true),
3155 filter: None,
3156 scope: ExportSnapshotScope::Network,
3157 }),
3158 )
3159 .expect("Failed to export snapshot")
3160 .value;
3161
3162 verify_snapshot_account(&snapshot, &pubkey1, &account1);
3163 let actual_account1 = snapshot
3164 .get(&pubkey1.to_string())
3165 .expect("Account fixture not found");
3166 assert!(
3167 actual_account1.parsed_data.is_none(),
3168 "Account1 should not have parsed data"
3169 );
3170
3171 verify_snapshot_account(&snapshot, &mint_pubkey, &mint_account);
3172 let mint_snapshot = snapshot
3173 .get(&mint_pubkey.to_string())
3174 .expect("Mint account snapshot not found");
3175 let parsed = mint_snapshot
3176 .parsed_data
3177 .as_ref()
3178 .expect("Parsed data should be present");
3179
3180 assert_eq!(parsed.program, "spl-token");
3181 assert_eq!(parsed.space, Mint::LEN as u64);
3182
3183 let parsed_info = parsed
3184 .parsed
3185 .as_object()
3186 .expect("Parsed data should be an object");
3187 let info = parsed_info
3188 .get("info")
3189 .expect("Parsed data should have info field")
3190 .as_object()
3191 .expect("Info field should be an object");
3192 assert_eq!(
3193 info.get("mintAuthority")
3194 .and_then(|v| v.as_str())
3195 .expect("mintAuthority should be a string"),
3196 mint_authority.to_string()
3197 );
3198 }
3199
3200 #[test]
3201 fn test_export_snapshot_pre_transaction() {
3202 use std::collections::HashMap;
3203
3204 use solana_signature::Signature;
3205 use surfpool_types::{ProfileResult, types::KeyedProfileResult};
3206
3207 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3208
3209 let account1_pubkey = Pubkey::new_unique();
3211 let account1 = Account {
3212 lamports: 1_000_000,
3213 data: vec![1, 2, 3, 4],
3214 owner: system_program::id(),
3215 executable: false,
3216 rent_epoch: 0,
3217 };
3218 set_account(&client, &account1_pubkey, &account1);
3219
3220 let account2_pubkey = Pubkey::new_unique();
3221 let account2 = Account {
3222 lamports: 2_000_000,
3223 data: vec![5, 6, 7, 8],
3224 owner: system_program::id(),
3225 executable: false,
3226 rent_epoch: 0,
3227 };
3228 set_account(&client, &account2_pubkey, &account2);
3229
3230 let account3_pubkey = Pubkey::new_unique();
3231 let account3 = Account {
3232 lamports: 3_000_000,
3233 data: vec![9, 10, 11, 12],
3234 owner: system_program::id(),
3235 executable: false,
3236 rent_epoch: 0,
3237 };
3238 set_account(&client, &account3_pubkey, &account3);
3239
3240 let signature = Signature::new_unique();
3242 let mut pre_execution_capture = BTreeMap::new();
3243 pre_execution_capture.insert(account1_pubkey, Some(account1.clone()));
3244 pre_execution_capture.insert(account2_pubkey, Some(account2.clone()));
3245
3246 let mut post_execution_capture = BTreeMap::new();
3247 let mut modified_account1 = account1.clone();
3248 modified_account1.lamports = 500_000;
3249 post_execution_capture.insert(account1_pubkey, Some(modified_account1.clone()));
3250 let mut modified_account2 = account2.clone();
3251 modified_account2.lamports = 2_500_000;
3252 post_execution_capture.insert(account2_pubkey, Some(modified_account2.clone()));
3253
3254 let profile = ProfileResult {
3255 pre_execution_capture,
3256 post_execution_capture,
3257 compute_units_consumed: 1000,
3258 log_messages: None,
3259 error_message: None,
3260 };
3261
3262 let keyed_profile = KeyedProfileResult::new(
3263 1,
3264 UuidOrSignature::Signature(signature),
3265 None,
3266 profile,
3267 HashMap::new(),
3268 );
3269
3270 client.context.svm_locker.with_svm_writer(|svm| {
3272 svm.executed_transaction_profiles
3273 .store(signature.to_string(), keyed_profile)
3274 .unwrap();
3275 });
3276
3277 let snapshot = client
3279 .rpc
3280 .export_snapshot(
3281 Some(client.context.clone()),
3282 Some(ExportSnapshotConfig {
3283 include_parsed_accounts: Some(false),
3284 filter: None,
3285 scope: ExportSnapshotScope::PreTransaction(signature.to_string()),
3286 }),
3287 )
3288 .expect("Failed to export snapshot")
3289 .value;
3290
3291 assert!(
3293 snapshot.contains_key(&account1_pubkey.to_string()),
3294 "Snapshot should contain account1 (touched by transaction)"
3295 );
3296 assert!(
3297 snapshot.contains_key(&account2_pubkey.to_string()),
3298 "Snapshot should contain account2 (touched by transaction)"
3299 );
3300 assert!(
3301 !snapshot.contains_key(&account3_pubkey.to_string()),
3302 "Snapshot should NOT contain account3 (not touched by transaction)"
3303 );
3304
3305 verify_snapshot_account(&snapshot, &account1_pubkey, &account1);
3307 verify_snapshot_account(&snapshot, &account2_pubkey, &account2);
3308
3309 let snapshot_account1 = snapshot
3311 .get(&account1_pubkey.to_string())
3312 .expect("Account1 should be in snapshot");
3313 assert_eq!(
3314 snapshot_account1.lamports, 1_000_000,
3315 "Account1 should have pre-execution lamports (1M), not post-execution (500K)"
3316 );
3317
3318 let snapshot_account2 = snapshot
3319 .get(&account2_pubkey.to_string())
3320 .expect("Account2 should be in snapshot");
3321 assert_eq!(
3322 snapshot_account2.lamports, 2_000_000,
3323 "Account2 should have pre-execution lamports (2M), not post-execution (2.5M)"
3324 );
3325
3326 println!(
3330 "Snapshot contains {} accounts (expected at least 2)",
3331 snapshot.len()
3332 );
3333 }
3334
3335 #[test]
3336 fn test_export_snapshot_filtering() {
3337 let system_account_pubkey = Pubkey::new_unique();
3338 println!("System Account Pubkey: {}", system_account_pubkey);
3339 let excluded_system_account_pubkey = Pubkey::new_unique();
3340 println!(
3341 "Excluded System Account Pubkey: {}",
3342 excluded_system_account_pubkey
3343 );
3344 let program_account_pubkey = Pubkey::new_unique();
3345 println!("Program Account Pubkey: {}", program_account_pubkey);
3346 let included_program_account_pubkey = Pubkey::new_unique();
3347 println!(
3348 "Included Program Account Pubkey: {}",
3349 included_program_account_pubkey
3350 );
3351
3352 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3353
3354 let system_account = Account {
3355 lamports: 1_000_000,
3356 data: vec![1, 2, 3, 4],
3357 owner: system_program::id(),
3358 executable: false,
3359 rent_epoch: 0,
3360 };
3361 set_account(&client, &system_account_pubkey, &system_account);
3362 set_account(&client, &excluded_system_account_pubkey, &system_account);
3363
3364 let program_account = Account {
3365 lamports: 2_000_000,
3366 data: vec![5, 6, 7, 8, 9],
3367 owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
3368 executable: false,
3369 rent_epoch: 0,
3370 };
3371 set_account(&client, &program_account_pubkey, &program_account);
3372 set_account(&client, &included_program_account_pubkey, &program_account);
3373
3374 let snapshot = client
3375 .rpc
3376 .export_snapshot(Some(client.context.clone()), None)
3377 .expect("Failed to export snapshot")
3378 .value;
3379 assert!(
3380 !snapshot.contains_key(&program_account_pubkey.to_string()),
3381 "Program account should be excluded by default"
3382 );
3383 assert!(
3384 !snapshot.contains_key(&included_program_account_pubkey.to_string()),
3385 "Program account should be excluded by default"
3386 );
3387 let snapshot = client
3388 .rpc
3389 .export_snapshot(
3390 Some(client.context.clone()),
3391 Some(ExportSnapshotConfig {
3392 filter: Some(ExportSnapshotFilter {
3393 include_accounts: Some(vec![included_program_account_pubkey.to_string()]),
3394 ..Default::default()
3395 }),
3396 ..Default::default()
3397 }),
3398 )
3399 .expect("Failed to export snapshot")
3400 .value;
3401 assert!(
3402 !snapshot.contains_key(&program_account_pubkey.to_string()),
3403 "Program account should be excluded by default"
3404 );
3405 assert!(
3406 snapshot.contains_key(&included_program_account_pubkey.to_string()),
3407 "Program account should be included when explicitly listed"
3408 );
3409
3410 let snapshot = client
3411 .rpc
3412 .export_snapshot(
3413 Some(client.context.clone()),
3414 Some(ExportSnapshotConfig {
3415 filter: Some(ExportSnapshotFilter {
3416 include_program_accounts: Some(true),
3417 exclude_accounts: Some(vec![excluded_system_account_pubkey.to_string()]),
3418 ..Default::default()
3419 }),
3420 ..Default::default()
3421 }),
3422 )
3423 .expect("Failed to export snapshot")
3424 .value;
3425
3426 assert!(
3427 snapshot.contains_key(&program_account_pubkey.to_string()),
3428 "Program account should be included when filter is set"
3429 );
3430 assert!(
3431 snapshot.contains_key(&included_program_account_pubkey.to_string()),
3432 "Included program account should be present"
3433 );
3434 assert!(
3435 snapshot.contains_key(&system_account_pubkey.to_string()),
3436 "System account should be present"
3437 );
3438 assert!(
3439 !snapshot.contains_key(&excluded_system_account_pubkey.to_string()),
3440 "Excluded system account should not be present"
3441 );
3442 }
3443
3444 #[tokio::test(flavor = "multi_thread")]
3445 async fn test_write_program_creates_accounts_automatically() {
3446 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3448 let program_id = Keypair::new();
3449
3450 let program_data_address =
3452 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3453
3454 let program_account_before = client.context.svm_locker.with_svm_reader(|svm_reader| {
3455 svm_reader.inner.get_account(&program_id.pubkey()).unwrap()
3456 });
3457 assert!(
3458 program_account_before.is_none(),
3459 "Program account should not exist initially"
3460 );
3461
3462 let program_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
3464 let result = client
3465 .rpc
3466 .write_program(
3467 Some(client.context.clone()),
3468 program_id.pubkey().to_string(),
3469 hex::encode(&program_data),
3470 0,
3471 None,
3472 )
3473 .await;
3474
3475 assert!(
3476 result.is_ok(),
3477 "Failed to write program: {:?}",
3478 result.err()
3479 );
3480
3481 let program_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3483 svm_reader.inner.get_account(&program_id.pubkey()).unwrap()
3484 });
3485 assert!(
3486 program_account.is_some(),
3487 "Program account should be created"
3488 );
3489
3490 let program_account = program_account.unwrap();
3491 assert_eq!(
3492 program_account.owner,
3493 solana_sdk_ids::bpf_loader_upgradeable::id()
3494 );
3495 assert!(
3496 program_account.executable,
3497 "Program account should be executable"
3498 );
3499
3500 let program_data_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3502 svm_reader.inner.get_account(&program_data_address).unwrap()
3503 });
3504 assert!(
3505 program_data_account.is_some(),
3506 "Program data account should be created"
3507 );
3508
3509 let program_data_account = program_data_account.unwrap();
3510 assert_eq!(
3511 program_data_account.owner,
3512 solana_sdk_ids::bpf_loader_upgradeable::id()
3513 );
3514 assert!(
3515 !program_data_account.executable,
3516 "Program data account should not be executable"
3517 );
3518
3519 println!("✅ Both accounts created successfully");
3520 }
3521
3522 #[tokio::test(flavor = "multi_thread")]
3523 async fn test_write_program_single_chunk_small() {
3524 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3526 let program_id = Keypair::new();
3527
3528 let program_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
3529 let data_hex = hex::encode(&program_data);
3530
3531 let result = client
3532 .rpc
3533 .write_program(
3534 Some(client.context.clone()),
3535 program_id.pubkey().to_string(),
3536 data_hex,
3537 0,
3538 None,
3539 )
3540 .await;
3541
3542 assert!(
3543 result.is_ok(),
3544 "Failed to write program: {:?}",
3545 result.err()
3546 );
3547
3548 let program_data_address =
3550 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3551 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3552 svm_reader
3553 .inner
3554 .get_account(&program_data_address)
3555 .unwrap()
3556 .unwrap()
3557 });
3558
3559 let metadata_size =
3560 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3561 );
3562 let written_data = &account.data[metadata_size..metadata_size + program_data.len()];
3563 assert_eq!(
3564 written_data, &program_data,
3565 "Written data should match input"
3566 );
3567
3568 println!("✅ Small program written correctly");
3569 }
3570
3571 #[tokio::test(flavor = "multi_thread")]
3572 async fn test_write_program_single_chunk_large() {
3573 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3575 let program_id = Keypair::new();
3576
3577 let program_data = vec![0xAB; 1024 * 1024]; let data_hex = hex::encode(&program_data);
3579
3580 let result = client
3581 .rpc
3582 .write_program(
3583 Some(client.context.clone()),
3584 program_id.pubkey().to_string(),
3585 data_hex,
3586 0,
3587 None,
3588 )
3589 .await;
3590
3591 assert!(
3592 result.is_ok(),
3593 "Failed to write large program: {:?}",
3594 result.err()
3595 );
3596
3597 let program_data_address =
3599 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3600 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3601 svm_reader
3602 .inner
3603 .get_account(&program_data_address)
3604 .unwrap()
3605 .unwrap()
3606 });
3607
3608 let metadata_size =
3609 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3610 );
3611 assert_eq!(
3612 account.data.len(),
3613 metadata_size + program_data.len(),
3614 "Account should have correct size"
3615 );
3616
3617 println!("✅ Large program (1MB) written successfully");
3618 }
3619
3620 #[tokio::test(flavor = "multi_thread")]
3621 async fn test_write_program_multiple_sequential_chunks() {
3622 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3624 let program_id = Keypair::new();
3625
3626 let chunks = vec![
3627 (vec![0x01; 1024], 0), (vec![0x02; 1024], 1024), (vec![0x03; 1024], 2048), (vec![0x04; 512], 3072), ];
3632
3633 for (i, (chunk_data, offset)) in chunks.iter().enumerate() {
3634 let result = client
3635 .rpc
3636 .write_program(
3637 Some(client.context.clone()),
3638 program_id.pubkey().to_string(),
3639 hex::encode(chunk_data),
3640 *offset,
3641 None,
3642 )
3643 .await;
3644
3645 assert!(
3646 result.is_ok(),
3647 "Failed to write chunk {} at offset {}: {:?}",
3648 i,
3649 offset,
3650 result.err()
3651 );
3652 }
3653
3654 let program_data_address =
3656 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3657 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3658 svm_reader
3659 .inner
3660 .get_account(&program_data_address)
3661 .unwrap()
3662 .unwrap()
3663 });
3664
3665 let metadata_size =
3666 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3667 );
3668
3669 for (chunk_data, offset) in chunks {
3671 let start = metadata_size + offset as usize;
3672 let end = start + chunk_data.len();
3673 let written = &account.data[start..end];
3674 assert_eq!(
3675 written,
3676 &chunk_data[..],
3677 "Chunk at offset {} should match",
3678 offset
3679 );
3680 }
3681
3682 println!("✅ Multiple sequential chunks written correctly");
3683 }
3684
3685 #[tokio::test(flavor = "multi_thread")]
3686 async fn test_write_program_non_sequential_chunks() {
3687 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3689 let program_id = Keypair::new();
3690
3691 let chunks = vec![
3692 (vec![0x03; 512], 2048), (vec![0x01; 1024], 0), (vec![0x02; 1024], 1024), ];
3696
3697 for (chunk_data, offset) in chunks.iter() {
3698 let result = client
3699 .rpc
3700 .write_program(
3701 Some(client.context.clone()),
3702 program_id.pubkey().to_string(),
3703 hex::encode(chunk_data),
3704 *offset,
3705 None,
3706 )
3707 .await;
3708
3709 assert!(
3710 result.is_ok(),
3711 "Failed at offset {}: {:?}",
3712 offset,
3713 result.err()
3714 );
3715 }
3716
3717 let program_data_address =
3719 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3720 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3721 svm_reader
3722 .inner
3723 .get_account(&program_data_address)
3724 .unwrap()
3725 .unwrap()
3726 });
3727
3728 let metadata_size =
3729 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3730 );
3731
3732 assert_eq!(
3734 &account.data[metadata_size..metadata_size + 1024],
3735 &vec![0x01; 1024][..]
3736 );
3737 assert_eq!(
3739 &account.data[metadata_size + 1024..metadata_size + 2048],
3740 &vec![0x02; 1024][..]
3741 );
3742 assert_eq!(
3744 &account.data[metadata_size + 2048..metadata_size + 2560],
3745 &vec![0x03; 512][..]
3746 );
3747
3748 println!("✅ Non-sequential chunks written correctly");
3749 }
3750
3751 #[tokio::test(flavor = "multi_thread")]
3752 async fn test_write_program_overlapping_writes() {
3753 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3755 let program_id = Keypair::new();
3756
3757 let initial_data = vec![0xFF; 1024];
3759 client
3760 .rpc
3761 .write_program(
3762 Some(client.context.clone()),
3763 program_id.pubkey().to_string(),
3764 hex::encode(&initial_data),
3765 0,
3766 None,
3767 )
3768 .await
3769 .unwrap();
3770
3771 let overwrite_data = vec![0xAA; 512];
3773 client
3774 .rpc
3775 .write_program(
3776 Some(client.context.clone()),
3777 program_id.pubkey().to_string(),
3778 hex::encode(&overwrite_data),
3779 256, None,
3781 )
3782 .await
3783 .unwrap();
3784
3785 let program_data_address =
3787 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3788 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3789 svm_reader
3790 .inner
3791 .get_account(&program_data_address)
3792 .unwrap()
3793 .unwrap()
3794 });
3795
3796 let metadata_size =
3797 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3798 );
3799
3800 assert_eq!(
3802 &account.data[metadata_size..metadata_size + 256],
3803 &vec![0xFF; 256][..]
3804 );
3805 assert_eq!(
3807 &account.data[metadata_size + 256..metadata_size + 768],
3808 &vec![0xAA; 512][..]
3809 );
3810 assert_eq!(
3812 &account.data[metadata_size + 768..metadata_size + 1024],
3813 &vec![0xFF; 256][..]
3814 );
3815
3816 println!("✅ Overlapping writes handled correctly");
3817 }
3818
3819 #[tokio::test(flavor = "multi_thread")]
3820 async fn test_write_program_zero_offset() {
3821 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3823 let program_id = Keypair::new();
3824
3825 let data = vec![0x42; 128];
3826 let result = client
3827 .rpc
3828 .write_program(
3829 Some(client.context.clone()),
3830 program_id.pubkey().to_string(),
3831 hex::encode(&data),
3832 0,
3833 None,
3834 )
3835 .await;
3836
3837 assert!(result.is_ok());
3838
3839 let program_data_address =
3840 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3841 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3842 svm_reader
3843 .inner
3844 .get_account(&program_data_address)
3845 .unwrap()
3846 .unwrap()
3847 });
3848
3849 let metadata_size =
3850 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3851 );
3852 assert_eq!(&account.data[metadata_size..metadata_size + 128], &data[..]);
3853
3854 println!("✅ Write at offset 0 works correctly");
3855 }
3856
3857 #[tokio::test(flavor = "multi_thread")]
3858 async fn test_write_program_large_offset() {
3859 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3861 let program_id = Keypair::new();
3862
3863 let large_offset = 1024 * 1024; let data = vec![0x99; 256];
3865
3866 let result = client
3867 .rpc
3868 .write_program(
3869 Some(client.context.clone()),
3870 program_id.pubkey().to_string(),
3871 hex::encode(&data),
3872 large_offset,
3873 None,
3874 )
3875 .await;
3876
3877 assert!(result.is_ok(), "Should handle large offset");
3878
3879 let program_data_address =
3880 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3881 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3882 svm_reader
3883 .inner
3884 .get_account(&program_data_address)
3885 .unwrap()
3886 .unwrap()
3887 });
3888
3889 let metadata_size =
3890 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3891 );
3892 let expected_size = metadata_size + large_offset as usize + data.len();
3893 assert_eq!(
3894 account.data.len(),
3895 expected_size,
3896 "Account should expand to fit data"
3897 );
3898
3899 let start = metadata_size + large_offset as usize;
3901 assert_eq!(&account.data[start..start + 256], &data[..]);
3902
3903 println!("✅ Large offset handled with account expansion");
3904 }
3905
3906 #[tokio::test(flavor = "multi_thread")]
3907 async fn test_write_program_empty_data() {
3908 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3910 let program_id = Keypair::new();
3911
3912 let result = client
3913 .rpc
3914 .write_program(
3915 Some(client.context.clone()),
3916 program_id.pubkey().to_string(),
3917 hex::encode(&[]),
3918 0,
3919 None,
3920 )
3921 .await;
3922
3923 assert!(result.is_ok(), "Should handle empty data");
3924
3925 println!("✅ Empty data handled correctly");
3926 }
3927
3928 #[tokio::test(flavor = "multi_thread")]
3929 async fn test_write_program_single_byte() {
3930 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3932 let program_id = Keypair::new();
3933
3934 let data = vec![0x42];
3935 let result = client
3936 .rpc
3937 .write_program(
3938 Some(client.context.clone()),
3939 program_id.pubkey().to_string(),
3940 hex::encode(&data),
3941 0,
3942 None,
3943 )
3944 .await;
3945
3946 assert!(result.is_ok());
3947
3948 let program_data_address =
3949 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
3950 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
3951 svm_reader
3952 .inner
3953 .get_account(&program_data_address)
3954 .unwrap()
3955 .unwrap()
3956 });
3957
3958 let metadata_size =
3959 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
3960 );
3961 assert_eq!(account.data[metadata_size], 0x42);
3962
3963 println!("✅ Single byte write works");
3964 }
3965
3966 #[tokio::test(flavor = "multi_thread")]
3967 async fn test_write_program_invalid_program_id() {
3968 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3970
3971 let result = client
3972 .rpc
3973 .write_program(
3974 Some(client.context.clone()),
3975 "not_a_valid_pubkey".to_string(),
3976 "deadbeef".to_string(),
3977 0,
3978 None,
3979 )
3980 .await;
3981
3982 assert!(result.is_err(), "Should fail with invalid program ID");
3983 assert!(
3984 result.unwrap_err().to_string().contains("Invalid pubkey"),
3985 "Error should mention invalid pubkey"
3986 );
3987
3988 println!("✅ Invalid program ID rejected");
3989 }
3990
3991 #[tokio::test(flavor = "multi_thread")]
3992 async fn test_write_program_invalid_hex_data() {
3993 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
3995 let program_id = Keypair::new();
3996
3997 let invalid_hex_strings = vec![
3998 "not_hex_at_all",
3999 "GHIJKLMN",
4000 "0x123", "12 34", ];
4003
4004 for invalid_hex in invalid_hex_strings {
4005 let result = client
4006 .rpc
4007 .write_program(
4008 Some(client.context.clone()),
4009 program_id.pubkey().to_string(),
4010 invalid_hex.to_string(),
4011 0,
4012 None,
4013 )
4014 .await;
4015
4016 assert!(
4017 result.is_err(),
4018 "Should fail with invalid hex: {}",
4019 invalid_hex
4020 );
4021 assert!(
4022 result.unwrap_err().to_string().contains("Invalid hex"),
4023 "Error should mention invalid hex"
4024 );
4025 }
4026
4027 println!("✅ Invalid hex data rejected");
4028 }
4029
4030 #[tokio::test(flavor = "multi_thread")]
4031 async fn test_write_program_rent_exemption() {
4032 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4034 let program_id = Keypair::new();
4035
4036 let small_data = vec![0x01; 128];
4038 client
4039 .rpc
4040 .write_program(
4041 Some(client.context.clone()),
4042 program_id.pubkey().to_string(),
4043 hex::encode(&small_data),
4044 0,
4045 None,
4046 )
4047 .await
4048 .unwrap();
4049
4050 let program_data_address =
4051 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4052
4053 let initial_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4054 svm_reader
4055 .inner
4056 .get_account(&program_data_address)
4057 .unwrap()
4058 .unwrap()
4059 .lamports
4060 });
4061
4062 let large_data = vec![0x02; 1024];
4064 client
4065 .rpc
4066 .write_program(
4067 Some(client.context.clone()),
4068 program_id.pubkey().to_string(),
4069 hex::encode(&large_data),
4070 10240, None,
4072 )
4073 .await
4074 .unwrap();
4075
4076 let final_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4077 svm_reader
4078 .inner
4079 .get_account(&program_data_address)
4080 .unwrap()
4081 .unwrap()
4082 .lamports
4083 });
4084
4085 assert!(
4086 final_lamports > initial_lamports,
4087 "Lamports should increase to maintain rent exemption"
4088 );
4089
4090 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4092 svm_reader
4093 .inner
4094 .get_account(&program_data_address)
4095 .unwrap()
4096 .unwrap()
4097 });
4098
4099 let required_lamports = client.context.svm_locker.with_svm_reader(|svm_reader| {
4100 svm_reader
4101 .inner
4102 .minimum_balance_for_rent_exemption(account.data.len())
4103 });
4104
4105 assert_eq!(
4106 account.lamports, required_lamports,
4107 "Account should have exact rent-exempt lamports"
4108 );
4109
4110 println!("✅ Rent exemption maintained during expansion");
4111 }
4112
4113 #[tokio::test(flavor = "multi_thread")]
4114 async fn test_write_program_account_ownership() {
4115 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4117 let program_id = Keypair::new();
4118 let authority = Keypair::new();
4119
4120 let data = vec![0xAB; 64];
4121 client
4122 .rpc
4123 .write_program(
4124 Some(client.context.clone()),
4125 program_id.pubkey().to_string(),
4126 hex::encode(&data),
4127 0,
4128 Some(authority.pubkey().to_string()),
4129 )
4130 .await
4131 .unwrap();
4132
4133 let program_data_address =
4134 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4135
4136 let program_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4138 svm_reader
4139 .inner
4140 .get_account(&program_id.pubkey())
4141 .unwrap()
4142 .unwrap()
4143 });
4144 assert_eq!(
4145 program_account.owner,
4146 solana_sdk_ids::bpf_loader_upgradeable::id(),
4147 "Program account should be owned by upgradeable loader"
4148 );
4149 assert!(
4150 program_account.executable,
4151 "Program account should be executable"
4152 );
4153
4154 let program_data_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4156 svm_reader
4157 .inner
4158 .get_account(&program_data_address)
4159 .unwrap()
4160 .unwrap()
4161 });
4162 assert_eq!(
4163 program_data_account.owner,
4164 solana_sdk_ids::bpf_loader_upgradeable::id(),
4165 "Program data account should be owned by upgradeable loader"
4166 );
4167 assert!(
4168 !program_data_account.executable,
4169 "Program data account should not be executable"
4170 );
4171
4172 let Ok(solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4175 slot: _,
4176 upgrade_authority_address,
4177 }) = bincode::deserialize::<solana_loader_v3_interface::state::UpgradeableLoaderState>(
4178 &program_data_account.data,
4179 )
4180 else {
4181 panic!("Program data account has incorrect state");
4182 };
4183
4184 assert_eq!(
4185 upgrade_authority_address,
4186 Some(authority.pubkey()),
4187 "Upgrade authority should match"
4188 );
4189
4190 println!("✅ Account ownership is correct");
4191 }
4192
4193 #[tokio::test(flavor = "multi_thread")]
4194 async fn test_write_program_metadata_preservation() {
4195 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4197 let program_id = Keypair::new();
4198
4199 client
4201 .rpc
4202 .write_program(
4203 Some(client.context.clone()),
4204 program_id.pubkey().to_string(),
4205 hex::encode(&vec![0x01; 128]),
4206 0,
4207 None,
4208 )
4209 .await
4210 .unwrap();
4211
4212 let program_data_address =
4213 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4214
4215 let initial_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4217 svm_reader
4218 .inner
4219 .get_account(&program_data_address)
4220 .unwrap()
4221 .unwrap()
4222 });
4223
4224 let metadata_size =
4225 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4226 );
4227 let initial_metadata = initial_account.data[..metadata_size].to_vec();
4228
4229 client
4231 .rpc
4232 .write_program(
4233 Some(client.context.clone()),
4234 program_id.pubkey().to_string(),
4235 hex::encode(&vec![0x02; 256]),
4236 128,
4237 None,
4238 )
4239 .await
4240 .unwrap();
4241
4242 let final_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4244 svm_reader
4245 .inner
4246 .get_account(&program_data_address)
4247 .unwrap()
4248 .unwrap()
4249 });
4250
4251 let final_metadata = final_account.data[..metadata_size].to_vec();
4252 assert_eq!(
4253 initial_metadata, final_metadata,
4254 "Metadata should be preserved"
4255 );
4256
4257 println!("✅ Metadata preserved across writes");
4258 }
4259
4260 #[tokio::test(flavor = "multi_thread")]
4261 async fn test_write_program_idempotent() {
4262 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4264 let program_id = Keypair::new();
4265
4266 let data = vec![0x55; 512];
4267 let offset = 100;
4268
4269 client
4271 .rpc
4272 .write_program(
4273 Some(client.context.clone()),
4274 program_id.pubkey().to_string(),
4275 hex::encode(&data),
4276 offset,
4277 None,
4278 )
4279 .await
4280 .unwrap();
4281
4282 let program_data_address =
4283 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4284
4285 let first_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4286 svm_reader
4287 .inner
4288 .get_account(&program_data_address)
4289 .unwrap()
4290 .unwrap()
4291 });
4292
4293 client
4295 .rpc
4296 .write_program(
4297 Some(client.context.clone()),
4298 program_id.pubkey().to_string(),
4299 hex::encode(&data),
4300 offset,
4301 None,
4302 )
4303 .await
4304 .unwrap();
4305
4306 let second_account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4307 svm_reader
4308 .inner
4309 .get_account(&program_data_address)
4310 .unwrap()
4311 .unwrap()
4312 });
4313
4314 assert_eq!(
4315 first_account.data, second_account.data,
4316 "Data should be identical"
4317 );
4318 assert_eq!(
4319 first_account.lamports, second_account.lamports,
4320 "Lamports should be identical"
4321 );
4322
4323 println!("✅ Writes are idempotent");
4324 }
4325
4326 #[tokio::test(flavor = "multi_thread")]
4327 async fn test_write_program_context_slot() {
4328 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4330 let program_id = Keypair::new();
4331
4332 let result = client
4333 .rpc
4334 .write_program(
4335 Some(client.context.clone()),
4336 program_id.pubkey().to_string(),
4337 hex::encode(&vec![0x42; 64]),
4338 0,
4339 None,
4340 )
4341 .await
4342 .unwrap();
4343
4344 assert!(result.context.slot > 0, "Context slot should be valid");
4345
4346 println!("✅ Response context is valid");
4347 }
4348
4349 #[tokio::test(flavor = "multi_thread")]
4350 async fn test_write_program_small_no_minimum_program_artifacts() {
4351 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4354 let program_id = Keypair::new();
4355
4356 let program_data = vec![0xAB; 100];
4357 let result = client
4358 .rpc
4359 .write_program(
4360 Some(client.context.clone()),
4361 program_id.pubkey().to_string(),
4362 hex::encode(&program_data),
4363 0,
4364 None,
4365 )
4366 .await;
4367
4368 assert!(
4369 result.is_ok(),
4370 "Failed to write program: {:?}",
4371 result.err()
4372 );
4373
4374 let program_data_address =
4375 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4376 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4377 svm_reader
4378 .inner
4379 .get_account(&program_data_address)
4380 .unwrap()
4381 .unwrap()
4382 });
4383
4384 let metadata_size =
4385 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4386 );
4387
4388 assert_eq!(
4390 account.data.len(),
4391 metadata_size + 100,
4392 "Account data length should be metadata_size ({}) + 100 = {}, but was {}",
4393 metadata_size,
4394 metadata_size + 100,
4395 account.data.len()
4396 );
4397
4398 let written_data = &account.data[metadata_size..];
4400 assert_eq!(
4401 written_data, &program_data,
4402 "Written data should match exactly"
4403 );
4404
4405 assert!(
4407 account.data[metadata_size + 100..].is_empty(),
4408 "There should be no trailing bytes beyond the written data"
4409 );
4410
4411 println!("✅ Small program has no minimum_program.so artifacts");
4412 }
4413
4414 #[tokio::test(flavor = "multi_thread")]
4415 async fn test_write_program_exact_account_size() {
4416 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4419
4420 let metadata_size =
4421 solana_loader_v3_interface::state::UpgradeableLoaderState::size_of_programdata_metadata(
4422 );
4423
4424 for data_len in [1usize, 100, 3311, 3312, 3313, 5000] {
4425 let program_id = Keypair::new();
4426 let program_data: Vec<u8> = (0..data_len).map(|i| (i % 256) as u8).collect();
4427
4428 let result = client
4429 .rpc
4430 .write_program(
4431 Some(client.context.clone()),
4432 program_id.pubkey().to_string(),
4433 hex::encode(&program_data),
4434 0,
4435 None,
4436 )
4437 .await;
4438
4439 assert!(
4440 result.is_ok(),
4441 "Failed to write program of size {}: {:?}",
4442 data_len,
4443 result.err()
4444 );
4445
4446 let program_data_address =
4447 solana_loader_v3_interface::get_program_data_address(&program_id.pubkey());
4448 let account = client.context.svm_locker.with_svm_reader(|svm_reader| {
4449 svm_reader
4450 .inner
4451 .get_account(&program_data_address)
4452 .unwrap()
4453 .unwrap()
4454 });
4455
4456 assert_eq!(
4457 account.data.len(),
4458 metadata_size + data_len,
4459 "For data_len={}, account data length should be {} but was {}",
4460 data_len,
4461 metadata_size + data_len,
4462 account.data.len()
4463 );
4464
4465 let written_data = &account.data[metadata_size..metadata_size + data_len];
4466 assert_eq!(
4467 written_data, &program_data,
4468 "For data_len={}, written content should match exactly",
4469 data_len
4470 );
4471 }
4472
4473 println!("✅ All program sizes produce exact account sizes");
4474 }
4475
4476 #[tokio::test(flavor = "multi_thread")]
4477 async fn test_write_program_execution_uses_written_bytes_not_noop() {
4478 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4481 let program_id = Keypair::new();
4482
4483 let mut error_program_elf = crate::surfnet::noop_program::NOOP_PROGRAM_ELF.to_vec();
4486 error_program_elf[124] = 0x01;
4489
4490 let result = client
4492 .rpc
4493 .write_program(
4494 Some(client.context.clone()),
4495 program_id.pubkey().to_string(),
4496 hex::encode(&error_program_elf),
4497 0,
4498 None,
4499 )
4500 .await;
4501 assert!(
4502 result.is_ok(),
4503 "Failed to write program: {:?}",
4504 result.err()
4505 );
4506
4507 let payer = Keypair::new();
4509 client
4510 .context
4511 .svm_locker
4512 .airdrop(&payer.pubkey(), 1_000_000_000)
4513 .unwrap()
4514 .unwrap();
4515
4516 let recent_blockhash = client
4518 .context
4519 .svm_locker
4520 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
4521 let invoke_ix = solana_instruction::Instruction {
4523 program_id: program_id.pubkey(),
4524 accounts: vec![],
4525 data: vec![],
4526 };
4527 let message = solana_message::Message::new_with_blockhash(
4528 &[invoke_ix],
4529 Some(&payer.pubkey()),
4530 &recent_blockhash,
4531 );
4532 let tx = VersionedTransaction::try_new(
4533 solana_message::VersionedMessage::Legacy(message),
4534 &[&payer],
4535 )
4536 .unwrap();
4537
4538 let sim_result = client.context.svm_locker.simulate_transaction(tx, false);
4540
4541 assert!(
4544 sim_result.is_err(),
4545 "Transaction should fail because the written program returns error (r0=1). \
4546 If it succeeded, the noop placeholder is still being executed instead of \
4547 the written program bytes."
4548 );
4549 }
4550
4551 #[test]
4552 fn test_stream_accounts_registers_multiple() {
4553 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4554
4555 let pubkey1 = Pubkey::new_unique();
4556 let pubkey2 = Pubkey::new_unique();
4557 let pubkey3 = Pubkey::new_unique();
4558
4559 let entries = vec![
4560 StreamAccountsEntry {
4561 pubkey: pubkey1.to_string(),
4562 include_owned_accounts: Some(true),
4563 },
4564 StreamAccountsEntry {
4565 pubkey: pubkey2.to_string(),
4566 include_owned_accounts: Some(false),
4567 },
4568 StreamAccountsEntry {
4569 pubkey: pubkey3.to_string(),
4570 include_owned_accounts: None,
4571 },
4572 ];
4573
4574 let result = client
4575 .rpc
4576 .stream_accounts(Some(client.context.clone()), entries);
4577 assert!(result.is_ok(), "stream_accounts should succeed");
4578
4579 let streamed = client
4581 .rpc
4582 .get_streamed_accounts(Some(client.context.clone()))
4583 .expect("Failed to get streamed accounts")
4584 .value;
4585
4586 let accounts = serde_json::to_value(&streamed).unwrap();
4587 let accounts_arr = accounts["accounts"].as_array().unwrap();
4588 assert_eq!(accounts_arr.len(), 3, "Should have 3 streamed accounts");
4589
4590 let find = |pk: &str| {
4592 accounts_arr
4593 .iter()
4594 .find(|a| a["pubkey"].as_str().unwrap() == pk)
4595 .unwrap()
4596 .clone()
4597 };
4598 assert_eq!(find(&pubkey1.to_string())["includeOwnedAccounts"], true);
4599 assert_eq!(find(&pubkey2.to_string())["includeOwnedAccounts"], false);
4600 assert_eq!(find(&pubkey3.to_string())["includeOwnedAccounts"], false);
4601 }
4602
4603 #[test]
4604 fn test_stream_accounts_empty_list() {
4605 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4606
4607 let result = client
4608 .rpc
4609 .stream_accounts(Some(client.context.clone()), vec![]);
4610 assert!(
4611 result.is_ok(),
4612 "stream_accounts with empty list should succeed"
4613 );
4614
4615 let streamed = client
4616 .rpc
4617 .get_streamed_accounts(Some(client.context.clone()))
4618 .expect("Failed to get streamed accounts")
4619 .value;
4620
4621 let accounts = serde_json::to_value(&streamed).unwrap();
4622 let accounts_arr = accounts["accounts"].as_array().unwrap();
4623 assert_eq!(
4624 accounts_arr.len(),
4625 0,
4626 "Should have no streamed accounts after empty call"
4627 );
4628 }
4629
4630 #[test]
4631 fn test_stream_accounts_invalid_pubkey() {
4632 let client = TestSetup::new(SurfnetCheatcodesRpc::empty());
4633
4634 let entries = vec![StreamAccountsEntry {
4635 pubkey: "not-a-valid-pubkey".to_string(),
4636 include_owned_accounts: None,
4637 }];
4638
4639 let result = client
4640 .rpc
4641 .stream_accounts(Some(client.context.clone()), entries);
4642 assert!(
4643 result.is_err(),
4644 "stream_accounts with invalid pubkey should fail"
4645 );
4646 }
4647}