solana_rpc_client/
mock_sender.rs

1//! A nonblocking [`RpcSender`] used for unit testing [`RpcClient`](crate::rpc_client::RpcClient).
2
3use {
4    crate::rpc_sender::*,
5    async_trait::async_trait,
6    base64::{prelude::BASE64_STANDARD, Engine},
7    serde_json::{json, Number, Value},
8    solana_account_decoder_client_types::{UiAccount, UiAccountData, UiAccountEncoding},
9    solana_rpc_client_api::{
10        client_error::Result,
11        config::RpcBlockProductionConfig,
12        request::RpcRequest,
13        response::{
14            Response, RpcAccountBalance, RpcBlockProduction, RpcBlockProductionRange, RpcBlockhash,
15            RpcConfirmedTransactionStatusWithSignature, RpcContactInfo, RpcIdentity,
16            RpcInflationGovernor, RpcInflationRate, RpcInflationReward, RpcKeyedAccount,
17            RpcPerfSample, RpcPrioritizationFee, RpcResponseContext, RpcSimulateTransactionResult,
18            RpcSnapshotSlotInfo, RpcSupply, RpcVersionInfo, RpcVoteAccountInfo,
19            RpcVoteAccountStatus,
20        },
21    },
22    solana_sdk::{
23        clock::{Slot, UnixTimestamp},
24        epoch_info::EpochInfo,
25        instruction::InstructionError,
26        message::MessageHeader,
27        pubkey::Pubkey,
28        signature::Signature,
29        sysvar::epoch_schedule::EpochSchedule,
30        transaction::{self, Transaction, TransactionError, TransactionVersion},
31    },
32    solana_transaction_status_client_types::{
33        option_serializer::OptionSerializer, EncodedConfirmedBlock,
34        EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
35        EncodedTransactionWithStatusMeta, Rewards, TransactionBinaryEncoding,
36        TransactionConfirmationStatus, TransactionStatus, UiCompiledInstruction, UiMessage,
37        UiRawMessage, UiTransaction, UiTransactionStatusMeta,
38    },
39    solana_version::Version,
40    std::{collections::HashMap, net::SocketAddr, str::FromStr, sync::RwLock},
41};
42
43pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
44
45pub type Mocks = HashMap<RpcRequest, Value>;
46pub struct MockSender {
47    mocks: RwLock<Mocks>,
48    url: String,
49}
50
51/// An [`RpcSender`] used for unit testing [`RpcClient`](crate::rpc_client::RpcClient).
52///
53/// This is primarily for internal use.
54///
55/// Unless directed otherwise, it will generally return a reasonable default
56/// response, at least for [`RpcRequest`] values for which responses have been
57/// implemented.
58///
59/// The behavior can be customized in two ways:
60///
61/// 1) The `url` constructor argument is not actually a URL, but a simple string
62///    directive that changes `MockSender`s behavior in specific scenarios.
63///
64///    If `url` is "fails" then any call to `send` will return `Ok(Value::Null)`.
65///
66///    It is customary to set the `url` to "succeeds" for mocks that should
67///    return successfully, though this value is not actually interpreted.
68///
69///    Other possible values of `url` are specific to different `RpcRequest`
70///    values. Read the implementation for specifics.
71///
72/// 2) Custom responses can be configured by providing [`Mocks`] to the
73///    [`MockSender::new_with_mocks`] constructor. This type is a [`HashMap`]
74///    from [`RpcRequest`] to a JSON [`Value`] response, Any entries in this map
75///    override the default behavior for the given request.
76impl MockSender {
77    pub fn new<U: ToString>(url: U) -> Self {
78        Self::new_with_mocks(url, Mocks::default())
79    }
80
81    pub fn new_with_mocks<U: ToString>(url: U, mocks: Mocks) -> Self {
82        Self {
83            url: url.to_string(),
84            mocks: RwLock::new(mocks),
85        }
86    }
87}
88
89#[async_trait]
90impl RpcSender for MockSender {
91    fn get_transport_stats(&self) -> RpcTransportStats {
92        RpcTransportStats::default()
93    }
94
95    async fn send(
96        &self,
97        request: RpcRequest,
98        params: serde_json::Value,
99    ) -> Result<serde_json::Value> {
100        if let Some(value) = self.mocks.write().unwrap().remove(&request) {
101            return Ok(value);
102        }
103        if self.url == "fails" {
104            return Ok(Value::Null);
105        }
106
107        let method = &request.build_request_json(42, params.clone())["method"];
108
109        let val = match method.as_str().unwrap() {
110            "getAccountInfo" => serde_json::to_value(Response {
111                context: RpcResponseContext { slot: 1, api_version: None },
112                value: Value::Null,
113            })?,
114            "getBalance" => serde_json::to_value(Response {
115                context: RpcResponseContext { slot: 1, api_version: None },
116                value: Value::Number(Number::from(50)),
117            })?,
118            "getEpochInfo" => serde_json::to_value(EpochInfo {
119                epoch: 1,
120                slot_index: 2,
121                slots_in_epoch: 32,
122                absolute_slot: 34,
123                block_height: 34,
124                transaction_count: Some(123),
125            })?,
126            "getSignatureStatuses" => {
127                let status: transaction::Result<()> = if self.url == "account_in_use" {
128                    Err(TransactionError::AccountInUse)
129                } else if self.url == "instruction_error" {
130                    Err(TransactionError::InstructionError(
131                        0,
132                        InstructionError::UninitializedAccount,
133                    ))
134                } else {
135                    Ok(())
136                };
137                let status = if self.url == "sig_not_found" {
138                    None
139                } else {
140                    let err = status.clone().err();
141                    Some(TransactionStatus {
142                        status,
143                        slot: 1,
144                        confirmations: None,
145                        err,
146                        confirmation_status: Some(TransactionConfirmationStatus::Finalized),
147                    })
148                };
149                let statuses: Vec<Option<TransactionStatus>> = params.as_array().unwrap()[0]
150                    .as_array()
151                    .unwrap()
152                    .iter()
153                    .map(|_| status.clone())
154                    .collect();
155                serde_json::to_value(Response {
156                    context: RpcResponseContext { slot: 1, api_version: None },
157                    value: statuses,
158                })?
159            }
160            "getTransaction" => serde_json::to_value(EncodedConfirmedTransactionWithStatusMeta {
161                slot: 2,
162                transaction: EncodedTransactionWithStatusMeta {
163                    version: Some(TransactionVersion::LEGACY),
164                    transaction: EncodedTransaction::Json(
165                        UiTransaction {
166                            signatures: vec!["3AsdoALgZFuq2oUVWrDYhg2pNeaLJKPLf8hU2mQ6U8qJxeJ6hsrPVpMn9ma39DtfYCrDQSvngWRP8NnTpEhezJpE".to_string()],
167                            message: UiMessage::Raw(
168                                UiRawMessage {
169                                    header: MessageHeader {
170                                        num_required_signatures: 1,
171                                        num_readonly_signed_accounts: 0,
172                                        num_readonly_unsigned_accounts: 1,
173                                    },
174                                    account_keys: vec![
175                                        "C6eBmAXKg6JhJWkajGa5YRGUfG4YKXwbxF5Ufv7PtExZ".to_string(),
176                                        "2Gd5eoR5J4BV89uXbtunpbNhjmw3wa1NbRHxTHzDzZLX".to_string(),
177                                        "11111111111111111111111111111111".to_string(),
178                                    ],
179                                    recent_blockhash: "D37n3BSG71oUWcWjbZ37jZP7UfsxG2QMKeuALJ1PYvM6".to_string(),
180                                    instructions: vec![UiCompiledInstruction {
181                                        program_id_index: 2,
182                                        accounts: vec![0, 1],
183                                        data: "3Bxs49DitAvXtoDR".to_string(),
184                                        stack_height: None,
185                                    }],
186                                    address_table_lookups: None,
187                                })
188                        }),
189                    meta: Some(UiTransactionStatusMeta {
190                            err: None,
191                            status: Ok(()),
192                            fee: 0,
193                            pre_balances: vec![499999999999999950, 50, 1],
194                            post_balances: vec![499999999999999950, 50, 1],
195                            inner_instructions: OptionSerializer::None,
196                            log_messages: OptionSerializer::None,
197                            pre_token_balances: OptionSerializer::None,
198                            post_token_balances: OptionSerializer::None,
199                            rewards: OptionSerializer::None,
200                            loaded_addresses: OptionSerializer::Skip,
201                            return_data: OptionSerializer::Skip,
202                            compute_units_consumed: OptionSerializer::Skip,
203                        }),
204                },
205                block_time: Some(1628633791),
206            })?,
207            "getTransactionCount" => json![1234],
208            "getSlot" => json![0],
209            "getMaxShredInsertSlot" => json![0],
210            "requestAirdrop" => Value::String(Signature::from([8; 64]).to_string()),
211            "getHighestSnapshotSlot" => json!(RpcSnapshotSlotInfo {
212                full: 100,
213                incremental: Some(110),
214            }),
215            "getBlockHeight" => Value::Number(Number::from(1234)),
216            "getSlotLeaders" => json!([PUBKEY]),
217            "getBlockProduction" => {
218                if params.is_null() {
219                    json!(Response {
220                        context: RpcResponseContext { slot: 1, api_version: None },
221                        value: RpcBlockProduction {
222                            by_identity: HashMap::new(),
223                            range: RpcBlockProductionRange {
224                                first_slot: 1,
225                                last_slot: 2,
226                            },
227                        },
228                    })
229                } else {
230                    let config: Vec<RpcBlockProductionConfig> =
231                        serde_json::from_value(params).unwrap();
232                    let config = config[0].clone();
233                    let mut by_identity = HashMap::new();
234                    by_identity.insert(config.identity.unwrap(), (1, 123));
235                    let config_range = config.range.unwrap_or_default();
236
237                    json!(Response {
238                        context: RpcResponseContext { slot: 1, api_version: None },
239                        value: RpcBlockProduction {
240                            by_identity,
241                            range: RpcBlockProductionRange {
242                                first_slot: config_range.first_slot,
243                                last_slot: {
244                                    config_range.last_slot.unwrap_or(2)
245                                },
246                            },
247                        },
248                    })
249                }
250            }
251            "getStakeMinimumDelegation" => json!(Response {
252                context: RpcResponseContext { slot: 1, api_version: None },
253                value: 123_456_789,
254            }),
255            "getSupply" => json!(Response {
256                context: RpcResponseContext { slot: 1, api_version: None },
257                value: RpcSupply {
258                    total: 100000000,
259                    circulating: 50000,
260                    non_circulating: 20000,
261                    non_circulating_accounts: vec![PUBKEY.to_string()],
262                },
263            }),
264            "getLargestAccounts" => {
265                let rpc_account_balance = RpcAccountBalance {
266                    address: PUBKEY.to_string(),
267                    lamports: 10000,
268                };
269
270                json!(Response {
271                    context: RpcResponseContext { slot: 1, api_version: None },
272                    value: vec![rpc_account_balance],
273                })
274            }
275            "getVoteAccounts" => {
276                json!(RpcVoteAccountStatus {
277                    current: vec![],
278                    delinquent: vec![RpcVoteAccountInfo {
279                        vote_pubkey: PUBKEY.to_string(),
280                        node_pubkey: PUBKEY.to_string(),
281                        activated_stake: 0,
282                        commission: 0,
283                        epoch_vote_account: false,
284                        epoch_credits: vec![],
285                        last_vote: 0,
286                        root_slot: Slot::default(),
287                    }],
288                })
289            }
290            "sendTransaction" => {
291                let signature = if self.url == "malicious" {
292                    Signature::from([8; 64]).to_string()
293                } else {
294                    let tx_str = params.as_array().unwrap()[0].as_str().unwrap().to_string();
295                    let data = BASE64_STANDARD.decode(tx_str).unwrap();
296                    let tx: Transaction = bincode::deserialize(&data).unwrap();
297                    tx.signatures[0].to_string()
298                };
299                Value::String(signature)
300            }
301            "simulateTransaction" => serde_json::to_value(Response {
302                context: RpcResponseContext { slot: 1, api_version: None },
303                value: RpcSimulateTransactionResult {
304                    err: None,
305                    logs: None,
306                    accounts: None,
307                    units_consumed: None,
308                    return_data: None,
309                    inner_instructions: None,
310                    replacement_blockhash: None
311                },
312            })?,
313            "getMinimumBalanceForRentExemption" => json![20],
314            "getVersion" => {
315                let version = Version::default();
316                json!(RpcVersionInfo {
317                    solana_core: version.to_string(),
318                    feature_set: Some(version.feature_set),
319                })
320            }
321            "getLatestBlockhash" => serde_json::to_value(Response {
322                context: RpcResponseContext { slot: 1, api_version: None },
323                value: RpcBlockhash {
324                    blockhash: PUBKEY.to_string(),
325                    last_valid_block_height: 1234,
326                },
327            })?,
328            "getFeeForMessage" => serde_json::to_value(Response {
329                context: RpcResponseContext { slot: 1, api_version: None },
330                value: json!(Some(0)),
331            })?,
332            "getClusterNodes" => serde_json::to_value(vec![RpcContactInfo {
333                pubkey: PUBKEY.to_string(),
334                gossip: Some(SocketAddr::from(([10, 239, 6, 48], 8899))),
335                tvu: Some(SocketAddr::from(([10, 239, 6, 48], 8865))),
336                tpu: Some(SocketAddr::from(([10, 239, 6, 48], 8856))),
337                tpu_quic: Some(SocketAddr::from(([10, 239, 6, 48], 8862))),
338                tpu_forwards: Some(SocketAddr::from(([10, 239, 6, 48], 8857))),
339                tpu_forwards_quic: Some(SocketAddr::from(([10, 239, 6, 48], 8863))),
340                tpu_vote: Some(SocketAddr::from(([10, 239, 6, 48], 8870))),
341                serve_repair: Some(SocketAddr::from(([10, 239, 6, 48], 8880))),
342                rpc: Some(SocketAddr::from(([10, 239, 6, 48], 8899))),
343                pubsub: Some(SocketAddr::from(([10, 239, 6, 48], 8900))),
344                version: Some("1.0.0 c375ce1f".to_string()),
345                feature_set: None,
346                shred_version: None,
347            }])?,
348            "getBlock" => serde_json::to_value(EncodedConfirmedBlock {
349                previous_blockhash: "mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B".to_string(),
350                blockhash: "3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA".to_string(),
351                parent_slot: 429,
352                transactions: vec![EncodedTransactionWithStatusMeta {
353                    transaction: EncodedTransaction::Binary(
354                        "ju9xZWuDBX4pRxX2oZkTjxU5jB4SSTgEGhX8bQ8PURNzyzqKMPPpNvWihx8zUe\
355                                 FfrbVNoAaEsNKZvGzAnTDy5bhNT9kt6KFCTBixpvrLCzg4M5UdFUQYrn1gdgjX\
356                                 pLHxcaShD81xBNaFDgnA2nkkdHnKtZt4hVSfKAmw3VRZbjrZ7L2fKZBx21CwsG\
357                                 hD6onjM2M3qZW5C8J6d1pj41MxKmZgPBSha3MyKkNLkAGFASK"
358                            .to_string(),
359                        TransactionBinaryEncoding::Base58,
360                    ),
361                    meta: None,
362                    version: Some(TransactionVersion::LEGACY),
363                }],
364                rewards: Rewards::new(),
365                num_partitions: None,
366                block_time: None,
367                block_height: Some(428),
368            })?,
369            "getBlocks" => serde_json::to_value(vec![1, 2, 3])?,
370            "getBlocksWithLimit" => serde_json::to_value(vec![1, 2, 3])?,
371            "getSignaturesForAddress" => {
372                serde_json::to_value(vec![RpcConfirmedTransactionStatusWithSignature {
373                    signature: crate::mock_sender_for_cli::SIGNATURE.to_string(),
374                    slot: 123,
375                    err: None,
376                    memo: None,
377                    block_time: None,
378                    confirmation_status: Some(TransactionConfirmationStatus::Finalized),
379                }])?
380            }
381            "getBlockTime" => serde_json::to_value(UnixTimestamp::default())?,
382            "getEpochSchedule" => serde_json::to_value(EpochSchedule::default())?,
383            "getRecentPerformanceSamples" => serde_json::to_value(vec![RpcPerfSample {
384                slot: 347873,
385                num_transactions: 125,
386                num_non_vote_transactions: Some(1),
387                num_slots: 123,
388                sample_period_secs: 60,
389            }])?,
390            "getRecentPrioritizationFees" => serde_json::to_value(vec![RpcPrioritizationFee {
391                slot: 123_456_789,
392                prioritization_fee: 10_000,
393            }])?,
394            "getIdentity" => serde_json::to_value(RpcIdentity {
395                identity: PUBKEY.to_string(),
396            })?,
397            "getInflationGovernor" => serde_json::to_value(
398                RpcInflationGovernor {
399                    initial: 0.08,
400                    terminal: 0.015,
401                    taper: 0.15,
402                    foundation: 0.05,
403                    foundation_term: 7.0,
404                })?,
405            "getInflationRate" => serde_json::to_value(
406                RpcInflationRate {
407                    total: 0.08,
408                    validator: 0.076,
409                    foundation: 0.004,
410                    epoch: 0,
411                })?,
412            "getInflationReward" => serde_json::to_value(vec![
413                Some(RpcInflationReward {
414                    epoch: 2,
415                    effective_slot: 224,
416                    amount: 2500,
417                    post_balance: 499999442500,
418                    commission: None,
419                })])?,
420            "minimumLedgerSlot" => json![123],
421            "getMaxRetransmitSlot" => json![123],
422            "getMultipleAccounts" => serde_json::to_value(Response {
423                context: RpcResponseContext { slot: 1, api_version: None },
424                value: vec![Value::Null, Value::Null]
425            })?,
426            "getProgramAccounts" => {
427                let pubkey = Pubkey::from_str(PUBKEY).unwrap();
428                serde_json::to_value(vec![
429                    RpcKeyedAccount {
430                        pubkey: PUBKEY.to_string(),
431                        account: mock_encoded_account(&pubkey)
432                    }
433                ])?
434            },
435            _ => Value::Null,
436        };
437        Ok(val)
438    }
439
440    fn url(&self) -> String {
441        format!("MockSender: {}", self.url)
442    }
443}
444
445pub(crate) fn mock_encoded_account(pubkey: &Pubkey) -> UiAccount {
446    UiAccount {
447        lamports: 1_000_000,
448        data: UiAccountData::Binary("".to_string(), UiAccountEncoding::Base64),
449        owner: pubkey.to_string(),
450        executable: false,
451        rent_epoch: 0,
452        space: Some(0),
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use {super::*, solana_account_decoder::encode_ui_account, solana_sdk::account::Account};
459
460    #[test]
461    fn test_mock_encoded_account() {
462        let pubkey = Pubkey::from_str(PUBKEY).unwrap();
463        let account = Account {
464            lamports: 1_000_000,
465            data: vec![],
466            owner: pubkey,
467            executable: false,
468            rent_epoch: 0,
469        };
470        let expected = encode_ui_account(&pubkey, &account, UiAccountEncoding::Base64, None, None);
471        assert_eq!(expected, mock_encoded_account(&pubkey));
472    }
473}