mollusk_helper/
context.rs

1use crate::account;
2use crate::account_store::InMemoryAccountStore;
3use crate::error::{MolluskHelperError, Result};
4use crate::token;
5use crate::transaction::TransactionBuilder;
6use mollusk_svm::account_store::AccountStore;
7use mollusk_svm::result::{InstructionResult, ProgramResult};
8use mollusk_svm::{Mollusk, MolluskContext};
9use solana_account::Account;
10use solana_address::Address;
11use solana_hash::Hash;
12use solana_instruction::Instruction;
13use solana_keypair::Keypair;
14use solana_message::{v0, AddressLookupTableAccount, VersionedMessage};
15use solana_program_pack::Pack;
16use solana_pubkey::Pubkey;
17use solana_signer::Signer;
18use spl_token::state::Account as TokenAccount;
19use std::collections::HashMap;
20use std::sync::{Arc, RwLock};
21
22pub const MEMO_PROGRAM_ID: Pubkey =
23    solana_pubkey::pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
24
25pub const MEMO_V1_PROGRAM_ID: Pubkey =
26    solana_pubkey::pubkey!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo");
27
28pub const TOKEN_2022_PROGRAM_ID: Pubkey =
29    solana_pubkey::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
30
31pub const ADDRESS_LOOKUP_TABLE_PROGRAM_ID: Pubkey =
32    solana_pubkey::pubkey!("AddressLookupTab1e1111111111111111111111111");
33
34pub const COMPUTE_BUDGET_PROGRAM_ID: Pubkey =
35    solana_pubkey::pubkey!("ComputeBudget111111111111111111111111111111");
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ProgramLoader {
39    V2,
40    V3,
41}
42
43fn add_default_programs(mollusk: &mut Mollusk) {
44    mollusk_svm_programs_token::token::add_program(mollusk);
45    mollusk_svm_programs_token::associated_token::add_program(mollusk);
46    mollusk_svm_programs_token::token2022::add_program(mollusk);
47    mollusk_svm_programs_memo::memo::add_program(mollusk);
48    mollusk_svm_programs_memo::memo_v1::add_program(mollusk);
49}
50
51pub struct MolluskContextHelper {
52    pub(crate) context: MolluskContext<InMemoryAccountStore>,
53    keypairs: Arc<RwLock<HashMap<String, Keypair>>>,
54}
55
56impl MolluskContextHelper {
57    pub fn new(program_id: &Pubkey, elf_bytes: &[u8]) -> Self {
58        Self::new_with_options(
59            program_id,
60            elf_bytes,
61            ProgramLoader::V3,
62            Self::current_unix_timestamp(),
63        )
64    }
65
66    pub fn new_with_timestamp(program_id: &Pubkey, elf_bytes: &[u8], unix_timestamp: u64) -> Self {
67        Self::new_with_options(program_id, elf_bytes, ProgramLoader::V3, unix_timestamp)
68    }
69
70    pub fn new_with_loader(program_id: &Pubkey, elf_bytes: &[u8], loader: ProgramLoader) -> Self {
71        Self::new_with_options(
72            program_id,
73            elf_bytes,
74            loader,
75            Self::current_unix_timestamp(),
76        )
77    }
78
79    pub fn new_with_options(
80        program_id: &Pubkey,
81        elf_bytes: &[u8],
82        loader: ProgramLoader,
83        unix_timestamp: u64,
84    ) -> Self {
85        let mut mollusk = Mollusk::default();
86
87        let loader_key = match loader {
88            ProgramLoader::V2 => &mollusk_svm::program::loader_keys::LOADER_V2,
89            ProgramLoader::V3 => &mollusk_svm::program::loader_keys::LOADER_V3,
90        };
91
92        mollusk.add_program_with_loader_and_elf(
93            &Self::pubkey_to_address(program_id),
94            loader_key,
95            elf_bytes,
96        );
97
98        add_default_programs(&mut mollusk);
99        mollusk.sysvars.clock.unix_timestamp = unix_timestamp as i64;
100
101        let store = InMemoryAccountStore::new();
102        let context = mollusk.with_context(store);
103
104        Self {
105            context,
106            keypairs: Arc::new(RwLock::new(HashMap::new())),
107        }
108    }
109
110    pub fn new_without_program() -> Self {
111        Self::new_without_program_with_timestamp(Self::current_unix_timestamp())
112    }
113
114    pub fn new_without_program_with_timestamp(unix_timestamp: u64) -> Self {
115        let mut mollusk = Mollusk::default();
116
117        add_default_programs(&mut mollusk);
118        mollusk.sysvars.clock.unix_timestamp = unix_timestamp as i64;
119
120        let store = InMemoryAccountStore::new();
121        let context = mollusk.with_context(store);
122
123        Self {
124            context,
125            keypairs: Arc::new(RwLock::new(HashMap::new())),
126        }
127    }
128
129    pub fn add_program(&mut self, program_id: &Pubkey, elf_bytes: &[u8]) {
130        self.add_program_with_loader(program_id, elf_bytes, ProgramLoader::V3);
131    }
132
133    pub fn add_program_with_loader(
134        &mut self,
135        program_id: &Pubkey,
136        elf_bytes: &[u8],
137        loader: ProgramLoader,
138    ) {
139        let loader_key = match loader {
140            ProgramLoader::V2 => &mollusk_svm::program::loader_keys::LOADER_V2,
141            ProgramLoader::V3 => &mollusk_svm::program::loader_keys::LOADER_V3,
142        };
143
144        self.context.mollusk.add_program_with_loader_and_elf(
145            &Self::pubkey_to_address(program_id),
146            loader_key,
147            elf_bytes,
148        );
149    }
150
151    pub fn current_unix_timestamp() -> u64 {
152        std::time::SystemTime::now()
153            .duration_since(std::time::UNIX_EPOCH)
154            .expect("Time went backwards")
155            .as_secs()
156    }
157
158    pub fn process_instruction(&self, instruction: &Instruction) -> Result<InstructionResult> {
159        let result = self.context.process_instruction(instruction);
160        match &result.program_result {
161            ProgramResult::Success => Ok(result),
162            ProgramResult::Failure(e) => Err(MolluskHelperError::ProgramError(e.clone())),
163            ProgramResult::UnknownError(e) => Err(MolluskHelperError::InstructionFailed(e.clone())),
164        }
165    }
166
167    pub fn process_instruction_unchecked(&self, instruction: &Instruction) -> InstructionResult {
168        self.context.process_instruction(instruction)
169    }
170
171    pub(crate) fn process_instruction_internal(
172        &self,
173        instruction: &Instruction,
174    ) -> InstructionResult {
175        self.context.process_instruction(instruction)
176    }
177
178    pub fn transaction(&self) -> TransactionBuilder<'_> {
179        TransactionBuilder::new(self)
180    }
181
182    pub(crate) fn snapshot_accounts(&self) -> HashMap<Address, Account> {
183        self.context.account_store.borrow().snapshot()
184    }
185
186    pub(crate) fn restore_accounts(&self, snapshot: HashMap<Address, Account>) {
187        self.context.account_store.borrow_mut().restore(snapshot);
188    }
189
190    pub fn add_account(&self, pubkey: &Pubkey, account: Account) {
191        let address = Self::pubkey_to_address(pubkey);
192        self.context
193            .account_store
194            .borrow_mut()
195            .add_account(address, account);
196    }
197
198    pub fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
199        let address = Self::pubkey_to_address(pubkey);
200        self.context.account_store.borrow().get_account(&address)
201    }
202
203    pub fn get_balance(&self, pubkey: &Pubkey) -> Option<u64> {
204        let address = Self::pubkey_to_address(pubkey);
205        self.context.account_store.borrow().get_balance(&address)
206    }
207
208    pub fn fund_account(&self, pubkey: &Pubkey, lamports: u64) {
209        let account = account::system_account_with_lamports(lamports);
210        self.add_account(pubkey, account);
211    }
212
213    pub fn store_keypair(&self, name: &str, keypair: Keypair) -> Result<()> {
214        self.keypairs
215            .write()
216            .map_err(|_| MolluskHelperError::LockError)?
217            .insert(name.to_string(), keypair);
218        Ok(())
219    }
220
221    pub fn sign_with(&self, name: &str, message: &[u8]) -> Result<[u8; 64]> {
222        let keypairs = self
223            .keypairs
224            .read()
225            .map_err(|_| MolluskHelperError::LockError)?;
226        let keypair = keypairs
227            .get(name)
228            .ok_or_else(|| MolluskHelperError::KeypairNotFound(name.to_string()))?;
229        Ok(keypair.sign_message(message).into())
230    }
231
232    pub fn get_keypair_pubkey(&self, name: &str) -> Result<Pubkey> {
233        let keypairs = self
234            .keypairs
235            .read()
236            .map_err(|_| MolluskHelperError::LockError)?;
237        let keypair = keypairs
238            .get(name)
239            .ok_or_else(|| MolluskHelperError::KeypairNotFound(name.to_string()))?;
240        Ok(keypair.pubkey())
241    }
242
243    pub fn update_unix_timestamp(&mut self, timestamp: i64) {
244        self.context.mollusk.sysvars.clock.unix_timestamp = timestamp;
245    }
246
247    pub fn get_unix_timestamp(&self) -> i64 {
248        self.context.mollusk.sysvars.clock.unix_timestamp
249    }
250
251    pub fn warp_to_slot(&mut self, slot: u64) {
252        self.context.mollusk.warp_to_slot(slot);
253    }
254
255    pub fn create_mint(&self, mint_pubkey: &Pubkey, authority: &Pubkey, decimals: u8) {
256        let account = token::create_mint_account(authority, decimals);
257        self.add_account(mint_pubkey, account);
258    }
259
260    pub fn create_token_account(
261        &self,
262        token_account_pubkey: &Pubkey,
263        mint: &Pubkey,
264        owner: &Pubkey,
265        amount: u64,
266    ) {
267        let account = token::create_token_account(mint, owner, amount);
268        self.add_account(token_account_pubkey, account);
269    }
270
271    pub fn create_native_token_account(
272        &self,
273        token_account_pubkey: &Pubkey,
274        owner: &Pubkey,
275        lamports: u64,
276    ) {
277        let account = token::create_native_token_account(owner, lamports);
278        self.add_account(token_account_pubkey, account);
279    }
280
281    pub fn get_token_balance(&self, token_account_pubkey: &Pubkey) -> Result<u64> {
282        let account = self
283            .get_account(token_account_pubkey)
284            .ok_or_else(|| MolluskHelperError::AccountNotFound(token_account_pubkey.to_string()))?;
285        let token_account =
286            TokenAccount::unpack(&account.data).map_err(MolluskHelperError::ProgramError)?;
287        Ok(token_account.amount)
288    }
289
290    pub fn mint_to(
291        &self,
292        mint: &Pubkey,
293        destination: &Pubkey,
294        authority: &Pubkey,
295        amount: u64,
296    ) -> Result<InstructionResult> {
297        let ix = token::mint_to_instruction(mint, destination, authority, amount);
298        self.process_instruction(&ix)
299    }
300
301    pub fn transfer_tokens(
302        &self,
303        source: &Pubkey,
304        destination: &Pubkey,
305        authority: &Pubkey,
306        amount: u64,
307    ) -> Result<InstructionResult> {
308        let ix = token::transfer_instruction(source, destination, authority, amount);
309        self.process_instruction(&ix)
310    }
311
312    pub fn sync_native(&self, token_account: &Pubkey) -> Result<InstructionResult> {
313        let ix = token::sync_native_instruction(token_account);
314        self.process_instruction(&ix)
315    }
316
317    pub fn transfer_sol(
318        &self,
319        from: &Pubkey,
320        to: &Pubkey,
321        lamports: u64,
322    ) -> Result<InstructionResult> {
323        let ix = solana_system_interface::instruction::transfer(from, to, lamports);
324        self.process_instruction(&ix)
325    }
326
327    pub fn get_associated_token_address(&self, wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
328        token::get_associated_token_address(wallet, mint)
329    }
330
331    pub fn create_lookup_table_account(
332        key: Pubkey,
333        addresses: Vec<Pubkey>,
334    ) -> AddressLookupTableAccount {
335        AddressLookupTableAccount { key, addresses }
336    }
337
338    pub fn build_versioned_tx_size(
339        payer: &Pubkey,
340        instructions: &[Instruction],
341        lookup_tables: &[AddressLookupTableAccount],
342    ) -> (usize, usize, i64) {
343        let recent_blockhash = Hash::default();
344        let message =
345            v0::Message::try_compile(payer, instructions, lookup_tables, recent_blockhash)
346                .expect("Failed to compile v0 message");
347        let versioned_message = VersionedMessage::V0(message);
348
349        let num_signers = instructions
350            .iter()
351            .flat_map(|ix| ix.accounts.iter())
352            .filter(|a| a.is_signer)
353            .map(|a| a.pubkey)
354            .collect::<std::collections::HashSet<_>>()
355            .len()
356            .max(1);
357
358        let message_bytes =
359            bincode::serialize(&versioned_message).expect("Failed to serialize message");
360        let tx_size = 1 + (num_signers * 64) + message_bytes.len();
361        let tx_limit: usize = 1232;
362        let remaining = tx_limit as i64 - tx_size as i64;
363
364        (tx_size, tx_limit, remaining)
365    }
366
367    pub fn pubkey_to_address(pubkey: &Pubkey) -> Address {
368        Address::new_from_array(pubkey.to_bytes())
369    }
370
371    pub fn address_to_pubkey(address: &Address) -> Pubkey {
372        Pubkey::from(address.to_bytes())
373    }
374
375    pub fn system_program() -> Pubkey {
376        solana_pubkey::pubkey!("11111111111111111111111111111111")
377    }
378
379    pub fn token_program() -> Pubkey {
380        token::TOKEN_PROGRAM_ID
381    }
382
383    pub fn token_2022_program() -> Pubkey {
384        TOKEN_2022_PROGRAM_ID
385    }
386
387    pub fn associated_token_program() -> Pubkey {
388        token::ASSOCIATED_TOKEN_PROGRAM_ID
389    }
390
391    pub fn memo_program() -> Pubkey {
392        MEMO_PROGRAM_ID
393    }
394
395    pub fn memo_v1_program() -> Pubkey {
396        MEMO_V1_PROGRAM_ID
397    }
398
399    pub fn address_lookup_table_program() -> Pubkey {
400        ADDRESS_LOOKUP_TABLE_PROGRAM_ID
401    }
402
403    pub fn compute_budget_program() -> Pubkey {
404        COMPUTE_BUDGET_PROGRAM_ID
405    }
406
407    pub fn native_mint() -> Pubkey {
408        token::NATIVE_MINT
409    }
410
411    pub fn rent_sysvar() -> Pubkey {
412        solana_pubkey::pubkey!("SysvarRent111111111111111111111111111111111")
413    }
414
415    pub fn add_program_account(&self, pubkey: &Pubkey, owner: &Pubkey, data: Vec<u8>) {
416        let account = account::program_account(owner, data);
417        self.add_account(pubkey, account);
418    }
419
420    pub fn create_associated_token_account_instruction(
421        payer: &Pubkey,
422        wallet: &Pubkey,
423        mint: &Pubkey,
424    ) -> Instruction {
425        token::create_associated_token_account_instruction(payer, wallet, mint)
426    }
427}