surfpool_sdk/cheatcodes/mod.rs
1use std::{
2 env,
3 path::{Path, PathBuf},
4};
5
6use solana_client::rpc_request::RpcRequest;
7use solana_epoch_info::EpochInfo;
8use solana_keypair::{EncodableKey, Keypair};
9use solana_pubkey::Pubkey;
10use solana_rpc_client::rpc_client::RpcClient;
11use solana_signer::Signer;
12use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
13
14use crate::error::{SurfnetError, SurfnetResult};
15pub mod builders;
16use builders::{CheatcodeBuilder, DeployProgram};
17
18/// Direct state manipulation helpers for a running Surfnet.
19///
20/// These bypass normal transaction flow to instantly set account state —
21/// perfect for test setup (funding wallets, minting tokens, etc.).
22///
23/// ```rust
24/// use surfpool_sdk::{Pubkey, Surfnet};
25/// use surfpool_sdk::cheatcodes::builders::SetAccount;
26///
27/// # async fn example() {
28/// let surfnet = Surfnet::start().await.unwrap();
29/// let cheats = surfnet.cheatcodes();
30///
31/// // Fund an account with 5 SOL
32/// let alice: Pubkey = "...".parse().unwrap();
33/// cheats.fund_sol(&alice, 5_000_000_000).unwrap();
34///
35/// // Fund a token account with 1000 USDC
36/// let usdc_mint: Pubkey = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".parse().unwrap();
37/// cheats.fund_token(&alice, &usdc_mint, 1_000_000_000, None).unwrap();
38///
39/// // Or build a typed cheatcode request:
40/// let custom = Pubkey::new_unique();
41/// let owner = Pubkey::new_unique();
42/// cheats
43/// .execute(
44/// SetAccount::new(custom)
45/// .lamports(42)
46/// .owner(owner)
47/// .data(vec![1, 2, 3]),
48/// )
49/// .unwrap();
50/// # }
51/// ```
52pub struct Cheatcodes<'a> {
53 rpc_url: &'a str,
54}
55
56impl<'a> Cheatcodes<'a> {
57 pub(crate) fn new(rpc_url: &'a str) -> Self {
58 Self { rpc_url }
59 }
60
61 fn rpc_client(&self) -> RpcClient {
62 RpcClient::new(self.rpc_url)
63 }
64
65 /// Set the SOL balance for an account in lamports.
66 ///
67 /// ```rust
68 /// use surfpool_sdk::{Pubkey, Surfnet};
69 ///
70 /// # async fn example() {
71 /// let surfnet = Surfnet::start().await.unwrap();
72 /// let cheats = surfnet.cheatcodes();
73 /// let recipient = Pubkey::new_unique();
74 ///
75 /// cheats.fund_sol(&recipient, 1_000_000_000).unwrap();
76 /// # }
77 /// ```
78 pub fn fund_sol(&self, address: &Pubkey, lamports: u64) -> SurfnetResult<()> {
79 let params = serde_json::json!([
80 address.to_string(),
81 { "lamports": lamports }
82 ]);
83 self.call_cheatcode("surfnet_setAccount", params)
84 }
85
86 /// Set arbitrary account state for a single account.
87 ///
88 /// This helper updates lamports, owner, and raw account data in one RPC call.
89 ///
90 /// ```rust
91 /// use surfpool_sdk::{Pubkey, Surfnet};
92 ///
93 /// # async fn example() {
94 /// let surfnet = Surfnet::start().await.unwrap();
95 /// let cheats = surfnet.cheatcodes();
96 /// let address = Pubkey::new_unique();
97 /// let owner = Pubkey::new_unique();
98 ///
99 /// cheats.set_account(&address, 500, &[1, 2, 3], &owner).unwrap();
100 /// # }
101 /// ```
102 pub fn set_account(
103 &self,
104 address: &Pubkey,
105 lamports: u64,
106 data: &[u8],
107 owner: &Pubkey,
108 ) -> SurfnetResult<()> {
109 let params = serde_json::json!([
110 address.to_string(),
111 {
112 "lamports": lamports,
113 "data": hex::encode(data),
114 "owner": owner.to_string()
115 }
116 ]);
117 self.call_cheatcode("surfnet_setAccount", params)
118 }
119
120 /// Fund a token account (creates the ATA if needed).
121 ///
122 /// Uses `spl_token` program by default. Pass `token_program` to use Token-2022.
123 ///
124 /// ```rust
125 /// use surfpool_sdk::{Pubkey, Surfnet};
126 ///
127 /// # async fn example() {
128 /// let surfnet = Surfnet::start().await.unwrap();
129 /// let cheats = surfnet.cheatcodes();
130 /// let owner = Pubkey::new_unique();
131 /// let mint = Pubkey::new_unique();
132 ///
133 /// cheats.fund_token(&owner, &mint, 1_000, None).unwrap();
134 /// # }
135 /// ```
136 pub fn fund_token(
137 &self,
138 owner: &Pubkey,
139 mint: &Pubkey,
140 amount: u64,
141 token_program: Option<&Pubkey>,
142 ) -> SurfnetResult<()> {
143 let program = token_program.copied().unwrap_or(spl_token_program_id());
144 let params = serde_json::json!([
145 owner.to_string(),
146 mint.to_string(),
147 { "amount": amount },
148 program.to_string()
149 ]);
150 self.call_cheatcode("surfnet_setTokenAccount", params)
151 }
152
153 /// Set the token balance for a wallet/mint pair.
154 ///
155 /// This is an alias for [`Self::fund_token`].
156 ///
157 /// ```rust
158 /// use surfpool_sdk::{Pubkey, Surfnet};
159 ///
160 /// # async fn example() {
161 /// let surfnet = Surfnet::start().await.unwrap();
162 /// let cheats = surfnet.cheatcodes();
163 /// let owner = Pubkey::new_unique();
164 /// let mint = Pubkey::new_unique();
165 ///
166 /// cheats.set_token_balance(&owner, &mint, 5_000, None).unwrap();
167 /// # }
168 /// ```
169 pub fn set_token_balance(
170 &self,
171 owner: &Pubkey,
172 mint: &Pubkey,
173 amount: u64,
174 token_program: Option<&Pubkey>,
175 ) -> SurfnetResult<()> {
176 self.fund_token(owner, mint, amount, token_program)
177 }
178
179 /// Get the associated token address for a wallet/mint pair.
180 ///
181 /// ```rust
182 /// use surfpool_sdk::{Pubkey, Surfnet};
183 ///
184 /// # async fn example() {
185 /// let surfnet = Surfnet::start().await.unwrap();
186 /// let cheats = surfnet.cheatcodes();
187 /// let owner = Pubkey::new_unique();
188 /// let mint = Pubkey::new_unique();
189 ///
190 /// let ata = cheats.get_ata(&owner, &mint, None);
191 /// println!("{ata}");
192 /// # }
193 /// ```
194 pub fn get_ata(&self, owner: &Pubkey, mint: &Pubkey, token_program: Option<&Pubkey>) -> Pubkey {
195 let program = token_program.copied().unwrap_or(spl_token_program_id());
196 get_associated_token_address_with_program_id(owner, mint, &program)
197 }
198
199 /// Fund multiple accounts with SOL using repeated `surfnet_setAccount` calls.
200 ///
201 /// ```rust
202 /// use surfpool_sdk::{Pubkey, Surfnet};
203 ///
204 /// # async fn example() {
205 /// let surfnet = Surfnet::start().await.unwrap();
206 /// let cheats = surfnet.cheatcodes();
207 /// let alice = Pubkey::new_unique();
208 /// let bob = Pubkey::new_unique();
209 ///
210 /// cheats
211 /// .fund_sol_many(&[(&alice, 1_000_000), (&bob, 2_000_000)])
212 /// .unwrap();
213 /// # }
214 /// ```
215 pub fn fund_sol_many(&self, accounts: &[(&Pubkey, u64)]) -> SurfnetResult<()> {
216 for (address, lamports) in accounts {
217 self.fund_sol(address, *lamports)?;
218 }
219 Ok(())
220 }
221
222 /// Fund multiple wallets with the same token and amount.
223 ///
224 /// ```rust
225 /// use surfpool_sdk::{Pubkey, Surfnet};
226 ///
227 /// # async fn example() {
228 /// let surfnet = Surfnet::start().await.unwrap();
229 /// let cheats = surfnet.cheatcodes();
230 /// let alice = Pubkey::new_unique();
231 /// let bob = Pubkey::new_unique();
232 /// let mint = Pubkey::new_unique();
233 ///
234 /// cheats
235 /// .fund_token_many(&[&alice, &bob], &mint, 1_000, None)
236 /// .unwrap();
237 /// # }
238 /// ```
239 pub fn fund_token_many(
240 &self,
241 owners: &[&Pubkey],
242 mint: &Pubkey,
243 amount: u64,
244 token_program: Option<&Pubkey>,
245 ) -> SurfnetResult<()> {
246 for owner in owners {
247 self.fund_token(owner, mint, amount, token_program)?;
248 }
249 Ok(())
250 }
251
252 /// Move Surfnet time forward to an absolute epoch.
253 ///
254 /// ```rust
255 /// use surfpool_sdk::Surfnet;
256 ///
257 /// # async fn example() {
258 /// let surfnet = Surfnet::start().await.unwrap();
259 /// let cheats = surfnet.cheatcodes();
260 ///
261 /// let epoch_info = cheats.time_travel_to_epoch(10).unwrap();
262 /// assert!(epoch_info.epoch >= 10);
263 /// # }
264 /// ```
265 pub fn time_travel_to_epoch(&self, epoch: u64) -> SurfnetResult<EpochInfo> {
266 self.time_travel(serde_json::json!([{ "absoluteEpoch": epoch }]))
267 }
268
269 /// Move Surfnet time forward to an absolute slot.
270 ///
271 /// ```rust
272 /// use surfpool_sdk::Surfnet;
273 ///
274 /// # async fn example() {
275 /// let surfnet = Surfnet::start().await.unwrap();
276 /// let cheats = surfnet.cheatcodes();
277 ///
278 /// let epoch_info = cheats.time_travel_to_slot(1_000).unwrap();
279 /// assert!(epoch_info.absolute_slot >= 1_000);
280 /// # }
281 /// ```
282 pub fn time_travel_to_slot(&self, slot: u64) -> SurfnetResult<EpochInfo> {
283 self.time_travel(serde_json::json!([{ "absoluteSlot": slot }]))
284 }
285
286 /// Move Surfnet time forward to an absolute Unix timestamp in milliseconds.
287 ///
288 /// ```rust
289 /// use surfpool_sdk::Surfnet;
290 ///
291 /// # async fn example() {
292 /// let surfnet = Surfnet::start().await.unwrap();
293 /// let cheats = surfnet.cheatcodes();
294 ///
295 /// let epoch_info = cheats.time_travel_to_timestamp(1_700_000_000_000).unwrap();
296 /// assert!(epoch_info.absolute_slot > 0);
297 /// # }
298 /// ```
299 pub fn time_travel_to_timestamp(&self, timestamp: u64) -> SurfnetResult<EpochInfo> {
300 self.time_travel(serde_json::json!([{ "absoluteTimestamp": timestamp }]))
301 }
302
303 /// Deploy a program from local workspace artifacts.
304 ///
305 /// This looks for:
306 /// - `target/deploy/{program_name}.so`
307 /// - `target/deploy/{program_name}-keypair.json`
308 /// - `target/idl/{program_name}.json` (optional)
309 ///
310 /// If an IDL file exists, it is registered after the program bytes are written.
311 ///
312 /// ```rust
313 /// use surfpool_sdk::Surfnet;
314 ///
315 /// # async fn example() {
316 /// let surfnet = Surfnet::start().await.unwrap();
317 /// let cheats = surfnet.cheatcodes();
318 ///
319 /// let program_id = cheats.deploy_program("my_program").unwrap();
320 /// println!("{program_id}");
321 /// # }
322 /// ```
323 pub fn deploy_program(&self, program_name: &str) -> SurfnetResult<Pubkey> {
324 let target_dir = resolve_target_dir(program_name)?;
325 let deploy_dir = target_dir.join("deploy");
326 let idl_dir = target_dir.join("idl");
327 let so_path = deploy_dir.join(format!("{program_name}.so"));
328 let keypair_path = deploy_dir.join(format!("{program_name}-keypair.json"));
329 let idl_path = idl_dir.join(format!("{program_name}.json"));
330
331 let builder = DeployProgram::from_keypair_path(&keypair_path)?
332 .so_path(so_path)
333 .idl_path_if_exists(idl_path);
334
335 self.deploy(builder)
336 }
337
338 /// Deploy a program described by a [`DeployProgram`] builder.
339 ///
340 /// This writes the program bytes with `surfnet_writeProgram` and, when present,
341 /// registers the parsed IDL with `surfnet_registerIdl`.
342 ///
343 /// ```rust
344 /// use surfpool_sdk::{Pubkey, Surfnet};
345 /// use surfpool_sdk::cheatcodes::builders::DeployProgram;
346 ///
347 /// # async fn example() {
348 /// let surfnet = Surfnet::start().await.unwrap();
349 /// let cheats = surfnet.cheatcodes();
350 /// let program_id = Pubkey::new_unique();
351 ///
352 /// let deployed_program = cheats
353 /// .deploy(
354 /// DeployProgram::new(program_id)
355 /// .so_path("target/deploy/my_program.so")
356 /// .idl_path("target/idl/my_program.json"),
357 /// )
358 /// .unwrap();
359 ///
360 /// assert_eq!(deployed_program, program_id);
361 /// # }
362 /// ```
363 pub fn deploy(&self, builder: DeployProgram) -> SurfnetResult<Pubkey> {
364 let program_id = builder.program_id();
365 let program_bytes = builder.load_so_bytes()?;
366 self.write_program(&program_id, &program_bytes)?;
367
368 if let Some(mut idl) = builder.load_idl()? {
369 idl.address = program_id.to_string();
370 self.register_idl(&idl)?;
371 }
372
373 Ok(program_id)
374 }
375
376 /// Execute a typed cheatcode builder.
377 ///
378 /// ```rust
379 /// use surfpool_sdk::{Pubkey, Surfnet};
380 /// use surfpool_sdk::cheatcodes::builders::ResetAccount;
381 ///
382 /// # async fn example() {
383 /// let surfnet = Surfnet::start().await.unwrap();
384 /// let cheats = surfnet.cheatcodes();
385 /// let address = Pubkey::new_unique();
386 ///
387 /// cheats.execute(ResetAccount::new(address)).unwrap();
388 /// # }
389 /// ```
390 pub fn execute<B: CheatcodeBuilder>(&self, builder: B) -> SurfnetResult<()> {
391 self.call_cheatcode(B::METHOD, builder.build())
392 }
393
394 /// Internal helper for `surfnet_timeTravel` requests that return [`EpochInfo`].
395 fn time_travel(&self, params: serde_json::Value) -> SurfnetResult<EpochInfo> {
396 let client = self.rpc_client();
397 client
398 .send::<EpochInfo>(
399 RpcRequest::Custom {
400 method: "surfnet_timeTravel",
401 },
402 params,
403 )
404 .map_err(|e| SurfnetError::Cheatcode(format!("surfnet_timeTravel: {e}")))
405 }
406
407 fn write_program(&self, program_id: &Pubkey, data: &[u8]) -> SurfnetResult<()> {
408 const PROGRAM_CHUNK_BYTES: usize = 15 * 1024 * 1024;
409
410 for (index, chunk) in data.chunks(PROGRAM_CHUNK_BYTES).enumerate() {
411 let offset = index * PROGRAM_CHUNK_BYTES;
412 let params = serde_json::json!([program_id.to_string(), hex::encode(chunk), offset,]);
413 self.call_cheatcode("surfnet_writeProgram", params)?;
414 }
415
416 Ok(())
417 }
418
419 fn register_idl(&self, idl: &surfpool_types::Idl) -> SurfnetResult<()> {
420 let client = self.rpc_client();
421 client
422 .send::<serde_json::Value>(
423 RpcRequest::Custom {
424 method: "surfnet_registerIdl",
425 },
426 serde_json::json!([idl]),
427 )
428 .map_err(|e| SurfnetError::Cheatcode(format!("surfnet_registerIdl: {e}")))?;
429 Ok(())
430 }
431
432 /// Internal helper for cheatcodes that return `()`.
433 fn call_cheatcode(&self, method: &'static str, params: serde_json::Value) -> SurfnetResult<()> {
434 let client = self.rpc_client();
435 client
436 .send::<serde_json::Value>(RpcRequest::Custom { method }, params)
437 .map_err(|e| SurfnetError::Cheatcode(format!("{method}: {e}")))?;
438 Ok(())
439 }
440}
441
442fn spl_token_program_id() -> Pubkey {
443 // spl_token::id() = TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
444 Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
445}
446
447fn read_keypair_pubkey(path: &Path) -> SurfnetResult<Pubkey> {
448 Keypair::read_from_file(path)
449 .map(|keypair| keypair.pubkey())
450 .map_err(|e| {
451 SurfnetError::Cheatcode(format!(
452 "failed to read deploy keypair from {}: {e}",
453 path.display()
454 ))
455 })
456}
457
458fn resolve_target_dir(program_name: &str) -> SurfnetResult<PathBuf> {
459 if let Ok(explicit_target_dir) = env::var("CARGO_TARGET_DIR") {
460 let target_dir = PathBuf::from(explicit_target_dir);
461 if has_program_artifacts(&target_dir, program_name) {
462 return Ok(target_dir);
463 }
464 }
465
466 let current_dir = env::current_dir().map_err(|e| {
467 SurfnetError::Cheatcode(format!("failed to resolve current working directory: {e}"))
468 })?;
469
470 for ancestor in current_dir.ancestors() {
471 let target_dir = ancestor.join("target");
472 if has_program_artifacts(&target_dir, program_name) {
473 return Ok(target_dir);
474 }
475 }
476
477 Err(SurfnetError::Cheatcode(format!(
478 "failed to locate target/deploy artifacts for program `{program_name}` starting from {}",
479 current_dir.display()
480 )))
481}
482
483fn has_program_artifacts(target_dir: &Path, program_name: &str) -> bool {
484 target_dir
485 .join("deploy")
486 .join(format!("{program_name}.so"))
487 .exists()
488 && target_dir
489 .join("deploy")
490 .join(format!("{program_name}-keypair.json"))
491 .exists()
492}
493
494#[cfg(test)]
495mod tests;