crm_cli_utils/
client.rs

1use anchor_lang::{
2    prelude::{ProgramError, Pubkey},
3    AccountDeserialize, Discriminator,
4};
5use regex::Regex;
6use solana_account_decoder::UiAccountEncoding;
7use solana_clap_utils::keypair::DefaultSigner;
8use solana_cli_config::{Config, ConfigInput};
9use solana_client::{
10    client_error::ClientError as SolanaClientError,
11    pubsub_client::PubsubClientError,
12    rpc_client::RpcClient,
13    rpc_config::{
14        RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcSendTransactionConfig,
15        RpcSimulateTransactionAccountsConfig, RpcSimulateTransactionConfig,
16    },
17    rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
18    rpc_response::RpcSimulateTransactionResult,
19};
20use solana_program::instruction::Instruction;
21use solana_remote_wallet::remote_wallet::RemoteWalletManager;
22use solana_sdk::{
23    account::Account,
24    bs58,
25    commitment_config::{CommitmentConfig, CommitmentLevel},
26    signer::Signer,
27    transaction::Transaction,
28};
29use std::{io, iter::Map, sync::Arc, time::Duration, vec::IntoIter};
30use tabled::Tabled;
31use thiserror::Error;
32
33use crate::render::render_object;
34
35const PROGRAM_LOG: &str = "Program log: ";
36const PROGRAM_DATA: &str = "Program data: ";
37
38pub struct ProgramAccountsIterator<T> {
39    inner: Map<IntoIter<(Pubkey, Account)>, AccountConverterFunction<T>>,
40}
41
42/// Function type that accepts solana accounts and returns deserialized anchor accounts
43type AccountConverterFunction<T> = fn((Pubkey, Account)) -> Result<(Pubkey, T), ClientError>;
44
45impl<T> Iterator for ProgramAccountsIterator<T> {
46    type Item = Result<(Pubkey, T), ClientError>;
47
48    fn next(&mut self) -> Option<Self::Item> {
49        self.inner.next()
50    }
51}
52
53#[derive(Debug, Error)]
54pub enum ClientError {
55    #[error("Account not found")]
56    AccountNotFound,
57    #[error("{0}")]
58    AnchorError(#[from] anchor_lang::error::Error),
59    #[error("{0}")]
60    ProgramError(#[from] ProgramError),
61    #[error("{0}")]
62    SolanaClientError(#[from] SolanaClientError),
63    #[error("{0}")]
64    SolanaClientPubsubError(#[from] PubsubClientError),
65    #[error("Unable to parse log: {0}")]
66    LogParseError(String),
67    #[error("Unable to parse event")]
68    EventParseError,
69    #[error("Simulate error{0}")]
70    SimulateError(String),
71}
72
73pub struct SimulateResult {
74    result: RpcSimulateTransactionResult,
75    lamports: u64,
76}
77
78pub struct Client {
79    pub config_path: String,
80    pub config: Config,
81    pub rpc_timeout: Duration,
82    pub commitment: CommitmentConfig,
83    pub confirm_transaction_initial_timeout: Duration,
84    //pub payer: Rc<dyn Signer>,
85    pub signers: Vec<Box<dyn Signer>>,
86}
87
88impl Client {
89    pub fn new(config_path: String) -> Result<Self, io::Error> {
90        let mut config = Config::load(config_path.as_str())?;
91        let rpc_timeout = Duration::from_secs(30);
92
93        if config.websocket_url.is_empty() {
94            config.websocket_url = Config::compute_websocket_url(&config.json_rpc_url);
95        }
96
97        let commitment = CommitmentConfig {
98            commitment: CommitmentLevel::Processed,
99        };
100        let confirm_transaction_initial_timeout = Duration::from_secs(30);
101        let default_signer_arg_name = "keypair".to_string();
102        let (_, default_signer_path) =
103            ConfigInput::compute_keypair_path_setting("", &config.keypair_path);
104        let default_signer = DefaultSigner::new(default_signer_arg_name, &default_signer_path);
105        let mut wallet_manager: Option<Arc<RemoteWalletManager>> = None;
106        let payer = default_signer.signer_from_path(&Default::default(), &mut wallet_manager);
107
108        Ok(Client {
109            config_path,
110            config,
111            rpc_timeout,
112            commitment,
113            confirm_transaction_initial_timeout,
114            //payer: Rc::from(payer.unwrap()),
115            signers: vec![payer.unwrap()],
116        })
117    }
118
119    #[allow(dead_code)]
120    pub fn rpc_client(&self) -> RpcClient {
121        RpcClient::new_with_timeouts_and_commitment(
122            self.config.json_rpc_url.to_string(),
123            self.rpc_timeout,
124            self.commitment,
125            self.confirm_transaction_initial_timeout,
126        )
127    }
128
129    pub fn lamports(&self) -> u64 {
130        let lamports = self.rpc_client().get_balance(&self.payer_key());
131        lamports.unwrap()
132    }
133
134    pub fn payer_key(&self) -> Pubkey {
135        self.signers[0].pubkey()
136        //self.payer.pubkey()
137    }
138
139    /// Returns the account at the given address.
140    pub fn account<T: AccountDeserialize>(&self, address: Pubkey) -> Result<T, ClientError> {
141        let account = self
142            .rpc_client()
143            .get_account_with_commitment(&address, CommitmentConfig::processed())?
144            .value
145            .ok_or(ClientError::AccountNotFound)?;
146        let mut data: &[u8] = &account.data;
147        T::try_deserialize(&mut data).map_err(Into::into)
148    }
149
150    /// Returns all program accounts of the given type matching the given filters
151    pub fn accounts<T: AccountDeserialize + Discriminator>(
152        &self,
153        program_id: &Pubkey,
154        filters: Vec<RpcFilterType>,
155    ) -> Result<Vec<(Pubkey, T)>, ClientError> {
156        self.accounts_lazy(program_id, filters)?.collect()
157    }
158
159    /// Returns all program accounts of the given type matching the given filters as an iterator
160    /// Deserialization is executed lazily
161    pub fn accounts_lazy<T: AccountDeserialize + Discriminator>(
162        &self,
163        program_id: &Pubkey,
164        filters: Vec<RpcFilterType>,
165    ) -> Result<ProgramAccountsIterator<T>, ClientError> {
166        let account_type_filter = RpcFilterType::Memcmp(Memcmp {
167            offset: 0,
168            bytes: MemcmpEncodedBytes::Base58(bs58::encode(T::discriminator()).into_string()),
169            encoding: None,
170        });
171        let config = RpcProgramAccountsConfig {
172            filters: Some([vec![account_type_filter], filters].concat()),
173            account_config: RpcAccountInfoConfig {
174                encoding: Some(UiAccountEncoding::Base64),
175                ..RpcAccountInfoConfig::default()
176            },
177            ..RpcProgramAccountsConfig::default()
178        };
179        Ok(ProgramAccountsIterator {
180            inner: self
181                .rpc_client()
182                .get_program_accounts_with_config(program_id, config)?
183                .into_iter()
184                .map(|(key, account)| {
185                    Ok((key, T::try_deserialize(&mut (&account.data as &[u8]))?))
186                }),
187        })
188    }
189
190    pub fn send_and_confirm(&self, ixs: &[Instruction]) {
191        let rpc_client = self.rpc_client();
192        let recent_blockhash = rpc_client.get_latest_blockhash().unwrap();
193        //let mut signers = vec![];
194        //signers.push(&*self.payer);
195        let tx = Transaction::new_signed_with_payer(
196            ixs,
197            Some(&self.payer_key()),
198            &self.signers,
199            recent_blockhash,
200        );
201        let resp = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
202            &tx,
203            self.commitment,
204            RpcSendTransactionConfig {
205                skip_preflight: true,
206                preflight_commitment: None,
207                encoding: None,
208                max_retries: Some(3),
209                min_context_slot: None,
210            },
211        );
212        if resp.is_err() {
213            println!(
214                "{}",
215                resp.err()
216                    .unwrap()
217                    .get_transaction_error()
218                    .unwrap()
219                    .to_string()
220            );
221        } else {
222            println!("{}", resp.unwrap().to_string());
223        }
224    }
225
226    pub fn simulate(&self, ixs: &[Instruction]) -> Result<SimulateResult, ClientError> {
227        let rpc_client = self.rpc_client();
228        let recent_blockhash = rpc_client.get_latest_blockhash().unwrap();
229        //let mut signers = vec![];
230        //signers.push(&*self.payer);
231        let tx = Transaction::new_signed_with_payer(
232            ixs,
233            Some(&self.payer_key()),
234            &self.signers,
235            recent_blockhash,
236        );
237        let mut config = RpcSimulateTransactionConfig::default();
238        config.replace_recent_blockhash = true;
239        config.commitment = Some(self.commitment);
240        config.accounts = Some(RpcSimulateTransactionAccountsConfig {
241            encoding: Some(UiAccountEncoding::Base64),
242            addresses: vec![self.payer_key().to_string()],
243        });
244        let before_lamports = self.lamports();
245        let resp = rpc_client.simulate_transaction_with_config(&tx, config);
246        if resp.is_err() {
247            return Err(ClientError::SimulateError(resp.err().unwrap().to_string()));
248        }
249        let result = resp.unwrap().value;
250        let lamports = if result.accounts.is_some() {
251            let accounts = result.accounts.clone().unwrap();
252            if accounts[0].is_some() {
253                before_lamports - accounts[0].clone().unwrap().lamports
254            } else {
255                before_lamports
256            }
257        } else {
258            before_lamports
259        };
260        Ok(SimulateResult { result, lamports })
261    }
262
263    pub fn simulate_and_print<
264        T: anchor_lang::Event + anchor_lang::AnchorDeserialize + Tabled + Copy,
265    >(
266        &self,
267        program_id: &Pubkey,
268        ixs: &[Instruction],
269    ) -> Option<T> {
270        let resp = self.simulate(ixs);
271        if resp.is_err() {
272            println!("Simulate error:{:?}", resp.err());
273            None
274        } else {
275            let result = resp.unwrap();
276            let event_parsed: Result<T, ClientError> = result.parse_event(program_id);
277            if event_parsed.is_err() {
278                for log in result.result.logs.clone().unwrap() {
279                    println!("{}", log);
280                }
281                None
282            } else {
283                let event = event_parsed.unwrap();
284                println!(
285                    "{}",
286                    render_object(
287                        event,
288                        format!(
289                            "SimulateSwap\n fee_lamports:{} units:{}",
290                            result.lamports,
291                            result.result.units_consumed.unwrap()
292                        )
293                        .as_str(),
294                        None
295                    )
296                );
297                Some(event)
298            }
299        }
300    }
301}
302
303/// Anchor event parse trait
304pub trait EventParser {
305    fn parse_event<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
306        &self,
307        program_id: &Pubkey,
308    ) -> Result<T, ClientError>;
309}
310
311impl EventParser for SimulateResult {
312    fn parse_event<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
313        &self,
314        program_id: &Pubkey,
315    ) -> Result<T, ClientError> {
316        let mut logs = &self.result.logs.clone().unwrap()[..];
317        if logs.len() == 0 {
318            println!("{:?}", self.result);
319            return Err(ClientError::EventParseError);
320        }
321        let mut execution = Execution::new(&mut logs)?;
322        let self_program_str = program_id.to_string();
323
324        for l in logs {
325            // Parse the log.
326            let (event, new_program, did_pop) = {
327                if self_program_str == execution.program() {
328                    handle_program_log(&self_program_str, l).unwrap_or_else(|e| {
329                        println!("Unable to parse log: {}", e);
330                        std::process::exit(1);
331                    })
332                } else {
333                    let (program, did_pop) = handle_system_log(&self_program_str, l);
334                    (None, program, did_pop)
335                }
336            };
337            // Emit the event.
338            if let Some(e) = event {
339                return Ok(e);
340                //f(&ctx, e);
341            }
342            // Switch program context on CPI.
343            if let Some(new_program) = new_program {
344                execution.push(new_program);
345            }
346            // Program returned.
347            if did_pop {
348                execution.pop();
349            }
350        }
351        Err(ClientError::EventParseError)
352    }
353}
354
355struct Execution {
356    stack: Vec<String>,
357}
358
359impl Execution {
360    pub fn new(logs: &mut &[String]) -> Result<Self, ClientError> {
361        let l = &logs[0];
362        *logs = &logs[1..];
363
364        let re = Regex::new(r"^Program (.*) invoke.*$").unwrap();
365        let c = re
366            .captures(l)
367            .ok_or_else(|| ClientError::LogParseError(l.to_string()))?;
368        let program = c
369            .get(1)
370            .ok_or_else(|| ClientError::LogParseError(l.to_string()))?
371            .as_str()
372            .to_string();
373        Ok(Self {
374            stack: vec![program],
375        })
376    }
377
378    pub fn program(&self) -> String {
379        assert!(!self.stack.is_empty());
380        self.stack[self.stack.len() - 1].clone()
381    }
382
383    pub fn push(&mut self, new_program: String) {
384        self.stack.push(new_program);
385    }
386
387    pub fn pop(&mut self) {
388        assert!(!self.stack.is_empty());
389        self.stack.pop().unwrap();
390    }
391}
392
393fn handle_program_log<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
394    self_program_str: &str,
395    l: &str,
396) -> Result<(Option<T>, Option<String>, bool), ClientError> {
397    // Log emitted from the current program.
398    if let Some(log) = l
399        .strip_prefix(PROGRAM_LOG)
400        .or_else(|| l.strip_prefix(PROGRAM_DATA))
401    {
402        let borsh_bytes = match anchor_lang::__private::base64::decode(&log) {
403            Ok(borsh_bytes) => borsh_bytes,
404            _ => {
405                //println!("Could not base64 decode log: {}", log);
406                return Ok((None, None, false));
407            }
408        };
409
410        let mut slice: &[u8] = &borsh_bytes[..];
411        let disc: [u8; 8] = {
412            let mut disc = [0; 8];
413            disc.copy_from_slice(&borsh_bytes[..8]);
414            slice = &slice[8..];
415            disc
416        };
417        let mut event = None;
418        if disc == T::discriminator() {
419            let e: T = anchor_lang::AnchorDeserialize::deserialize(&mut slice)
420                .map_err(|e| ClientError::LogParseError(e.to_string()))?;
421            event = Some(e);
422        }
423        Ok((event, None, false))
424    }
425    // System log.
426    else {
427        let (program, did_pop) = handle_system_log(self_program_str, l);
428        Ok((None, program, did_pop))
429    }
430}
431
432fn handle_system_log(this_program_str: &str, log: &str) -> (Option<String>, bool) {
433    if log.starts_with(&format!("Program {} log:", this_program_str)) {
434        (Some(this_program_str.to_string()), false)
435    } else if log.contains("invoke") {
436        (Some("cpi".to_string()), false) // Any string will do.
437    } else {
438        let re = Regex::new(r"^Program (.*) success*$").unwrap();
439        if re.is_match(log) {
440            (None, true)
441        } else {
442            (None, false)
443        }
444    }
445}