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