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_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
50impl 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}