v0.2.3-pre.007
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 127
|
# version: 128
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.3-pre.6.fix.1"
|
version = "0.2.3-pre.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {
|
||||||
|
"code": -32602,
|
||||||
|
"message": "invalid transaction: failed to sanitize accounts offsets correctly"
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"context": {
|
||||||
|
"slot": 431000031,
|
||||||
|
"apiVersion": "4.2.1"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"err": null,
|
||||||
|
"logs": null,
|
||||||
|
"accounts": null,
|
||||||
|
"unitsConsumed": 99,
|
||||||
|
"returnData": null,
|
||||||
|
"replacementBlockhash": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"context": {
|
||||||
|
"slot": 431000030,
|
||||||
|
"apiVersion": "4.2.1"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"err": null,
|
||||||
|
"logs": [
|
||||||
|
"Program 11111111111111111111111111111111 invoke [1]",
|
||||||
|
"Program 11111111111111111111111111111111 success"
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"lamports": 1000,
|
||||||
|
"data": [
|
||||||
|
"",
|
||||||
|
"base64"
|
||||||
|
],
|
||||||
|
"owner": "11111111111111111111111111111111",
|
||||||
|
"executable": false,
|
||||||
|
"rentEpoch": 0,
|
||||||
|
"space": 0
|
||||||
|
},
|
||||||
|
null
|
||||||
|
],
|
||||||
|
"unitsConsumed": 150,
|
||||||
|
"loadedAccountsDataSize": 64,
|
||||||
|
"returnData": {
|
||||||
|
"programId": "11111111111111111111111111111111",
|
||||||
|
"data": [
|
||||||
|
"AQI=",
|
||||||
|
"base64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"innerInstructions": [],
|
||||||
|
"replacementBlockhash": {
|
||||||
|
"blockhash": "ComputeBudget111111111111111111111111111111",
|
||||||
|
"lastValidBlockHeight": 430123999
|
||||||
|
},
|
||||||
|
"fee": 5000,
|
||||||
|
"preBalances": [
|
||||||
|
10000,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"postBalances": [
|
||||||
|
5000,
|
||||||
|
5000
|
||||||
|
],
|
||||||
|
"preTokenBalances": [],
|
||||||
|
"postTokenBalances": [],
|
||||||
|
"loadedAddresses": {
|
||||||
|
"writable": [],
|
||||||
|
"readonly": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 15
|
// version: 16
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
|
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
|
||||||
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
|
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
|
||||||
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
|
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
|
||||||
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives, all eight read wrappers and both write
|
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through
|
||||||
//! submissions through `pre.006`, including complete modern/legacy `getTransaction` coverage and centralized no-resend protection for writes.
|
//! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete
|
||||||
//! `simulateTransaction` and the `0.2.4` family remain staged.
|
//! modern/legacy `getTransaction` coverage. The `0.2.4` family remains staged.
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
|
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
/// Binary encoding accepted for serialized transaction input payloads.
|
/// Binary encoding accepted for serialized transaction input payloads.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -452,7 +452,6 @@ impl SolanaSimulationAccountsConfig {
|
|||||||
|
|
||||||
/// Serializes the nested account config to the Solana JSON-RPC wire object.
|
/// Serializes the nested account config to the Solana JSON-RPC wire object.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||||
let mut object = serde_json::Map::new();
|
let mut object = serde_json::Map::new();
|
||||||
if let std::option::Option::Some(encoding) = self.encoding {
|
if let std::option::Option::Some(encoding) = self.encoding {
|
||||||
@@ -545,7 +544,6 @@ impl SolanaSimulateTransactionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the simulation config would serialize to an empty object.
|
/// Returns whether the simulation config would serialize to an empty object.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn is_empty(&self) -> bool {
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
return self.commitment.is_none()
|
return self.commitment.is_none()
|
||||||
&& self.encoding.is_none()
|
&& self.encoding.is_none()
|
||||||
@@ -558,7 +556,6 @@ impl SolanaSimulateTransactionConfig {
|
|||||||
|
|
||||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||||
let mut object = serde_json::Map::new();
|
let mut object = serde_json::Map::new();
|
||||||
if let std::option::Option::Some(commitment) = self.commitment {
|
if let std::option::Option::Some(commitment) = self.commitment {
|
||||||
@@ -1050,7 +1047,6 @@ impl SolanaSimulateTransactionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes a simulation result while preserving optional/nullable current Agave fields.
|
/// Decodes a simulation result while preserving optional/nullable current Agave fields.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<WireSimulateTransactionResult>(method, value);
|
let decoded = crate::decode_wire_json::<WireSimulateTransactionResult>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -1357,6 +1353,65 @@ impl crate::HttpTransportPool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes typed `simulateTransaction` through the common KSP HTTP transport path.
|
||||||
|
///
|
||||||
|
/// Transport forwards the already encoded transaction as opaque text. The wrapper enforces only deterministic RPC invariants that do not require
|
||||||
|
/// decoding transaction bytes: `sigVerify` cannot be combined with `replaceRecentBlockhash`, and simulation account-return encoding cannot use the
|
||||||
|
/// legacy `binary` / `base58` account encodings rejected by the targeted Agave runtime.
|
||||||
|
pub async fn simulate_transaction(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
transaction: &str,
|
||||||
|
config: std::option::Option<&crate::SolanaSimulateTransactionConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaSimulateTransactionResult>> {
|
||||||
|
if let std::option::Option::Some(config) = config {
|
||||||
|
if config.sig_verify() == std::option::Option::Some(true) && config.replace_recent_blockhash() == std::option::Option::Some(true) {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
||||||
|
"simulateTransaction sigVerify may not be used with replaceRecentBlockhash",
|
||||||
|
)
|
||||||
|
.with_context("rpc_method", "simulateTransaction"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(accounts) = config.accounts()
|
||||||
|
&& let std::option::Option::Some(encoding) = accounts.encoding()
|
||||||
|
&& (encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::Base58)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
||||||
|
"simulateTransaction account-return encoding must be base64, base64+zstd, or jsonParsed",
|
||||||
|
)
|
||||||
|
.with_context("rpc_method", "simulateTransaction")
|
||||||
|
.with_context("accounts_encoding", encoding.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut params = std::vec![serde_json::Value::String(transaction.to_owned())];
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& !config.is_empty()
|
||||||
|
{
|
||||||
|
params.push(config.to_json_value());
|
||||||
|
}
|
||||||
|
let value = self.execute_transaction_rpc("simulateTransaction", role, params).await;
|
||||||
|
let value = match value {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let contextual = decode_transaction_contextual_wire("simulateTransaction", value);
|
||||||
|
let (context, value) = match contextual {
|
||||||
|
std::result::Result::Ok(contextual) => contextual,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let result = crate::SolanaSimulateTransactionResult::decode_wire("simulateTransaction", value);
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(result) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, result)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes typed `getTransactionCount` through the common KSP HTTP transport path.
|
/// Executes typed `getTransactionCount` through the common KSP HTTP transport path.
|
||||||
pub async fn get_transaction_count(
|
pub async fn get_transaction_count(
|
||||||
&self,
|
&self,
|
||||||
@@ -1612,7 +1667,6 @@ fn decode_transaction_version_field(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn decode_simulation_accounts_field(
|
fn decode_simulation_accounts_field(
|
||||||
method: &str,
|
method: &str,
|
||||||
field: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
|
field: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
|
||||||
@@ -1640,7 +1694,6 @@ fn decode_simulation_accounts_field(
|
|||||||
return std::result::Result::Ok(crate::SolanaWireField::Value(accounts));
|
return std::result::Result::Ok(crate::SolanaWireField::Value(accounts));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn decode_replacement_blockhash_field(
|
fn decode_replacement_blockhash_field(
|
||||||
method: &str,
|
method: &str,
|
||||||
field: crate::SolanaWireField<serde_json::Value>,
|
field: crate::SolanaWireField<serde_json::Value>,
|
||||||
@@ -1716,7 +1769,6 @@ struct WireConfirmedTransaction {
|
|||||||
transaction_index: crate::SolanaWireField<u32>,
|
transaction_index: crate::SolanaWireField<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct WireSimulateTransactionResult {
|
struct WireSimulateTransactionResult {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
// version: 14
|
// version: 15
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -321,3 +321,29 @@ fn public_transaction_pre_006_write_wrappers_and_complete_configs_are_available_
|
|||||||
assert_eq!(send.max_retries(), std::option::Option::Some(5));
|
assert_eq!(send.max_retries(), std::option::Option::Some(5));
|
||||||
assert_eq!(send.min_context_slot(), std::option::Option::Some(431_000_000));
|
assert_eq!(send.min_context_slot(), std::option::Option::Some(431_000_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_transaction_pre_007_simulation_wrapper_and_complete_config_are_available_from_crate_root() {
|
||||||
|
let _simulate_transaction = ksp_onchain_transport_lib::HttpTransportPool::simulate_transaction;
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let accounts = ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
|
||||||
|
std::vec![address],
|
||||||
|
);
|
||||||
|
let config = ksp_onchain_transport_lib::SolanaSimulateTransactionConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(431_000_000),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(accounts),
|
||||||
|
);
|
||||||
|
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed));
|
||||||
|
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
|
||||||
|
assert_eq!(config.sig_verify(), std::option::Option::Some(true));
|
||||||
|
assert_eq!(
|
||||||
|
config.accounts().and_then(ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::encoding),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
// version: 11
|
// version: 12
|
||||||
|
|
||||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ fn release_pre_007_v0_2_2_exact_set_is_complete_without_advancing_future_release
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact_before_wrapper_completion() {
|
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact() {
|
||||||
let mut expected = std::vec![
|
let mut expected = std::vec![
|
||||||
"getFeeForMessage",
|
"getFeeForMessage",
|
||||||
"getLatestBlockhash",
|
"getLatestBlockhash",
|
||||||
@@ -359,3 +359,44 @@ fn release_pre_006_transaction_subset_adds_both_write_submissions_without_advanc
|
|||||||
assert_eq!(reads.len(), 8);
|
assert_eq!(reads.len(), 8);
|
||||||
assert_eq!(writes.len(), 2);
|
assert_eq!(writes.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_pre_007_completes_all_v0_2_3_transaction_wrappers_with_exact_safety_classes() {
|
||||||
|
let reads = std::vec![
|
||||||
|
"getFeeForMessage",
|
||||||
|
"getLatestBlockhash",
|
||||||
|
"getRecentPrioritizationFees",
|
||||||
|
"getSignaturesForAddress",
|
||||||
|
"getSignatureStatuses",
|
||||||
|
"getTransaction",
|
||||||
|
"getTransactionCount",
|
||||||
|
"isBlockhashValid",
|
||||||
|
];
|
||||||
|
let writes = std::vec!["requestAirdrop", "sendTransaction"];
|
||||||
|
let simulations = std::vec!["simulateTransaction"];
|
||||||
|
for method_name in &reads {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 read descriptor must exist");
|
||||||
|
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||||
|
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||||
|
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||||
|
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||||
|
}
|
||||||
|
for method_name in &writes {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 write descriptor must exist");
|
||||||
|
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||||
|
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||||
|
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission);
|
||||||
|
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||||
|
}
|
||||||
|
for method_name in &simulations {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 simulation descriptor must exist");
|
||||||
|
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||||
|
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||||
|
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
|
||||||
|
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||||
|
}
|
||||||
|
assert_eq!(reads.len(), 8);
|
||||||
|
assert_eq!(writes.len(), 2);
|
||||||
|
assert_eq!(simulations.len(), 1);
|
||||||
|
assert_eq!(reads.len() + writes.len() + simulations.len(), 11);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
||||||
@@ -391,6 +391,42 @@ fn serve_transaction_timeout_and_count(delay: std::time::Duration) -> (std::stri
|
|||||||
return (format!("http://{address}"), handle);
|
return (format!("http://{address}"), handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serve_transaction_status_then_body(
|
||||||
|
status_line: &'static str,
|
||||||
|
body: &'static str,
|
||||||
|
) -> (std::string::String, std::thread::JoinHandle<(std::string::String, 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 first_stream, _) = listener.accept().expect("fixture server must accept first request");
|
||||||
|
let first_request = read_transaction_request(&mut first_stream);
|
||||||
|
let first_response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||||
|
std::io::Write::write_all(&mut first_stream, first_response.as_bytes()).expect("fixture first response must write");
|
||||||
|
|
||||||
|
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
|
||||||
|
return loop {
|
||||||
|
match listener.accept() {
|
||||||
|
std::result::Result::Ok((mut retry_stream, _)) => {
|
||||||
|
let retry_request = read_transaction_request(&mut retry_stream);
|
||||||
|
let retry_response =
|
||||||
|
format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body,);
|
||||||
|
std::io::Write::write_all(&mut retry_stream, retry_response.as_bytes()).expect("fixture retry response must write");
|
||||||
|
break (first_request, retry_request);
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
if std::time::Instant::now() >= deadline {
|
||||||
|
panic!("fixture server did not receive retry before deadline");
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => panic!("fixture listener failed while waiting for retry: {error}"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (format!("http://{address}"), handle);
|
||||||
|
}
|
||||||
|
|
||||||
fn unused_local_url() -> std::string::String {
|
fn unused_local_url() -> std::string::String {
|
||||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("unused-port probe must bind");
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("unused-port probe must bind");
|
||||||
let address = listener.local_addr().expect("unused-port probe address must resolve");
|
let address = listener.local_addr().expect("unused-port probe address must resolve");
|
||||||
@@ -1139,3 +1175,193 @@ async fn typed_write_wrappers_can_retry_when_connection_failure_proves_not_dispa
|
|||||||
let request = handle.join().expect("secondary fixture server must join");
|
let request = handle.join().expect("secondary fixture server must join");
|
||||||
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("sendTransaction"));
|
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("sendTransaction"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_serializes_complete_config_and_preserves_rich_result() {
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(crate::SolanaAccountEncoding::Base64), std::vec![address]);
|
||||||
|
let config = crate::SolanaSimulateTransactionConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(431_000_000),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(accounts),
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let response = pool
|
||||||
|
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-base64-transaction", std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("simulateTransaction rich fixture must succeed");
|
||||||
|
assert_eq!(response.context().slot(), 431_000_030);
|
||||||
|
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
|
||||||
|
assert!(response.value().err().is_null());
|
||||||
|
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&150));
|
||||||
|
assert_eq!(response.value().loaded_accounts_data_size().value(), std::option::Option::Some(&64));
|
||||||
|
assert_eq!(response.value().fee().value(), std::option::Option::Some(&5_000));
|
||||||
|
assert_eq!(response.value().pre_balances().value(), std::option::Option::Some(&std::vec![10_000, 0]));
|
||||||
|
assert_eq!(response.value().post_balances().value(), std::option::Option::Some(&std::vec![5_000, 5_000]));
|
||||||
|
let returned_accounts = response.value().accounts().value().expect("simulation accounts must be present");
|
||||||
|
assert_eq!(returned_accounts.len(), 2);
|
||||||
|
assert!(returned_accounts[0].is_some());
|
||||||
|
assert!(returned_accounts[1].is_none());
|
||||||
|
let replacement = response.value().replacement_blockhash().value().expect("replacement blockhash must be present");
|
||||||
|
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
|
||||||
|
assert_eq!(replacement.last_valid_block_height(), 430_123_999);
|
||||||
|
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("simulateTransaction"));
|
||||||
|
assert_eq!(
|
||||||
|
body["params"],
|
||||||
|
serde_json::json!([
|
||||||
|
"opaque-base64-transaction",
|
||||||
|
{
|
||||||
|
"commitment":"confirmed",
|
||||||
|
"encoding":"base64",
|
||||||
|
"replaceRecentBlockhash":true,
|
||||||
|
"sigVerify":false,
|
||||||
|
"minContextSlot":431000000,
|
||||||
|
"innerInstructions":true,
|
||||||
|
"accounts":{"encoding":"base64","addresses":["11111111111111111111111111111111"]}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_supports_base58_and_omits_empty_config() {
|
||||||
|
let base58 = crate::SolanaSimulateTransactionConfig::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base58),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let _ = pool
|
||||||
|
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-base58-transaction", std::option::Option::Some(&base58))
|
||||||
|
.await
|
||||||
|
.expect("simulateTransaction base58 fixture must succeed");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
|
||||||
|
|
||||||
|
let empty = crate::SolanaSimulateTransactionConfig::default();
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let _ = pool
|
||||||
|
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-default-transaction", std::option::Option::Some(&empty))
|
||||||
|
.await
|
||||||
|
.expect("simulateTransaction empty config must canonicalize to omission");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-default-transaction"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_supports_all_current_account_return_encodings() {
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let encodings = [crate::SolanaAccountEncoding::Base64, crate::SolanaAccountEncoding::Base64Zstd, crate::SolanaAccountEncoding::JsonParsed];
|
||||||
|
for encoding in encodings {
|
||||||
|
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(encoding), std::vec![address]);
|
||||||
|
let config = crate::SolanaSimulateTransactionConfig::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(accounts),
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let _ = pool
|
||||||
|
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("supported simulation account encoding must execute");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["params"][1]["accounts"]["encoding"], serde_json::json!(encoding.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_rejects_deterministic_invalid_configs_before_io() {
|
||||||
|
let pool = transaction_pool_for_url(unused_local_url().as_str());
|
||||||
|
let conflicting = crate::SolanaSimulateTransactionConfig::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&conflicting)).await;
|
||||||
|
assert_eq!(result.expect_err("conflicting simulation flags must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58];
|
||||||
|
for encoding in rejected_encodings {
|
||||||
|
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(encoding), std::vec![address]);
|
||||||
|
let config = crate::SolanaSimulateTransactionConfig::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(accounts),
|
||||||
|
);
|
||||||
|
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&config)).await;
|
||||||
|
assert_eq!(result.expect_err("unsupported simulation account encoding must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_preserves_partial_null_and_omitted_result() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let response = pool
|
||||||
|
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("partial simulateTransaction fixture must succeed");
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
assert!(response.value().err().is_null());
|
||||||
|
assert!(response.value().logs().is_null());
|
||||||
|
assert!(response.value().accounts().is_null());
|
||||||
|
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&99));
|
||||||
|
assert!(response.value().loaded_accounts_data_size().is_omitted());
|
||||||
|
assert!(response.value().return_data().is_null());
|
||||||
|
assert!(response.value().inner_instructions().is_omitted());
|
||||||
|
assert!(response.value().replacement_blockhash().is_null());
|
||||||
|
assert!(response.value().fee().is_omitted());
|
||||||
|
assert!(response.value().loaded_addresses().is_omitted());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_preserves_rpc_application_error() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.error.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "invalid-opaque-transaction", std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("simulation RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_simulate_transaction_retries_temporary_http_failure_safely() {
|
||||||
|
let (url, handle) = serve_transaction_status_then_body("503 Service Unavailable", include_str!("../fixtures/http/simulate_transaction.partial.json"));
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 1);
|
||||||
|
let response = pool
|
||||||
|
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-retry-safe-transaction", std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("retry-safe simulation must recover after temporary HTTP failure");
|
||||||
|
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&99));
|
||||||
|
let (first, second) = handle.join().expect("retry fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(first.as_str())["method"], serde_json::json!("simulateTransaction"));
|
||||||
|
assert_eq!(transaction_request_body(second.as_str())["method"], serde_json::json!("simulateTransaction"));
|
||||||
|
}
|
||||||
|
|||||||
313
deltas/0.2.3/pre.007.md
Normal file
313
deltas/0.2.3/pre.007.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
<!-- file: deltas/0.2.3/pre.007.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.3-pre.007` — `simulateTransaction` complet
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente corrigée et validée localement par l'opérateur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.006-fix.001
|
||||||
|
workspace.package.version = "0.2.3-pre.6.fix.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation opérateur du 2026-08-18 a confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats Transport de cette base :
|
||||||
|
|
||||||
|
```text
|
||||||
|
175 unit tests
|
||||||
|
18 public API tests
|
||||||
|
12 release completeness tests
|
||||||
|
1 smoke Devnet ignored / opt-in
|
||||||
|
0 warning signalé par check/clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Activer la dernière méthode Transactions de `0.2.3` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
simulateTransaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Après cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
11 / 11 wrappers Transactions exécutés
|
||||||
|
8 Read / RetrySafe
|
||||||
|
2 WriteSubmission / NeverAfterDispatch
|
||||||
|
1 Simulation / RetrySafe
|
||||||
|
```
|
||||||
|
|
||||||
|
La tranche ne décode ni ne construit une transaction Solana et n'ajoute aucune dépendance codec/SDK.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Conformément à `VER-ID-009` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.6.fix.1 -> 0.2.3-pre.7
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` passe de `version: 127` à `version: 128`.
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n'est ajoutée.
|
||||||
|
|
||||||
|
## Complétude `KSP-TRANSPORT-007`
|
||||||
|
|
||||||
|
La page Solana RPC courante documente pour `simulateTransaction` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction string
|
||||||
|
commitment
|
||||||
|
encoding = base58 | base64
|
||||||
|
replaceRecentBlockhash
|
||||||
|
sigVerify
|
||||||
|
minContextSlot
|
||||||
|
innerInstructions
|
||||||
|
accounts
|
||||||
|
```
|
||||||
|
|
||||||
|
La source primaire Agave `v4.2.1` confirme les mêmes champs dans `RpcSimulateTransactionConfig`, avec des booléens runtime par défaut à `false`.
|
||||||
|
|
||||||
|
KSP expose donc la config complète via `SolanaSimulateTransactionConfig`. Les booléens restent `Option<bool>` côté KSP afin de distinguer l'omission de
|
||||||
|
l'envoi explicite de `false` sans modifier la sémantique RPC.
|
||||||
|
|
||||||
|
Les deux encodings de transaction réellement acceptés sont couverts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
base58
|
||||||
|
base64
|
||||||
|
```
|
||||||
|
|
||||||
|
La transaction reste une chaîne opaque : Transport ne tente pas de reproduire le décodage, la désérialisation ou la sanitization Agave.
|
||||||
|
|
||||||
|
## Sous-config `accounts`
|
||||||
|
|
||||||
|
Agave `v4.2.1` utilise `RpcSimulateTransactionAccountsConfig` avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
encoding: Option<UiAccountEncoding>
|
||||||
|
addresses: Vec<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
La source runtime rejette explicitement les encodings Account legacy :
|
||||||
|
|
||||||
|
```text
|
||||||
|
binary
|
||||||
|
base58
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP accepte donc les variantes actuellement compatibles avec ce retour :
|
||||||
|
|
||||||
|
```text
|
||||||
|
base64
|
||||||
|
base64+zstd
|
||||||
|
jsonParsed
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces trois variantes sont exercées par tests HTTP. `binary` et `base58` sont rejetées avant I/O avec `ERROR_CODE_INVALID_RPC_PARAMETERS`.
|
||||||
|
|
||||||
|
Quand aucun encoding Account n'est fourni, KSP omet le champ et laisse le runtime appliquer son défaut `base64`.
|
||||||
|
|
||||||
|
Agave borne dynamiquement le nombre d'adresses demandées au nombre de comptes contenu dans la transaction. Comme KSP ne décode pas la transaction dans
|
||||||
|
cette release, cette limite reste volontairement côté runtime/provider au lieu d'introduire une validation partielle ou un codec transactionnel.
|
||||||
|
|
||||||
|
## Invariant `sigVerify` / `replaceRecentBlockhash`
|
||||||
|
|
||||||
|
Agave `v4.2.1` refuse explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
sigVerify = true
|
||||||
|
replaceRecentBlockhash = true
|
||||||
|
```
|
||||||
|
|
||||||
|
avec l'erreur de paramètres indiquant que `sigVerify` ne peut pas être utilisé avec `replaceRecentBlockhash`.
|
||||||
|
|
||||||
|
Cet invariant est déterministe et ne dépend pas du contenu encodé de la transaction. KSP le rejette donc avant toute sélection/exécution HTTP.
|
||||||
|
|
||||||
|
## Résultat riche et lossless
|
||||||
|
|
||||||
|
`SolanaSimulateTransactionResult` devient le décodeur production du résultat contextualisé.
|
||||||
|
|
||||||
|
Les champs courants Agave `v4.2.1` sont tous préservés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
err
|
||||||
|
logs
|
||||||
|
accounts
|
||||||
|
unitsConsumed
|
||||||
|
loadedAccountsDataSize
|
||||||
|
returnData
|
||||||
|
innerInstructions
|
||||||
|
replacementBlockhash
|
||||||
|
fee
|
||||||
|
preBalances
|
||||||
|
postBalances
|
||||||
|
preTokenBalances
|
||||||
|
postTokenBalances
|
||||||
|
loadedAddresses
|
||||||
|
```
|
||||||
|
|
||||||
|
La page publique Solana résume seulement un sous-ensemble de ces champs. KSP conserve également les champs runtime supplémentaires afin de respecter
|
||||||
|
`KSP-TRANSPORT-007` et de ne pas perdre de données disponibles.
|
||||||
|
|
||||||
|
Les champs optionnels utilisent `SolanaWireField<T>` lorsque l'absence, le `null` explicite et la valeur concrète doivent rester distinguables :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Omitted
|
||||||
|
Null
|
||||||
|
Value(T)
|
||||||
|
```
|
||||||
|
|
||||||
|
Les comptes retournés réutilisent `SolanaAccount` et conservent les `null` positionnels. Les sous-arbres Program/SPL riches (`returnData`, inner
|
||||||
|
instructions, token balances, loaded addresses) restent lossless via `serde_json::Value` aux frontières déjà prévues, sans imposer les crates
|
||||||
|
transaction-status à Transport.
|
||||||
|
|
||||||
|
## Retry de simulation
|
||||||
|
|
||||||
|
Le descriptor existant reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RpcOperationKind::Simulation
|
||||||
|
TransportRetryClass::RetrySafe
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrairement à `requestAirdrop` et `sendTransaction`, la simulation n'est pas soumise à la règle no-resend d'une écriture : elle ne diffuse pas la
|
||||||
|
transaction au cluster.
|
||||||
|
|
||||||
|
Un test end-to-end exerce réellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
simulate_transaction
|
||||||
|
-> descriptor central
|
||||||
|
-> execute_standard_rpc
|
||||||
|
-> HTTP 503
|
||||||
|
-> retry central
|
||||||
|
-> HTTP 200
|
||||||
|
-> résultat typé
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test démontre donc que la classification `RetrySafe` est utilisée par le vrai chemin wrapper/executor et pas uniquement par une fonction pure de
|
||||||
|
policy.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
Sept tests unitaires ajoutent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
config complète + résultat riche
|
||||||
|
base58 + canonicalisation config vide
|
||||||
|
base64 / base64+zstd / jsonParsed pour accounts
|
||||||
|
rejet avant I/O du conflit sigVerify/replaceRecentBlockhash et de binary/base58 account encoding
|
||||||
|
préservation omitted/null/value d'un résultat partiel
|
||||||
|
préservation d'une erreur RPC applicative
|
||||||
|
retry sûr après HTTP 503 puis succès
|
||||||
|
```
|
||||||
|
|
||||||
|
Un test public vérifie la disponibilité du wrapper et de la config complète à la racine de crate.
|
||||||
|
|
||||||
|
Une canarie release vérifie la couverture finale exacte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
8 Read / RetrySafe
|
||||||
|
2 WriteSubmission / NeverAfterDispatch
|
||||||
|
1 Simulation / RetrySafe
|
||||||
|
11 / 11 méthodes Transactions V0_2_3
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultat attendu après application :
|
||||||
|
|
||||||
|
```text
|
||||||
|
182 unit tests
|
||||||
|
19 public API tests
|
||||||
|
13 release completeness tests
|
||||||
|
1 smoke Devnet ignored / opt-in
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation synchronisée
|
||||||
|
|
||||||
|
`crates/ksp-onchain-transport-lib/src/lib.rs` indique désormais que les 11 wrappers Transactions sont actifs après `pre.007`.
|
||||||
|
|
||||||
|
`docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md` passe en version 6 et enregistre le résultat de la tranche.
|
||||||
|
|
||||||
|
Le planning reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.008 audit rétroactif KSP-TRANSPORT-007 sur 0.2.1 -> 0.2.3 + remédiations éventuelles
|
||||||
|
pre.009 clôture finale documentaire/validation/smoke/prompt 0.2.4
|
||||||
|
```
|
||||||
|
|
||||||
|
`CHANGELOG.md` reste réservé à `0.2.3-rel.001`.
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/simulate_transaction.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/simulate_transaction.partial.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/simulate_transaction.error.json
|
||||||
|
deltas/0.2.3/pre.007.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
|
||||||
|
Aucune dépendance ajoutée, supprimée ou modifiée.
|
||||||
|
|
||||||
|
En particulier, restent absentes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
base64 direct dans Transport
|
||||||
|
bs58 direct dans Transport
|
||||||
|
wincode
|
||||||
|
solana-client
|
||||||
|
crate RPC SDK
|
||||||
|
transaction-status client types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations à exécuter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune commande n'est déclarée réussie sans exécution réelle ou preuve opérateur.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après validation et commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.3-pre.007
|
||||||
|
```
|
||||||
|
|
||||||
|
passer à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.008
|
||||||
|
réaudit rétroactif KSP-TRANSPORT-007 sur les wrappers HTTP 0.2.1 -> 0.2.3
|
||||||
|
+ remédiations éventuelles avant la clôture
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md -->
|
<!-- file: docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 6 -->
|
||||||
|
|
||||||
# Plan `0.2.3` — HTTP Transactions
|
# Plan `0.2.3` — HTTP Transactions
|
||||||
|
|
||||||
@@ -725,3 +725,22 @@ La tranche suivante reste `pre.003` : `getFeeForMessage`, `getLatestBlockhash`,
|
|||||||
|
|
||||||
La prévision est ajustée : `pre.008` devient l'audit rétroactif `KSP-TRANSPORT-007` et la clôture finale est déplacée à `pre.009`.
|
La prévision est ajustée : `pre.008` devient l'audit rétroactif `KSP-TRANSPORT-007` et la clôture finale est déplacée à `pre.009`.
|
||||||
|
|
||||||
|
## Résultat de `pre.007`
|
||||||
|
|
||||||
|
`pre.007` active la dernière méthode Transaction, `simulateTransaction`, et porte donc la couverture typée `0.2.3` à **11/11** :
|
||||||
|
|
||||||
|
- transaction d'entrée conservée comme chaîne opaque, sans dépendance codec/SDK ;
|
||||||
|
- encodings d'entrée `base58` et `base64` couverts ;
|
||||||
|
- config complète conservée : `commitment`, `encoding`, `replaceRecentBlockhash`, `sigVerify`, `minContextSlot`, `innerInstructions`, `accounts` ;
|
||||||
|
- sous-config `accounts` capable d'émettre `base64`, `base64+zstd` et `jsonParsed`, avec rejet local de `binary/base58` conformément à Agave `v4.2.1` ;
|
||||||
|
- incompatibilité déterministe `sigVerify=true` + `replaceRecentBlockhash=true` rejetée avant I/O ;
|
||||||
|
- limite dynamique du nombre d'adresses `accounts` laissée au runtime puisque KSP ne décode pas la transaction ;
|
||||||
|
- résultat riche préservé losslessly, y compris les champs Agave `v4.2.1` au-delà du résumé documentaire public : `fee`, balances, token balances et loaded addresses ;
|
||||||
|
- `omitted/null/value` conservés pour les champs optionnels avec `SolanaWireField<T>` ;
|
||||||
|
- accounts de simulation décodés via le wire Account KSP existant, avec `null` positionnels conservés ;
|
||||||
|
- erreur RPC applicative conservée sans transformation en erreur transport ;
|
||||||
|
- descriptor central `Simulation / RetrySafe` exercé end-to-end par un scénario HTTP 503 puis succès, démontrant que la simulation peut être retentée ;
|
||||||
|
- canarie release exacte `8 Read / 2 WriteSubmission / 1 Simulation`, soit les 11 méthodes `V0_2_3`.
|
||||||
|
|
||||||
|
Aucune dépendance n'est ajoutée. Le travail fonctionnel initialement planifié pour les 11 Transactions est désormais implémenté ; `pre.008` reste volontairement
|
||||||
|
réservée au réaudit rétroactif `KSP-TRANSPORT-007` des wrappers HTTP `0.2.1` -> `0.2.3`, puis `pre.009` à la clôture.
|
||||||
|
|||||||
Reference in New Issue
Block a user