v0.2.7-pre.009
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 25
|
||||
// version: 26
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -23,6 +23,8 @@
|
||||
//! request/frame/message limits and control-frame handling. `0.2.7-pre.006` adds the typed subscription registry with stable local IDs and internal remote-ID
|
||||
//! routing. `0.2.7-pre.007` adds finite reconnect, deterministic resubscribe and continuity-gap tracking. `0.2.7-pre.008` makes per-subscription notification
|
||||
//! backpressure terminal and observable, preserves safe terminal error codes, performs best-effort remote cleanup and proves bounded capacity reuse.
|
||||
//! `0.2.7-pre.009` opens the first stable typed WebSocket wrappers for account, program-account and transaction-log subscriptions without exposing a raw
|
||||
//! provider-extension subscription API.
|
||||
|
||||
mod client;
|
||||
mod constants;
|
||||
@@ -41,10 +43,12 @@ mod rpc_method;
|
||||
mod rpc_tokens;
|
||||
mod rpc_transactions;
|
||||
mod settings;
|
||||
mod ws_accounts;
|
||||
mod ws_lifecycle;
|
||||
mod ws_session;
|
||||
mod ws_settings;
|
||||
mod ws_subscription;
|
||||
mod ws_transactions;
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||
pub use self::client::HttpEndpointAvailability;
|
||||
@@ -122,9 +126,9 @@ pub use self::resilience::evaluate_transport_retry;
|
||||
pub use self::rpc_accounts::SolanaAccount;
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaAccountBalance;
|
||||
/// Wire-preserving account data returned by Solana HTTP account methods.
|
||||
/// Wire-preserving account data returned by Solana account HTTP and WebSocket methods.
|
||||
pub use self::rpc_accounts::SolanaAccountData;
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
/// Account-data encoding accepted by Solana account HTTP and WebSocket methods.
|
||||
pub use self::rpc_accounts::SolanaAccountEncoding;
|
||||
/// Shared account configuration used by account-info and token-account list methods.
|
||||
pub use self::rpc_accounts::SolanaAccountInfoConfig;
|
||||
@@ -202,15 +206,15 @@ pub use self::rpc_cluster::SolanaVoteAccountInfo;
|
||||
pub use self::rpc_cluster::SolanaVoteAccountStatus;
|
||||
/// Configuration accepted by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountsConfig;
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
/// Commitment level accepted by typed Solana HTTP and WebSocket adapters.
|
||||
pub use self::rpc_common::SolanaCommitment;
|
||||
/// Optional commitment-only configuration shared by typed Solana HTTP RPC methods.
|
||||
/// Optional commitment-only configuration shared by typed Solana RPC methods.
|
||||
pub use self::rpc_common::SolanaCommitmentConfig;
|
||||
/// Optional commitment and minimum-context configuration shared by typed Solana HTTP RPC methods.
|
||||
pub use self::rpc_common::SolanaContextConfig;
|
||||
/// Typed Solana RPC context shared by contextual HTTP responses.
|
||||
/// Typed Solana RPC context shared by contextual HTTP and WebSocket responses.
|
||||
pub use self::rpc_common::SolanaRpcContext;
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
/// Generic contextual result returned by typed Solana HTTP and WebSocket adapters.
|
||||
pub use self::rpc_common::SolanaRpcResponse;
|
||||
/// Inflation-governor values returned by `getInflationGovernor`.
|
||||
pub use self::rpc_economics::SolanaInflationGovernor;
|
||||
@@ -310,6 +314,12 @@ pub use self::settings::HttpRoleLimits;
|
||||
pub use self::settings::HttpRoleName;
|
||||
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
||||
pub use self::settings::HttpTransportSettings;
|
||||
/// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method.
|
||||
pub use self::ws_accounts::SolanaAccountSubscribeConfig;
|
||||
/// One `programNotification` payload preserving contextual and non-contextual upstream forms.
|
||||
pub use self::ws_accounts::SolanaProgramNotification;
|
||||
/// Configuration accepted by the standard Solana `programSubscribe` WebSocket method.
|
||||
pub use self::ws_accounts::SolanaProgramSubscribeConfig;
|
||||
/// Stable local identity assigned to one physical WebSocket session.
|
||||
pub use self::ws_lifecycle::WsSessionId;
|
||||
/// Safe runtime snapshot for one physical WebSocket session.
|
||||
@@ -346,6 +356,10 @@ pub use self::ws_settings::WsSessionSettings;
|
||||
pub use self::ws_settings::WsTransportSettings;
|
||||
/// Typed handle for one logical Solana WebSocket subscription.
|
||||
pub use self::ws_subscription::WsSubscription;
|
||||
/// Typed value carried by a contextual Solana `logsNotification`.
|
||||
pub use self::ws_transactions::SolanaLogsNotification;
|
||||
/// Filter accepted by the standard Solana `logsSubscribe` WebSocket method.
|
||||
pub use self::ws_transactions::SolanaLogsSubscribeFilter;
|
||||
|
||||
/// Owning tracing target for events emitted by the on-chain transport crate.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
const MAX_MEMCMP_BYTES: usize = 128;
|
||||
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
|
||||
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
|
||||
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
/// Account-data encoding accepted by Solana account HTTP and WebSocket methods.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaAccountEncoding {
|
||||
/// Legacy binary/base58 request encoding.
|
||||
@@ -71,7 +71,8 @@ impl SolanaDataSliceConfig {
|
||||
return self.length;
|
||||
}
|
||||
|
||||
fn to_json_value(self) -> serde_json::Value {
|
||||
/// Serializes this data-slice configuration to the Solana JSON-RPC wire object.
|
||||
pub(crate) fn to_json_value(self) -> serde_json::Value {
|
||||
return serde_json::json!({"offset": self.offset, "length": self.length});
|
||||
}
|
||||
}
|
||||
@@ -277,7 +278,8 @@ pub enum SolanaProgramAccountFilter {
|
||||
}
|
||||
|
||||
impl SolanaProgramAccountFilter {
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
/// Serializes this program-account filter to the Solana JSON-RPC wire representation.
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::DataSize(size) => serde_json::json!({"dataSize": size}),
|
||||
Self::Memcmp(filter) => serde_json::json!({"memcmp": filter.to_json_value()}),
|
||||
@@ -385,7 +387,7 @@ impl SolanaParsedAccountData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire-preserving account data returned by Solana HTTP account methods.
|
||||
/// Wire-preserving account data returned by Solana account HTTP and WebSocket methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaAccountData {
|
||||
/// Legacy single-string binary form retained for backwards compatibility.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
/// Commitment level accepted by typed Solana HTTP and WebSocket adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
@@ -24,7 +24,7 @@ impl SolanaCommitment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional commitment-only configuration shared by typed Solana HTTP RPC methods.
|
||||
/// Optional commitment-only configuration shared by typed Solana RPC methods.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaCommitmentConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
@@ -94,7 +94,7 @@ impl SolanaContextConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed Solana RPC context shared by contextual HTTP responses.
|
||||
/// Typed Solana RPC context shared by contextual HTTP and WebSocket responses.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaRpcContext {
|
||||
slot: u64,
|
||||
@@ -128,7 +128,7 @@ impl SolanaRpcContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
/// Generic contextual result returned by typed Solana HTTP and WebSocket adapters.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaRpcResponse<T> {
|
||||
context: crate::SolanaRpcContext,
|
||||
@@ -168,7 +168,7 @@ pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, val
|
||||
return match decoded {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP response has an invalid wire shape")
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana RPC response has an invalid wire shape")
|
||||
.with_context("rpc_method", method)
|
||||
.with_source(error),
|
||||
),
|
||||
@@ -181,7 +181,7 @@ pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_c
|
||||
return match parsed {
|
||||
std::result::Result::Ok(pubkey) => std::result::Result::Ok(pubkey),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP response contains an invalid public key")
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana RPC response contains an invalid public key")
|
||||
.with_context("rpc_method", method)
|
||||
.with_context("field", field),
|
||||
),
|
||||
|
||||
278
crates/ksp-onchain-transport-lib/src/ws_accounts.rs
Normal file
278
crates/ksp-onchain-transport-lib/src/ws_accounts.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_accounts.rs
|
||||
// version: 1
|
||||
|
||||
const MAX_PROGRAM_SUBSCRIBE_FILTERS: usize = 4;
|
||||
const MAX_PROGRAM_SUBSCRIBE_RAW_MEMCMP_BYTES: usize = 128;
|
||||
|
||||
/// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method.
|
||||
///
|
||||
/// `minContextSlot` is deliberately absent: Agave `v4.2.1` carries that field in the shared account config but the PubSub handler ignores it, so KSP does
|
||||
/// not expose it as an effective WebSocket option.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaAccountSubscribeConfig {
|
||||
encoding: std::option::Option<crate::SolanaAccountEncoding>,
|
||||
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
}
|
||||
|
||||
impl SolanaAccountSubscribeConfig {
|
||||
/// Creates an explicit `accountSubscribe` configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
encoding: std::option::Option<crate::SolanaAccountEncoding>,
|
||||
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
) -> Self {
|
||||
return Self { encoding, data_slice, commitment };
|
||||
}
|
||||
|
||||
/// Returns the optional account-data encoding.
|
||||
#[must_use]
|
||||
pub const fn encoding(&self) -> std::option::Option<crate::SolanaAccountEncoding> {
|
||||
return self.encoding;
|
||||
}
|
||||
|
||||
/// Returns the optional account-data slice.
|
||||
#[must_use]
|
||||
pub const fn data_slice(&self) -> std::option::Option<crate::SolanaDataSliceConfig> {
|
||||
return self.data_slice;
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
return self.encoding.is_none() && self.data_slice.is_none() && self.commitment.is_none();
|
||||
}
|
||||
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(encoding) = self.encoding {
|
||||
object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(data_slice) = self.data_slice {
|
||||
object.insert("dataSlice".to_owned(), data_slice.to_json_value());
|
||||
}
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration accepted by the standard Solana `programSubscribe` WebSocket method.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaProgramSubscribeConfig {
|
||||
account: crate::SolanaAccountSubscribeConfig,
|
||||
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
|
||||
with_context: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl SolanaProgramSubscribeConfig {
|
||||
/// Creates an explicit `programSubscribe` configuration.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
account: crate::SolanaAccountSubscribeConfig,
|
||||
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
|
||||
with_context: std::option::Option<bool>,
|
||||
) -> Self {
|
||||
return Self { account, filters, with_context };
|
||||
}
|
||||
|
||||
/// Returns the shared WebSocket account configuration.
|
||||
#[must_use]
|
||||
pub const fn account(&self) -> &crate::SolanaAccountSubscribeConfig {
|
||||
return &self.account;
|
||||
}
|
||||
|
||||
/// Returns the ordered program-account filters.
|
||||
#[must_use]
|
||||
pub fn filters(&self) -> &[crate::SolanaProgramAccountFilter] {
|
||||
return self.filters.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the optional `withContext` request; omission uses the upstream default `false`.
|
||||
#[must_use]
|
||||
pub const fn with_context(&self) -> std::option::Option<bool> {
|
||||
return self.with_context;
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
return self.account.is_empty() && self.filters.is_empty() && self.with_context.is_none();
|
||||
}
|
||||
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
let account = self.account.to_json_value();
|
||||
let mut object = match account {
|
||||
serde_json::Value::Object(object) => object,
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
if !self.filters.is_empty() {
|
||||
let filters = self.filters.iter().map(crate::SolanaProgramAccountFilter::to_json_value).collect::<std::vec::Vec<_>>();
|
||||
object.insert("filters".to_owned(), serde_json::Value::Array(filters));
|
||||
}
|
||||
if let std::option::Option::Some(with_context) = self.with_context {
|
||||
object.insert("withContext".to_owned(), serde_json::Value::Bool(with_context));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// One `programNotification` payload, preserving whether the upstream wire result was contextualized.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaProgramNotification {
|
||||
/// Program account payload without a surrounding RPC context.
|
||||
Account(crate::SolanaKeyedAccount),
|
||||
/// Program account payload wrapped in an RPC context.
|
||||
Context(crate::SolanaRpcResponse<crate::SolanaKeyedAccount>),
|
||||
}
|
||||
|
||||
impl SolanaProgramNotification {
|
||||
/// Returns the program account regardless of the upstream context-wrapper form.
|
||||
#[must_use]
|
||||
pub const fn account(&self) -> &crate::SolanaKeyedAccount {
|
||||
return match self {
|
||||
Self::Account(account) => account,
|
||||
Self::Context(response) => response.value(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the RPC context when the upstream notification included one.
|
||||
#[must_use]
|
||||
pub const fn context(&self) -> std::option::Option<&crate::SolanaRpcContext> {
|
||||
return match self {
|
||||
Self::Account(_) => std::option::Option::None,
|
||||
Self::Context(response) => std::option::Option::Some(response.context()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::WsSession {
|
||||
/// Subscribes to changes for one Solana account through standard `accountSubscribe`.
|
||||
pub async fn account_subscribe(
|
||||
&self,
|
||||
account: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaAccountSubscribeConfig>,
|
||||
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaAccount>>> {
|
||||
let mut params = std::vec![serde_json::Value::String(account.to_string())];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
return self.subscribe_typed(crate::WsSubscriptionKind::Account, params, |value| decode_account_notification("accountSubscribe", value)).await;
|
||||
}
|
||||
|
||||
/// Subscribes to account changes owned by one Solana program through standard `programSubscribe`.
|
||||
pub async fn program_subscribe(
|
||||
&self,
|
||||
program_id: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaProgramSubscribeConfig>,
|
||||
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaProgramNotification>> {
|
||||
if let std::option::Option::Some(config) = config {
|
||||
let validation = validate_program_subscribe_filters(config.filters());
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
let mut params = std::vec![serde_json::Value::String(program_id.to_string())];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
return self.subscribe_typed(crate::WsSubscriptionKind::Program, params, |value| decode_program_notification("programSubscribe", value)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse {
|
||||
context: serde_json::Value,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireProgramNotification {
|
||||
Context(WireRpcResponse),
|
||||
Account(serde_json::Value),
|
||||
}
|
||||
|
||||
fn decode_account_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaAccount>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = crate::SolanaAccount::decode_wire(method, wire.value);
|
||||
let account = match account {
|
||||
std::result::Result::Ok(account) => account,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, account));
|
||||
}
|
||||
|
||||
fn decode_program_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaProgramNotification> {
|
||||
let decoded = crate::decode_wire_json::<WireProgramNotification>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match wire {
|
||||
WireProgramNotification::Account(value) => {
|
||||
let account = crate::SolanaKeyedAccount::decode_wire(method, value);
|
||||
match account {
|
||||
std::result::Result::Ok(account) => std::result::Result::Ok(crate::SolanaProgramNotification::Account(account)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
WireProgramNotification::Context(wire) => {
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = crate::SolanaKeyedAccount::decode_wire(method, wire.value);
|
||||
let account = match account {
|
||||
std::result::Result::Ok(account) => account,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
std::result::Result::Ok(crate::SolanaProgramNotification::Context(crate::SolanaRpcResponse::new(context, account)))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_program_subscribe_filters(filters: &[crate::SolanaProgramAccountFilter]) -> ksp_core_lib::Result<()> {
|
||||
if filters.len() > MAX_PROGRAM_SUBSCRIBE_FILTERS {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "programSubscribe accepts at most 4 filters on the targeted Agave runtime")
|
||||
.with_context("rpc_method", "programSubscribe")
|
||||
.with_context("filter_count", filters.len().to_string()),
|
||||
);
|
||||
}
|
||||
for filter in filters {
|
||||
if let crate::SolanaProgramAccountFilter::Memcmp(memcmp) = filter
|
||||
&& let crate::SolanaMemcmpBytes::Bytes(bytes) = memcmp.bytes()
|
||||
&& bytes.len() > MAX_PROGRAM_SUBSCRIBE_RAW_MEMCMP_BYTES
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "raw programSubscribe memcmp data accepts at most 128 bytes")
|
||||
.with_context("rpc_method", "programSubscribe")
|
||||
.with_context("memcmp_byte_count", bytes.len().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/ws_accounts.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
use futures_util::SinkExt; // rust-rules: trait-import
|
||||
use futures_util::StreamExt; // rust-rules: trait-import
|
||||
@@ -156,9 +156,8 @@ impl WsSession {
|
||||
|
||||
/// Creates one crate-internal typed standard Solana subscription through the actor-owned registry.
|
||||
///
|
||||
/// Public typed wrappers are introduced in later prereleases. Keeping this constructor crate-private prevents a raw provider-extension subscription API
|
||||
/// from becoming part of the stable KSP surface while still making the generic typed engine testable and reusable by those wrappers.
|
||||
#[allow(dead_code)] // Staged in pre.006 and consumed by the public typed standard wrappers starting in pre.009.
|
||||
/// Public typed standard wrappers consume this constructor while it remains crate-private, preventing a raw provider-extension subscription API from
|
||||
/// becoming part of the stable KSP surface.
|
||||
pub(crate) async fn subscribe_typed<T, F>(
|
||||
&self,
|
||||
kind: crate::WsSubscriptionKind,
|
||||
|
||||
104
crates/ksp-onchain-transport-lib/src/ws_transactions.rs
Normal file
104
crates/ksp-onchain-transport-lib/src/ws_transactions.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/ws_transactions.rs
|
||||
// version: 1
|
||||
|
||||
/// Filter accepted by the standard Solana `logsSubscribe` WebSocket method.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaLogsSubscribeFilter {
|
||||
/// Subscribe to all transactions except simple vote transactions.
|
||||
All,
|
||||
/// Subscribe to all transactions including simple vote transactions.
|
||||
AllWithVotes,
|
||||
/// Subscribe only to transactions mentioning exactly one public key.
|
||||
Mentions(ksp_core_lib::Pubkey),
|
||||
}
|
||||
|
||||
impl SolanaLogsSubscribeFilter {
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::All => serde_json::Value::String("all".to_owned()),
|
||||
Self::AllWithVotes => serde_json::Value::String("allWithVotes".to_owned()),
|
||||
Self::Mentions(pubkey) => serde_json::json!({"mentions": [pubkey.to_string()]}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed value carried by a contextual Solana `logsNotification`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaLogsNotification {
|
||||
signature: std::string::String,
|
||||
err: std::option::Option<serde_json::Value>,
|
||||
logs: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaLogsNotification {
|
||||
/// Returns the base58 transaction signature exactly as reported by the RPC node.
|
||||
#[must_use]
|
||||
pub fn signature(&self) -> &str {
|
||||
return self.signature.as_str();
|
||||
}
|
||||
|
||||
/// Returns the nullable transaction-error wire value without interpreting Program/runtime error semantics.
|
||||
#[must_use]
|
||||
pub const fn err(&self) -> std::option::Option<&serde_json::Value> {
|
||||
return self.err.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the ordered transaction log messages.
|
||||
#[must_use]
|
||||
pub fn logs(&self) -> &[std::string::String] {
|
||||
return self.logs.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::WsSession {
|
||||
/// Subscribes to Solana transaction logs through standard `logsSubscribe`.
|
||||
pub async fn logs_subscribe(
|
||||
&self,
|
||||
filter: &crate::SolanaLogsSubscribeFilter,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaLogsNotification>>> {
|
||||
let mut params = std::vec![filter.to_json_value()];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment().is_some()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
return self.subscribe_typed(crate::WsSubscriptionKind::Logs, params, |value| decode_logs_notification("logsSubscribe", value)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse {
|
||||
context: serde_json::Value,
|
||||
value: WireLogsNotification,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireLogsNotification {
|
||||
signature: std::string::String,
|
||||
err: serde_json::Value,
|
||||
logs: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
fn decode_logs_notification(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaLogsNotification>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let err = match wire.value.err {
|
||||
serde_json::Value::Null => std::option::Option::None,
|
||||
value => std::option::Option::Some(value),
|
||||
};
|
||||
let notification = crate::SolanaLogsNotification { signature: wire.value.signature, err, logs: wire.value.logs };
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, notification));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/ws_transactions.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user