* tpu-client: Move `send_messages_with_spinner` from program (#20960)
We have too many ways of sending transactions, and too many
reimplementations of the same logic all over the place.
The program deploy logic and stake-o-matic currently make the
most use of the TPU client, so this merges their implementations into
one place to be reused by both. Yay for consolidation!
(cherry picked from commit 5f7b60576f
)
# Conflicts:
# cli/src/program.rs
# client/src/mock_sender.rs
* Fix merge issues, use older APIs
* Update mock sender fee to match block height
Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
448 lines
19 KiB
Rust
448 lines
19 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,
|
|
RpcConfirmedTransactionStatusWithSignature, RpcContactInfo, RpcFees, RpcIdentity,
|
|
RpcInflationGovernor, RpcInflationRate, RpcInflationReward, RpcKeyedAccount,
|
|
RpcPerfSample, RpcResponseContext, RpcSimulateTransactionResult, RpcStakeActivation,
|
|
RpcSupply, RpcVersionInfo, RpcVoteAccountInfo, RpcVoteAccountStatus,
|
|
StakeActivationState,
|
|
},
|
|
rpc_sender::*,
|
|
},
|
|
serde_json::{json, Number, Value},
|
|
solana_account_decoder::{UiAccount, UiAccountEncoding},
|
|
solana_sdk::{
|
|
account::Account,
|
|
clock::{Slot, UnixTimestamp},
|
|
epoch_info::EpochInfo,
|
|
fee_calculator::{FeeCalculator, FeeRateGovernor},
|
|
instruction::InstructionError,
|
|
message::MessageHeader,
|
|
pubkey::Pubkey,
|
|
signature::Signature,
|
|
sysvar::epoch_schedule::EpochSchedule,
|
|
transaction::{self, Transaction, TransactionError},
|
|
},
|
|
solana_transaction_status::{
|
|
EncodedConfirmedBlock, EncodedConfirmedTransaction, EncodedTransaction,
|
|
EncodedTransactionWithStatusMeta, Rewards, TransactionConfirmationStatus,
|
|
TransactionStatus, UiCompiledInstruction, UiMessage, UiRawMessage, UiTransaction,
|
|
UiTransactionEncoding, UiTransactionStatusMeta,
|
|
},
|
|
solana_version::Version,
|
|
std::{collections::HashMap, net::SocketAddr, str::FromStr, 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 get_transport_stats(&self) -> RpcTransportStats {
|
|
RpcTransportStats::default()
|
|
}
|
|
|
|
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: 1234,
|
|
last_valid_block_height: 1234,
|
|
})
|
|
.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,
|
|
})?
|
|
}
|
|
"getTransaction" => serde_json::to_value(EncodedConfirmedTransaction {
|
|
slot: 2,
|
|
transaction: EncodedTransactionWithStatusMeta {
|
|
transaction: EncodedTransaction::Json(
|
|
UiTransaction {
|
|
signatures: vec!["3AsdoALgZFuq2oUVWrDYhg2pNeaLJKPLf8hU2mQ6U8qJxeJ6hsrPVpMn9ma39DtfYCrDQSvngWRP8NnTpEhezJpE".to_string()],
|
|
message: UiMessage::Raw(
|
|
UiRawMessage {
|
|
header: MessageHeader {
|
|
num_required_signatures: 1,
|
|
num_readonly_signed_accounts: 0,
|
|
num_readonly_unsigned_accounts: 1,
|
|
},
|
|
account_keys: vec![
|
|
"C6eBmAXKg6JhJWkajGa5YRGUfG4YKXwbxF5Ufv7PtExZ".to_string(),
|
|
"2Gd5eoR5J4BV89uXbtunpbNhjmw3wa1NbRHxTHzDzZLX".to_string(),
|
|
"11111111111111111111111111111111".to_string(),
|
|
],
|
|
recent_blockhash: "D37n3BSG71oUWcWjbZ37jZP7UfsxG2QMKeuALJ1PYvM6".to_string(),
|
|
instructions: vec![UiCompiledInstruction {
|
|
program_id_index: 2,
|
|
accounts: vec![0, 1],
|
|
data: "3Bxs49DitAvXtoDR".to_string(),
|
|
}],
|
|
})
|
|
}),
|
|
meta: Some(UiTransactionStatusMeta {
|
|
err: None,
|
|
status: Ok(()),
|
|
fee: 0,
|
|
pre_balances: vec![499999999999999950, 50, 1],
|
|
post_balances: vec![499999999999999950, 50, 1],
|
|
inner_instructions: None,
|
|
log_messages: None,
|
|
pre_token_balances: None,
|
|
post_token_balances: None,
|
|
rewards: None,
|
|
}),
|
|
},
|
|
block_time: Some(1628633791),
|
|
})?,
|
|
"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::Activating,
|
|
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![RpcVoteAccountInfo {
|
|
vote_pubkey: PUBKEY.to_string(),
|
|
node_pubkey: PUBKEY.to_string(),
|
|
activated_stake: 0,
|
|
commission: 0,
|
|
epoch_vote_account: false,
|
|
epoch_credits: vec![],
|
|
last_vote: 0,
|
|
root_slot: Slot::default(),
|
|
}],
|
|
})
|
|
}
|
|
"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,
|
|
},
|
|
})?,
|
|
"getMinimumBalanceForRentExemption" => json![20],
|
|
"getVersion" => {
|
|
let version = Version::default();
|
|
json!(RpcVersionInfo {
|
|
solana_core: version.to_string(),
|
|
feature_set: Some(version.feature_set),
|
|
})
|
|
}
|
|
"getClusterNodes" => serde_json::to_value(vec![RpcContactInfo {
|
|
pubkey: PUBKEY.to_string(),
|
|
gossip: Some(SocketAddr::from(([10, 239, 6, 48], 8899))),
|
|
tpu: Some(SocketAddr::from(([10, 239, 6, 48], 8856))),
|
|
rpc: Some(SocketAddr::from(([10, 239, 6, 48], 8899))),
|
|
version: Some("1.0.0 c375ce1f".to_string()),
|
|
feature_set: None,
|
|
shred_version: None,
|
|
}])?,
|
|
"getBlock" => serde_json::to_value(EncodedConfirmedBlock {
|
|
previous_blockhash: "mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B".to_string(),
|
|
blockhash: "3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA".to_string(),
|
|
parent_slot: 429,
|
|
transactions: vec![EncodedTransactionWithStatusMeta {
|
|
transaction: EncodedTransaction::Binary(
|
|
"ju9xZWuDBX4pRxX2oZkTjxU5jB4SSTgEGhX8bQ8PURNzyzqKMPPpNvWihx8zUe\
|
|
FfrbVNoAaEsNKZvGzAnTDy5bhNT9kt6KFCTBixpvrLCzg4M5UdFUQYrn1gdgjX\
|
|
pLHxcaShD81xBNaFDgnA2nkkdHnKtZt4hVSfKAmw3VRZbjrZ7L2fKZBx21CwsG\
|
|
hD6onjM2M3qZW5C8J6d1pj41MxKmZgPBSha3MyKkNLkAGFASK"
|
|
.to_string(),
|
|
UiTransactionEncoding::Base58,
|
|
),
|
|
meta: None,
|
|
}],
|
|
rewards: Rewards::new(),
|
|
block_time: None,
|
|
block_height: Some(428),
|
|
})?,
|
|
"getBlocks" => serde_json::to_value(vec![1, 2, 3])?,
|
|
"getBlocksWithLimit" => serde_json::to_value(vec![1, 2, 3])?,
|
|
"getSignaturesForAddress" => {
|
|
serde_json::to_value(vec![RpcConfirmedTransactionStatusWithSignature {
|
|
signature: SIGNATURE.to_string(),
|
|
slot: 123,
|
|
err: None,
|
|
memo: None,
|
|
block_time: None,
|
|
confirmation_status: Some(TransactionConfirmationStatus::Finalized),
|
|
}])?
|
|
}
|
|
"getBlockTime" => serde_json::to_value(UnixTimestamp::default())?,
|
|
"getEpochSchedule" => serde_json::to_value(EpochSchedule::default())?,
|
|
"getRecentPerformanceSamples" => serde_json::to_value(vec![RpcPerfSample {
|
|
slot: 347873,
|
|
num_transactions: 125,
|
|
num_slots: 123,
|
|
sample_period_secs: 60,
|
|
}])?,
|
|
"getIdentity" => serde_json::to_value(RpcIdentity {
|
|
identity: PUBKEY.to_string(),
|
|
})?,
|
|
"getInflationGovernor" => serde_json::to_value(
|
|
RpcInflationGovernor {
|
|
initial: 0.08,
|
|
terminal: 0.015,
|
|
taper: 0.15,
|
|
foundation: 0.05,
|
|
foundation_term: 7.0,
|
|
})?,
|
|
"getInflationRate" => serde_json::to_value(
|
|
RpcInflationRate {
|
|
total: 0.08,
|
|
validator: 0.076,
|
|
foundation: 0.004,
|
|
epoch: 0,
|
|
})?,
|
|
"getInflationReward" => serde_json::to_value(vec![
|
|
Some(RpcInflationReward {
|
|
epoch: 2,
|
|
effective_slot: 224,
|
|
amount: 2500,
|
|
post_balance: 499999442500,
|
|
commission: None,
|
|
})])?,
|
|
"minimumLedgerSlot" => json![123],
|
|
"getMaxRetransmitSlot" => json![123],
|
|
"getMultipleAccounts" => serde_json::to_value(Response {
|
|
context: RpcResponseContext { slot: 1 },
|
|
value: vec![Value::Null, Value::Null]
|
|
})?,
|
|
"getProgramAccounts" => {
|
|
let pubkey = Pubkey::from_str(&PUBKEY.to_string()).unwrap();
|
|
let account = Account {
|
|
lamports: 1_000_000,
|
|
data: vec![],
|
|
owner: pubkey,
|
|
executable: false,
|
|
rent_epoch: 0,
|
|
};
|
|
serde_json::to_value(vec![
|
|
RpcKeyedAccount {
|
|
pubkey: PUBKEY.to_string(),
|
|
account: UiAccount::encode(
|
|
&pubkey,
|
|
&account,
|
|
UiAccountEncoding::Base64,
|
|
None,
|
|
None,
|
|
)
|
|
}
|
|
])?
|
|
},
|
|
_ => Value::Null,
|
|
};
|
|
Ok(val)
|
|
}
|
|
}
|