1use {
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
51impl 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}