v0.3.10-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||
<!-- version: 24 -->
|
||||
<!-- version: 25 -->
|
||||
|
||||
# Utilisation de `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -449,7 +449,9 @@ let inflation_rate = pool.get_inflation_rate(&role).await;
|
||||
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
|
||||
```
|
||||
|
||||
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
|
||||
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Pour les consumers qui doivent conserver la provenance exacte d’un pool multi-endpoint, `get_block_observed(...)` exécute la même forme moderne et retourne la valeur typée avec le nom sûr de l’endpoint et le provider réellement gagnants après sélection/retry/reroute. Cette projection n’expose ni URL, ni headers, ni body HTTP brut. Une réponse RPC `null` reste `None` dans la valeur observée.
|
||||
|
||||
Lorsque `transactionDetails = full` et `encoding = base64`, chaque `SolanaBlockTransaction` conserve le transaction wire Base64, `meta` et `version`. Le consumer peut alors projeter ces DTOs vers sa couche métier sans faire dépendre Transport d’une canonicalisation RAW particulière. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
|
||||
|
||||
## 6. Exécution JSON-RPC standard générique
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[],"numRewardPartitions":0,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
|
||||
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
|
||||
@@ -591,25 +591,42 @@ impl crate::HttpTransportPool {
|
||||
slot: u64,
|
||||
config: std::option::Option<&crate::SolanaGetBlockConfig>,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
||||
"getBlock commitment must be confirmed or finalized when explicitly provided",
|
||||
)
|
||||
.with_context("rpc_method", "getBlock")
|
||||
.with_context("commitment", "processed"),
|
||||
);
|
||||
}
|
||||
let mut params = std::vec![serde_json::json!(slot)];
|
||||
if let std::option::Option::Some(config) = config {
|
||||
params.push((*config).to_json_value());
|
||||
}
|
||||
let params = get_block_params(slot, config);
|
||||
let params = match params {
|
||||
std::result::Result::Ok(params) => params,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return self.execute_get_block(role, params).await;
|
||||
}
|
||||
|
||||
/// Executes the current object-form `getBlock` request and reports the safe identity of the endpoint that produced the successful response.
|
||||
///
|
||||
/// Routing, admission, timeout and retry behavior are identical to [`Self::get_block`]. The returned observation never contains an endpoint URL,
|
||||
/// HTTP headers or a raw HTTP body.
|
||||
pub async fn get_block_observed(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
slot: u64,
|
||||
config: std::option::Option<&crate::SolanaGetBlockConfig>,
|
||||
) -> ksp_core_lib::Result<crate::HttpObservedValue<std::option::Option<crate::SolanaConfirmedBlock>>> {
|
||||
let params = get_block_params(slot, config);
|
||||
let params = match params {
|
||||
std::result::Result::Ok(params) => params,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let observed = self.execute_blocks_rpc_observed("getBlock", role, params).await;
|
||||
let observed = match observed {
|
||||
std::result::Result::Ok(observed) => observed,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let (value, endpoint_name, provider) = observed.into_parts();
|
||||
let block = decode_get_block(value);
|
||||
return match block {
|
||||
std::result::Result::Ok(block) => std::result::Result::Ok(crate::HttpObservedValue::new(block, endpoint_name, provider)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes the deprecated bare-encoding `getBlock` request form retained by Solana RPC for backwards compatibility.
|
||||
#[deprecated(note = "use HttpTransportPool::get_block with SolanaGetBlockConfig; the bare encoding request form is deprecated")]
|
||||
pub async fn get_block_legacy(
|
||||
@@ -635,16 +652,8 @@ impl crate::HttpTransportPool {
|
||||
params: std::vec::Vec<serde_json::Value>,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
||||
let value = self.execute_blocks_rpc("getBlock", role, params).await;
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if value.is_null() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value);
|
||||
return match block {
|
||||
std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)),
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => decode_get_block(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
@@ -829,6 +838,48 @@ impl crate::HttpTransportPool {
|
||||
};
|
||||
return self.execute_standard_rpc(role, method, params).await;
|
||||
}
|
||||
|
||||
async fn execute_blocks_rpc_observed(
|
||||
&self,
|
||||
method_name: &'static str,
|
||||
role: &crate::HttpRoleName,
|
||||
params: std::vec::Vec<serde_json::Value>,
|
||||
) -> ksp_core_lib::Result<crate::HttpObservedValue<serde_json::Value>> {
|
||||
let method = blocks_descriptor(method_name);
|
||||
let method = match method {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return self.execute_standard_rpc_observed(role, method, params).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_get_block(value: serde_json::Value) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
||||
if value.is_null() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value);
|
||||
return match block {
|
||||
std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn get_block_params(slot: u64, config: std::option::Option<&crate::SolanaGetBlockConfig>) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlock commitment must be confirmed or finalized when explicitly provided")
|
||||
.with_context("rpc_method", "getBlock")
|
||||
.with_context("commitment", "processed"),
|
||||
);
|
||||
}
|
||||
let mut params = std::vec![serde_json::json!(slot)];
|
||||
if let std::option::Option::Some(config) = config {
|
||||
params.push((*config).to_json_value());
|
||||
}
|
||||
return std::result::Result::Ok(params);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 49
|
||||
// version: 50
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -1021,3 +1021,10 @@ fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_
|
||||
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
|
||||
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_3_10_pre_004_observed_get_block_surface_is_available_from_crate_root() {
|
||||
let method = ksp_onchain_transport_lib::HttpTransportPool::get_block_observed;
|
||||
let _ = method;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
#[test]
|
||||
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
|
||||
@@ -188,6 +188,35 @@ fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHand
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn serve_status_and_count(status_line: &'static str) -> (std::string::String, std::thread::JoinHandle<(usize, std::string::String)>) {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("fixture server must accept first request");
|
||||
let first_request = read_request(&mut stream);
|
||||
let response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||
let mut count = 1_usize;
|
||||
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(120);
|
||||
while std::time::Instant::now() < deadline {
|
||||
match listener.accept() {
|
||||
std::result::Result::Ok((mut retry_stream, _)) => {
|
||||
let _ = read_request(&mut retry_stream);
|
||||
std::io::Write::write_all(&mut retry_stream, response.as_bytes()).expect("fixture retry response must write");
|
||||
count = count.saturating_add(1);
|
||||
},
|
||||
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
},
|
||||
std::result::Result::Err(error) => panic!("fixture listener failed while counting retries: {error}"),
|
||||
}
|
||||
}
|
||||
return (count, first_request);
|
||||
});
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
@@ -784,3 +813,92 @@ async fn typed_get_block_rejects_processed_commitment_before_io() {
|
||||
assert_eq!(error.context()[1].key(), "commitment");
|
||||
assert_eq!(error.context()[1].value(), "processed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_004_get_block_observed_reports_actual_winner_after_retry_reroute() {
|
||||
let (first_url, first_handle) = serve_status_and_count("429 Too Many Requests");
|
||||
let (winner_url, winner_handle) = serve_once(include_str!("../fixtures/http/get_block.observed_material.success.json"));
|
||||
let urls = [(first_url.as_str(), "first-endpoint", "first-provider"), (winner_url.as_str(), "winner-endpoint", "winner-provider")];
|
||||
let mut endpoints = std::vec::Vec::with_capacity(urls.len());
|
||||
for (url, endpoint_name, provider) in urls {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
endpoints.push(crate::HttpEndpointSettings::new(
|
||||
endpoint_name,
|
||||
true,
|
||||
crate::HttpProviderName::new(provider),
|
||||
crate::HttpClusterName::new("local"),
|
||||
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
));
|
||||
}
|
||||
let pool = crate::HttpTransportPool::new(crate::HttpTransportSettings::new(
|
||||
endpoints,
|
||||
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
))
|
||||
.expect("observed fixture pool must build");
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(0),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
let observed = pool
|
||||
.get_block_observed(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("retry-safe observed getBlock must succeed on the second endpoint");
|
||||
assert_eq!(observed.endpoint_name(), "winner-endpoint");
|
||||
assert_eq!(observed.provider().as_str(), "winner-provider");
|
||||
let block = observed.value().as_ref().expect("winning response must contain a block");
|
||||
let transactions = block.transactions().value().expect("winning block must contain transactions");
|
||||
assert_eq!(transactions.len(), 1);
|
||||
assert!(matches!(
|
||||
transactions[0].transaction(),
|
||||
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }
|
||||
));
|
||||
let rendered = format!("{observed:?}");
|
||||
assert!(rendered.contains("winner-endpoint"));
|
||||
assert!(rendered.contains("winner-provider"));
|
||||
assert!(rendered.contains("<available>"));
|
||||
assert!(!rendered.contains("AQAAAAAAAA"));
|
||||
let (first_count, first_request) = first_handle.join().expect("first fixture server must join");
|
||||
assert_eq!(first_count, 1);
|
||||
assert_eq!(request_body(first_request.as_str())["method"], serde_json::json!("getBlock"));
|
||||
let winner_request = winner_handle.join().expect("winner fixture server must join");
|
||||
assert_eq!(request_body(winner_request.as_str())["method"], serde_json::json!("getBlock"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_004_get_block_observed_preserves_null_and_reuses_get_block_validation() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let observed = pool
|
||||
.get_block_observed(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::None)
|
||||
.await
|
||||
.expect("observed null getBlock must succeed");
|
||||
assert!(observed.value().is_none());
|
||||
assert_eq!(observed.endpoint_name(), "fixture");
|
||||
assert_eq!(observed.provider().as_str(), "fixture");
|
||||
handle.join().expect("fixture server must join");
|
||||
let processed = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(0),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
let result = pool.get_block_observed(&crate::HttpRoleName::new("default"), 1, std::option::Option::Some(&processed)).await;
|
||||
let error = result.expect_err("processed observed getBlock commitment must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user