279 lines
11 KiB
Rust
279 lines
11 KiB
Rust
//! An [`RpcSender`] used for unit testing [`RpcClient`](crate::rpc_client::RpcClient).
|
|
|
|
use {
|
|
crate::{
|
|
client_error::Result,
|
|
rpc_config::RpcBlockProductionConfig,
|
|
rpc_request::RpcRequest,
|
|
rpc_response::{
|
|
Response, RpcAccountBalance, RpcBlockProduction, RpcBlockProductionRange, RpcFees,
|
|
RpcResponseContext, RpcSimulateTransactionResult, RpcStakeActivation, RpcSupply,
|
|
RpcVersionInfo, RpcVoteAccountStatus, StakeActivationState,
|
|
},
|
|
rpc_sender::RpcSender,
|
|
},
|
|
serde_json::{json, Number, Value},
|
|
solana_sdk::{
|
|
epoch_info::EpochInfo,
|
|
fee_calculator::{FeeCalculator, FeeRateGovernor},
|
|
instruction::InstructionError,
|
|
signature::Signature,
|
|
transaction::{self, Transaction, TransactionError},
|
|
},
|
|
solana_transaction_status::{TransactionConfirmationStatus, TransactionStatus},
|
|
solana_version::Version,
|
|
std::{collections::HashMap, sync::RwLock},
|
|
};
|
|
|
|
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
|
|
pub const SIGNATURE: &str =
|
|
"43yNSFC6fYTuPgTNFFhF4axw7AfWxB2BPdurme8yrsWEYwm8299xh8n6TAHjGymiSub1XtyxTNyd9GBfY2hxoBw8";
|
|
|
|
pub type Mocks = HashMap<RpcRequest, Value>;
|
|
pub struct MockSender {
|
|
mocks: RwLock<Mocks>,
|
|
url: String,
|
|
}
|
|
|
|
/// An [`RpcSender`] used for unit testing [`RpcClient`](crate::rpc_client::RpcClient).
|
|
///
|
|
/// This is primarily for internal use.
|
|
///
|
|
/// Unless directed otherwise, it will generally return a reasonable default
|
|
/// response, at least for [`RpcRequest`] values for which responses have been
|
|
/// implemented.
|
|
///
|
|
/// The behavior can be customized in two ways:
|
|
///
|
|
/// 1) The `url` constructor argument is not actually a URL, but a simple string
|
|
/// directive that changes `MockSender`s behavior in specific scenarios.
|
|
///
|
|
/// If `url` is "fails" then any call to `send` will return `Ok(Value::Null)`.
|
|
///
|
|
/// It is customary to set the `url` to "succeeds" for mocks that should
|
|
/// return sucessfully, though this value is not actually interpreted.
|
|
///
|
|
/// Other possible values of `url` are specific to different `RpcRequest`
|
|
/// values. Read the implementation for specifics.
|
|
///
|
|
/// 2) Custom responses can be configured by providing [`Mocks`] to the
|
|
/// [`MockSender::new_with_mocks`] constructor. This type is a [`HashMap`]
|
|
/// from [`RpcRequest`] to a JSON [`Value`] response, Any entries in this map
|
|
/// override the default behavior for the given request.
|
|
impl MockSender {
|
|
pub fn new(url: String) -> Self {
|
|
Self::new_with_mocks(url, Mocks::default())
|
|
}
|
|
|
|
pub fn new_with_mocks(url: String, mocks: Mocks) -> Self {
|
|
Self {
|
|
url,
|
|
mocks: RwLock::new(mocks),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RpcSender for MockSender {
|
|
fn send(&self, request: RpcRequest, params: serde_json::Value) -> Result<serde_json::Value> {
|
|
if let Some(value) = self.mocks.write().unwrap().remove(&request) {
|
|
return Ok(value);
|
|
}
|
|
if self.url == "fails" {
|
|
return Ok(Value::Null);
|
|
}
|
|
|
|
let method = &request.build_request_json(42, params.clone())["method"];
|
|
|
|
let val = match method.as_str().unwrap() {
|
|
"getAccountInfo" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: Value::Null,
|
|
})?,
|
|
"getBalance" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: Value::Number(Number::from(50)),
|
|
})?,
|
|
"getRecentBlockhash" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: (
|
|
Value::String(PUBKEY.to_string()),
|
|
serde_json::to_value(FeeCalculator::default()).unwrap(),
|
|
),
|
|
})?,
|
|
"getEpochInfo" => serde_json::to_value(EpochInfo {
|
|
epoch: 1,
|
|
slot_index: 2,
|
|
slots_in_epoch: 32,
|
|
absolute_slot: 34,
|
|
block_height: 34,
|
|
transaction_count: Some(123),
|
|
})?,
|
|
"getFeeCalculatorForBlockhash" => {
|
|
let value = if self.url == "blockhash_expired" {
|
|
Value::Null
|
|
} else {
|
|
serde_json::to_value(Some(FeeCalculator::default())).unwrap()
|
|
};
|
|
serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value,
|
|
})?
|
|
}
|
|
"getFeeRateGovernor" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: serde_json::to_value(FeeRateGovernor::default()).unwrap(),
|
|
})?,
|
|
"getFees" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: serde_json::to_value(RpcFees {
|
|
blockhash: PUBKEY.to_string(),
|
|
fee_calculator: FeeCalculator::default(),
|
|
last_valid_slot: 42,
|
|
last_valid_block_height: 42,
|
|
})
|
|
.unwrap(),
|
|
})?,
|
|
"getSignatureStatuses" => {
|
|
let status: transaction::Result<()> = if self.url == "account_in_use" {
|
|
Err(TransactionError::AccountInUse)
|
|
} else if self.url == "instruction_error" {
|
|
Err(TransactionError::InstructionError(
|
|
0,
|
|
InstructionError::UninitializedAccount,
|
|
))
|
|
} else {
|
|
Ok(())
|
|
};
|
|
let status = if self.url == "sig_not_found" {
|
|
None
|
|
} else {
|
|
let err = status.clone().err();
|
|
Some(TransactionStatus {
|
|
status,
|
|
slot: 1,
|
|
confirmations: None,
|
|
err,
|
|
confirmation_status: Some(TransactionConfirmationStatus::Finalized),
|
|
})
|
|
};
|
|
let statuses: Vec<Option<TransactionStatus>> = params.as_array().unwrap()[0]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|_| status.clone())
|
|
.collect();
|
|
serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: statuses,
|
|
})?
|
|
}
|
|
"getTransactionCount" => json![1234],
|
|
"getSlot" => json![0],
|
|
"getMaxShredInsertSlot" => json![0],
|
|
"requestAirdrop" => Value::String(Signature::new(&[8; 64]).to_string()),
|
|
"getSnapshotSlot" => Value::Number(Number::from(0)),
|
|
"getBlockHeight" => Value::Number(Number::from(1234)),
|
|
"getSlotLeaders" => json!([PUBKEY]),
|
|
"getBlockProduction" => {
|
|
if params.is_null() {
|
|
json!(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: RpcBlockProduction {
|
|
by_identity: HashMap::new(),
|
|
range: RpcBlockProductionRange {
|
|
first_slot: 1,
|
|
last_slot: 2,
|
|
},
|
|
},
|
|
})
|
|
} else {
|
|
let config: Vec<RpcBlockProductionConfig> =
|
|
serde_json::from_value(params).unwrap();
|
|
let config = config[0].clone();
|
|
let mut by_identity = HashMap::new();
|
|
by_identity.insert(config.identity.unwrap(), (1, 123));
|
|
let config_range = config.range.unwrap_or_default();
|
|
|
|
json!(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: RpcBlockProduction {
|
|
by_identity,
|
|
range: RpcBlockProductionRange {
|
|
first_slot: config_range.first_slot,
|
|
last_slot: {
|
|
if let Some(last_slot) = config_range.last_slot {
|
|
last_slot
|
|
} else {
|
|
2
|
|
}
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
"getStakeActivation" => json!(RpcStakeActivation {
|
|
state: StakeActivationState::Active,
|
|
active: 123,
|
|
inactive: 12,
|
|
}),
|
|
"getSupply" => json!(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: RpcSupply {
|
|
total: 100000000,
|
|
circulating: 50000,
|
|
non_circulating: 20000,
|
|
non_circulating_accounts: vec![PUBKEY.to_string()],
|
|
},
|
|
}),
|
|
"getLargestAccounts" => {
|
|
let rpc_account_balance = RpcAccountBalance {
|
|
address: PUBKEY.to_string(),
|
|
lamports: 10000,
|
|
};
|
|
|
|
json!(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: vec![rpc_account_balance],
|
|
})
|
|
}
|
|
"getVoteAccounts" => {
|
|
json!(RpcVoteAccountStatus {
|
|
current: vec![],
|
|
delinquent: vec![],
|
|
})
|
|
}
|
|
"sendTransaction" => {
|
|
let signature = if self.url == "malicious" {
|
|
Signature::new(&[8; 64]).to_string()
|
|
} else {
|
|
let tx_str = params.as_array().unwrap()[0].as_str().unwrap().to_string();
|
|
let data = base64::decode(tx_str).unwrap();
|
|
let tx: Transaction = bincode::deserialize(&data).unwrap();
|
|
tx.signatures[0].to_string()
|
|
};
|
|
Value::String(signature)
|
|
}
|
|
"simulateTransaction" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: RpcSimulateTransactionResult {
|
|
err: None,
|
|
logs: None,
|
|
accounts: None,
|
|
units_consumed: None,
|
|
},
|
|
})?,
|
|
"getMinimumBalanceForRentExemption" => json![20],
|
|
"getVersion" => {
|
|
let version = Version::default();
|
|
json!(RpcVersionInfo {
|
|
solana_core: version.to_string(),
|
|
feature_set: Some(version.feature_set),
|
|
})
|
|
}
|
|
_ => Value::Null,
|
|
};
|
|
Ok(val)
|
|
}
|
|
}
|