Skip to main content

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;