safecoin_client/
mock_sender.rs

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