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