diff --git a/client/src/mock_rpc_client_request.rs b/client/src/mock_rpc_client_request.rs index 2bfd40564..90a7d3d09 100644 --- a/client/src/mock_rpc_client_request.rs +++ b/client/src/mock_rpc_client_request.rs @@ -71,6 +71,17 @@ impl GenericRpcClientRequest for MockRpcClientRequest { serde_json::to_value(FeeCalculator::default()).unwrap(), ), })?, + RpcRequest::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, + })? + } RpcRequest::GetFeeRateGovernor => serde_json::to_value(Response { context: RpcResponseContext { slot: 1 }, value: serde_json::to_value(FeeRateGovernor::default()).unwrap(), diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index c09f8f13c..a760e155b 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -6,8 +6,8 @@ use crate::{ rpc_request::RpcRequest, rpc_response::{ Response, RpcAccount, RpcBlockhashFeeCalculator, RpcConfirmedBlock, RpcContactInfo, - RpcEpochInfo, RpcFeeRateGovernor, RpcIdentity, RpcKeyedAccount, RpcLeaderSchedule, - RpcResponse, RpcVersionInfo, RpcVoteAccountStatus, + RpcEpochInfo, RpcFeeCalculator, RpcFeeRateGovernor, RpcIdentity, RpcKeyedAccount, + RpcLeaderSchedule, RpcResponse, RpcVersionInfo, RpcVoteAccountStatus, }, }; use bincode::serialize; @@ -839,6 +839,35 @@ impl RpcClient { }) } + pub fn get_fee_calculator_for_blockhash( + &self, + blockhash: &Hash, + ) -> io::Result> { + let response = self + .client + .send( + &RpcRequest::GetFeeCalculatorForBlockhash, + json!([blockhash.to_string()]), + 0, + ) + .map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("GetFeeCalculatorForBlockhash request failure: {:?}", e), + ) + })?; + let Response { value, .. } = serde_json::from_value::>>( + response, + ) + .map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("GetFeeCalculatorForBlockhash parse failure: {:?}", e), + ) + })?; + Ok(value.map(|rf| rf.fee_calculator)) + } + pub fn get_fee_rate_governor(&self) -> RpcResponse { let response = self .client diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 391f19800..d10ec03d8 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -21,6 +21,7 @@ pub enum RpcRequest { GetNumBlocksSinceSignatureConfirmation, GetProgramAccounts, GetRecentBlockhash, + GetFeeCalculatorForBlockhash, GetFeeRateGovernor, GetSignatureStatus, GetSlot, @@ -64,6 +65,7 @@ impl RpcRequest { } RpcRequest::GetProgramAccounts => "getProgramAccounts", RpcRequest::GetRecentBlockhash => "getRecentBlockhash", + RpcRequest::GetFeeCalculatorForBlockhash => "getFeeCalculatorForBlockhash", RpcRequest::GetFeeRateGovernor => "getFeeRateGovernor", RpcRequest::GetSignatureStatus => "getSignatureStatus", RpcRequest::GetSlot => "getSlot", @@ -127,7 +129,7 @@ mod tests { assert_eq!(request["params"], json!([addr])); let test_request = RpcRequest::GetBalance; - let request = test_request.build_request_json(1, json!([addr])); + let request = test_request.build_request_json(1, json!([addr.clone()])); assert_eq!(request["method"], "getBalance"); let test_request = RpcRequest::GetEpochInfo; @@ -142,6 +144,10 @@ mod tests { let request = test_request.build_request_json(1, Value::Null); assert_eq!(request["method"], "getRecentBlockhash"); + let test_request = RpcRequest::GetFeeCalculatorForBlockhash; + let request = test_request.build_request_json(1, json!([addr.clone()])); + assert_eq!(request["method"], "getFeeCalculatorForBlockhash"); + let test_request = RpcRequest::GetFeeRateGovernor; let request = test_request.build_request_json(1, Value::Null); assert_eq!(request["method"], "getFeeRateGovernor"); diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index a0e6cc4b4..d2b0bfc46 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -152,6 +152,12 @@ pub struct RpcBlockhashFeeCalculator { pub fee_calculator: FeeCalculator, } +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RpcFeeCalculator { + pub fee_calculator: FeeCalculator, +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct RpcFeeRateGovernor { diff --git a/client/src/thin_client.rs b/client/src/thin_client.rs index ce2e52424..110cbe4c1 100644 --- a/client/src/thin_client.rs +++ b/client/src/thin_client.rs @@ -445,6 +445,16 @@ impl SyncClient for ThinClient { } } + fn get_fee_calculator_for_blockhash( + &self, + blockhash: &Hash, + ) -> TransportResult> { + let fee_calculator = self + .rpc_client() + .get_fee_calculator_for_blockhash(blockhash)?; + Ok(fee_calculator) + } + fn get_fee_rate_governor(&self) -> TransportResult { let fee_rate_governor = self.rpc_client().get_fee_rate_governor()?; Ok(fee_rate_governor.value) diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 934e8e3f7..b4d4d8c94 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -9,9 +9,10 @@ use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; use solana_client::rpc_response::{ Response, RpcAccount, RpcBlockCommitment, RpcBlockhashFeeCalculator, RpcConfirmedBlock, - RpcContactInfo, RpcEpochInfo, RpcFeeRateGovernor, RpcIdentity, RpcKeyedAccount, - RpcLeaderSchedule, RpcResponseContext, RpcSignatureConfirmation, RpcStorageTurn, - RpcTransactionEncoding, RpcVersionInfo, RpcVoteAccountInfo, RpcVoteAccountStatus, + RpcContactInfo, RpcEpochInfo, RpcFeeCalculator, RpcFeeRateGovernor, RpcIdentity, + RpcKeyedAccount, RpcLeaderSchedule, RpcResponseContext, RpcSignatureConfirmation, + RpcStorageTurn, RpcTransactionEncoding, RpcVersionInfo, RpcVoteAccountInfo, + RpcVoteAccountStatus, }; use solana_faucet::faucet::request_airdrop_transaction; use solana_ledger::{ @@ -33,6 +34,7 @@ use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; use std::{ collections::HashMap, net::{SocketAddr, UdpSocket}, + str::FromStr, sync::{Arc, RwLock}, thread::sleep, time::{Duration, Instant}, @@ -165,6 +167,18 @@ impl JsonRpcRequestProcessor { ) } + fn get_fee_calculator_for_blockhash( + &self, + blockhash: &Hash, + ) -> RpcResponse> { + let bank = &*self.bank(None); + let fee_calculator = bank.get_fee_calculator(blockhash); + new_response( + bank, + fee_calculator.map(|fee_calculator| RpcFeeCalculator { fee_calculator }), + ) + } + fn get_fee_rate_governor(&self) -> RpcResponse { let bank = &*self.bank(None); let fee_rate_governor = bank.get_fee_rate_governor(); @@ -503,6 +517,13 @@ pub trait RpcSol { commitment: Option, ) -> RpcResponse; + #[rpc(meta, name = "getFeeCalculatorForBlockhash")] + fn get_fee_calculator_for_blockhash( + &self, + meta: Self::Metadata, + blockhash: String, + ) -> RpcResponse>; + #[rpc(meta, name = "getFeeRateGovernor")] fn get_fee_rate_governor(&self, meta: Self::Metadata) -> RpcResponse; @@ -831,6 +852,20 @@ impl RpcSol for RpcSolImpl { .get_recent_blockhash(commitment) } + fn get_fee_calculator_for_blockhash( + &self, + meta: Self::Metadata, + blockhash: String, + ) -> RpcResponse> { + debug!("get_fee_calculator_for_blockhash rpc request received"); + let blockhash = + Hash::from_str(&blockhash).map_err(|e| Error::invalid_params(format!("{:?}", e)))?; + meta.request_processor + .read() + .unwrap() + .get_fee_calculator_for_blockhash(&blockhash) + } + fn get_fee_rate_governor(&self, meta: Self::Metadata) -> RpcResponse { debug!("get_fee_rate_governor rpc request received"); meta.request_processor @@ -1821,6 +1856,54 @@ pub mod tests { assert_eq!(expected, result); } + #[test] + fn test_rpc_get_fee_calculator_for_blockhash() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); + + let (blockhash, fee_calculator) = bank.last_blockhash_with_fee_calculator(); + let fee_calculator = RpcFeeCalculator { fee_calculator }; + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getFeeCalculatorForBlockhash","params":["{:?}"]}}"#, + blockhash + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":fee_calculator, + }, + "id": 1 + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + + // Expired (non-existent) blockhash + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getFeeCalculatorForBlockhash","params":["{:?}"]}}"#, + Hash::default() + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":Value::Null, + }, + "id": 1 + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + } + #[test] fn test_rpc_get_fee_rate_governor() { let bob_pubkey = Pubkey::new_rand(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index bce34dbdd..14fa8c294 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -24,6 +24,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getConfirmedBlocks](jsonrpc-api.md#getconfirmedblocks) * [getEpochInfo](jsonrpc-api.md#getepochinfo) * [getEpochSchedule](jsonrpc-api.md#getepochschedule) +* [getFeeCalculatorForBlockhash](jsonrpc-api.md#getfeecalculatorforblockhash) * [getFeeRateGovernor](jsonrpc-api.md#getfeerategovernor) * [getGenesisHash](jsonrpc-api.md#getgenesishash) * [getIdentity](jsonrpc-api.md#getidentity) @@ -405,6 +406,30 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":{"firstNormalEpoch":8,"firstNormalSlot":8160,"leaderScheduleSlotOffset":8192,"slotsPerEpoch":8192,"warmup":true},"id":1} ``` +### getFeeCalculatorForBlockhash + +Returns the fee calculator associated with the query blockhash, or `null` if the blockhash has expired + +#### Parameters: + +* `blockhash: `, query blockhash as a Base58 encoded string + +#### Results: + +The `result` field will be `null` if the query blockhash has expired, otherwise an `object` with the following fields: + +* `feeCalculator: `, `FeeCalculator` object describing the cluster fee rate at the queried blockhash + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getFeeCalculatorForBlockhash", "params":["GJxqhuxcgfn5Tcj6y3f8X4FeCDd2RQ6SnEMo1AAxrPRZ"]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":221},"value":{"feeCalculator":{"lamportsPerSignature":5000}}},"id":1} +``` + ### getFeeRateGovernor Returns the fee rate governor information from the root bank diff --git a/runtime/src/bank_client.rs b/runtime/src/bank_client.rs index 51f57cc81..bfee83648 100644 --- a/runtime/src/bank_client.rs +++ b/runtime/src/bank_client.rs @@ -137,6 +137,10 @@ impl SyncClient for BankClient { Ok(self.bank.last_blockhash_with_fee_calculator()) } + fn get_fee_calculator_for_blockhash(&self, blockhash: &Hash) -> Result> { + Ok(self.bank.get_fee_calculator(blockhash)) + } + fn get_fee_rate_governor(&self) -> Result { Ok(self.bank.get_fee_rate_governor().clone()) } diff --git a/sdk/src/client.rs b/sdk/src/client.rs index 48f0bcf3b..714b1282f 100644 --- a/sdk/src/client.rs +++ b/sdk/src/client.rs @@ -72,6 +72,10 @@ pub trait SyncClient { commitment_config: CommitmentConfig, ) -> Result<(Hash, FeeCalculator)>; + /// Get `Some(FeeCalculator)` associated with `blockhash` if it is still in + /// the BlockhashQueue`, otherwise `None` + fn get_fee_calculator_for_blockhash(&self, blockhash: &Hash) -> Result>; + /// Get recent fee rate governor fn get_fee_rate_governor(&self) -> Result;