0.4.3
This commit is contained in:
@@ -8,7 +8,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot"
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ pub async fn run_listener_app(config_path: &str) -> core::result::Result<(), cra
|
|||||||
yellowstone_grpc_url = ?config.yellowstone_grpc_url,
|
yellowstone_grpc_url = ?config.yellowstone_grpc_url,
|
||||||
bootstrap_database = config.bootstrap_database,
|
bootstrap_database = config.bootstrap_database,
|
||||||
listener_poll_interval_ms = config.listener_poll_interval_ms,
|
listener_poll_interval_ms = config.listener_poll_interval_ms,
|
||||||
|
enable_ws_slot_subscribe = config.enable_ws_slot_subscribe,
|
||||||
|
enable_ws_logs_subscribe = config.enable_ws_logs_subscribe,
|
||||||
|
enable_ws_program_subscribe = config.enable_ws_program_subscribe,
|
||||||
|
ws_program_subscribe_program_ids = ?config.ws_program_subscribe_program_ids,
|
||||||
"khbb listener app starting"
|
"khbb listener app starting"
|
||||||
);
|
);
|
||||||
let pool_result = crate::create_sqlite_pool(&config.database_url).await;
|
let pool_result = crate::create_sqlite_pool(&config.database_url).await;
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ pub struct KhbbAppConfig {
|
|||||||
pub bootstrap_database: bool,
|
pub bootstrap_database: bool,
|
||||||
/// Polling interval used by the current runtime skeleton.
|
/// Polling interval used by the current runtime skeleton.
|
||||||
pub listener_poll_interval_ms: u64,
|
pub listener_poll_interval_ms: u64,
|
||||||
|
/// Enables or disables `slotSubscribe` during listener startup.
|
||||||
|
pub enable_ws_slot_subscribe: bool,
|
||||||
|
/// Enables or disables `logsSubscribe` during listener startup.
|
||||||
|
pub enable_ws_logs_subscribe: bool,
|
||||||
|
/// Enables or disables `programSubscribe` during listener startup.
|
||||||
|
pub enable_ws_program_subscribe: bool,
|
||||||
|
/// Program ids used when `programSubscribe` is enabled.
|
||||||
|
pub ws_program_subscribe_program_ids: std::vec::Vec<std::string::String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KhbbAppConfig {
|
impl KhbbAppConfig {
|
||||||
@@ -80,6 +88,24 @@ impl KhbbAppConfig {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if self.enable_ws_program_subscribe && self.ws_program_subscribe_program_ids.is_empty() {
|
||||||
|
return Err(crate::KhbbError::Config {
|
||||||
|
message: std::string::String::from(
|
||||||
|
"ws_program_subscribe_program_ids must not be empty when enable_ws_program_subscribe is true",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.enable_ws_program_subscribe {
|
||||||
|
for program_id in &self.ws_program_subscribe_program_ids {
|
||||||
|
if program_id.trim().is_empty() {
|
||||||
|
return Err(crate::KhbbError::Config {
|
||||||
|
message: std::string::String::from(
|
||||||
|
"ws_program_subscribe_program_ids must not contain empty program ids",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,6 +127,10 @@ mod tests {
|
|||||||
log_filter: std::string::String::from("info"),
|
log_filter: std::string::String::from("info"),
|
||||||
bootstrap_database: true,
|
bootstrap_database: true,
|
||||||
listener_poll_interval_ms: 1000,
|
listener_poll_interval_ms: 1000,
|
||||||
|
enable_ws_slot_subscribe: true,
|
||||||
|
enable_ws_logs_subscribe: true,
|
||||||
|
enable_ws_program_subscribe: false,
|
||||||
|
ws_program_subscribe_program_ids: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +187,6 @@ mod tests {
|
|||||||
std::env::temp_dir().join(std::format!("khbb_config_test_{}", uuid::Uuid::new_v4()));
|
std::env::temp_dir().join(std::format!("khbb_config_test_{}", uuid::Uuid::new_v4()));
|
||||||
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
|
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
|
||||||
let config_path = temp_dir.join("config.json");
|
let config_path = temp_dir.join("config.json");
|
||||||
|
|
||||||
let config_json = r#"{
|
let config_json = r#"{
|
||||||
"database_url": "sqlite://./dbdata/app.db",
|
"database_url": "sqlite://./dbdata/app.db",
|
||||||
"solana_http_rpc_url": "https://mainnet.helius-rpc.com/?api-key=test",
|
"solana_http_rpc_url": "https://mainnet.helius-rpc.com/?api-key=test",
|
||||||
@@ -165,7 +194,11 @@ mod tests {
|
|||||||
"yellowstone_grpc_url": "https://mainnet.helius-rpc.com:443",
|
"yellowstone_grpc_url": "https://mainnet.helius-rpc.com:443",
|
||||||
"log_filter": "info",
|
"log_filter": "info",
|
||||||
"bootstrap_database": true,
|
"bootstrap_database": true,
|
||||||
"listener_poll_interval_ms": 1000
|
"listener_poll_interval_ms": 1000,
|
||||||
|
"enable_ws_slot_subscribe": true,
|
||||||
|
"enable_ws_logs_subscribe": true,
|
||||||
|
"enable_ws_program_subscribe": false,
|
||||||
|
"ws_program_subscribe_program_ids": []
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
std::fs::write(&config_path, config_json).expect("write config file");
|
std::fs::write(&config_path, config_json).expect("write config file");
|
||||||
@@ -177,4 +210,22 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(&config_path);
|
let _ = std::fs::remove_file(&config_path);
|
||||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_rejects_empty_program_ids_when_program_subscribe_is_enabled() {
|
||||||
|
let mut config = build_valid_config();
|
||||||
|
config.enable_ws_program_subscribe = true;
|
||||||
|
config.ws_program_subscribe_program_ids = vec![];
|
||||||
|
let result = config.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_rejects_empty_program_id_entry() {
|
||||||
|
let mut config = build_valid_config();
|
||||||
|
config.enable_ws_program_subscribe = true;
|
||||||
|
config.ws_program_subscribe_program_ids = vec![std::string::String::from("")];
|
||||||
|
let result = config.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// file: khbb_lib/src/listener.rs
|
// file: khbb_lib/src/listener.rs
|
||||||
|
|
||||||
//! Listener runtime skeleton.
|
//! Listener runtime skeleton.
|
||||||
//!
|
//!
|
||||||
//! This module does not yet connect to Solana RPC, WebSocket or gRPC streams.
|
//! This module does not yet connect to Solana RPC, WebSocket or gRPC streams.
|
||||||
@@ -44,6 +45,7 @@ pub async fn run_listener_runtime(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
let mut ws_subscription_handles = std::vec::Vec::<crate::KhbbWsSubscriptionHandle>::new();
|
||||||
let ws_connect_result = ws_client.connect().await;
|
let ws_connect_result = ws_client.connect().await;
|
||||||
match ws_connect_result {
|
match ws_connect_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -57,6 +59,7 @@ pub async fn run_listener_runtime(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if config.enable_ws_slot_subscribe {
|
||||||
let slot_subscribe_result = ws_client.slot_subscribe(1).await;
|
let slot_subscribe_result = ws_client.slot_subscribe(1).await;
|
||||||
let slot_subscribe_output = match slot_subscribe_result {
|
let slot_subscribe_output = match slot_subscribe_result {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@@ -109,6 +112,128 @@ pub async fn run_listener_runtime(
|
|||||||
subscription_id = slot_subscription_handle.subscription_id,
|
subscription_id = slot_subscription_handle.subscription_id,
|
||||||
"slot websocket subscription established"
|
"slot websocket subscription established"
|
||||||
);
|
);
|
||||||
|
ws_subscription_handles.push(slot_subscription_handle);
|
||||||
|
}
|
||||||
|
if config.enable_ws_logs_subscribe {
|
||||||
|
let logs_subscribe_result = ws_client
|
||||||
|
.logs_subscribe(solana_rpc_client_api::config::RpcTransactionLogsFilter::All, None, 2)
|
||||||
|
.await;
|
||||||
|
let logs_subscribe_output = match logs_subscribe_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let insert_ws_outgoing_result = crate::storage::insert_raw_ws_message(
|
||||||
|
pool,
|
||||||
|
session.id,
|
||||||
|
"outgoing",
|
||||||
|
&logs_subscribe_output.request_body,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match insert_ws_outgoing_result {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
"failed to store outgoing websocket logs subscribe request"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let insert_ws_incoming_result = crate::storage::insert_raw_ws_message(
|
||||||
|
pool,
|
||||||
|
session.id,
|
||||||
|
"incoming",
|
||||||
|
&logs_subscribe_output.response_body,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match insert_ws_incoming_result {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
"failed to store incoming websocket logs subscribe response"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let logs_subscription_handle = crate::KhbbWsSubscriptionHandle {
|
||||||
|
request_id: logs_subscribe_output.request_id,
|
||||||
|
subscription_id: logs_subscribe_output.subscription_id,
|
||||||
|
kind: crate::KhbbWsSubscriptionKind::Logs,
|
||||||
|
};
|
||||||
|
tracing::info!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
request_id = logs_subscription_handle.request_id,
|
||||||
|
subscription_id = logs_subscription_handle.subscription_id,
|
||||||
|
"logs websocket subscription established"
|
||||||
|
);
|
||||||
|
ws_subscription_handles.push(logs_subscription_handle);
|
||||||
|
}
|
||||||
|
if config.enable_ws_program_subscribe {
|
||||||
|
let mut program_request_id: u64 = 10;
|
||||||
|
for program_id in &config.ws_program_subscribe_program_ids {
|
||||||
|
let program_subscribe_result =
|
||||||
|
ws_client.program_subscribe(program_id, None, program_request_id).await;
|
||||||
|
let program_subscribe_output = match program_subscribe_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let insert_ws_outgoing_result = crate::storage::insert_raw_ws_message(
|
||||||
|
pool,
|
||||||
|
session.id,
|
||||||
|
"outgoing",
|
||||||
|
&program_subscribe_output.request_body,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match insert_ws_outgoing_result {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
program_id = %program_id,
|
||||||
|
"failed to store outgoing websocket program subscribe request"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let insert_ws_incoming_result = crate::storage::insert_raw_ws_message(
|
||||||
|
pool,
|
||||||
|
session.id,
|
||||||
|
"incoming",
|
||||||
|
&program_subscribe_output.response_body,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match insert_ws_incoming_result {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
program_id = %program_id,
|
||||||
|
"failed to store incoming websocket program subscribe response"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let program_subscription_handle = crate::KhbbWsSubscriptionHandle {
|
||||||
|
request_id: program_subscribe_output.request_id,
|
||||||
|
subscription_id: program_subscribe_output.subscription_id,
|
||||||
|
kind: crate::KhbbWsSubscriptionKind::Program,
|
||||||
|
};
|
||||||
|
tracing::info!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
request_id = program_subscription_handle.request_id,
|
||||||
|
subscription_id = program_subscription_handle.subscription_id,
|
||||||
|
program_id = %program_id,
|
||||||
|
"program websocket subscription established"
|
||||||
|
);
|
||||||
|
ws_subscription_handles.push(program_subscription_handle);
|
||||||
|
program_request_id = program_request_id.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut final_status = std::string::String::from("stopped");
|
let mut final_status = std::string::String::from("stopped");
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -190,6 +315,102 @@ pub async fn run_listener_runtime(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let method_value_result =
|
||||||
|
serde_json::from_str::<serde_json::Value>(&message_text);
|
||||||
|
match method_value_result {
|
||||||
|
Ok(json_value) => {
|
||||||
|
let method_option = json_value
|
||||||
|
.get("method")
|
||||||
|
.and_then(serde_json::Value::as_str);
|
||||||
|
match method_option {
|
||||||
|
Some("slotNotification") => {
|
||||||
|
let parse_result =
|
||||||
|
crate::solana_rpc_ws::parse_slot_notification(&message_text);
|
||||||
|
match parse_result {
|
||||||
|
Ok(notification) => {
|
||||||
|
tracing::trace!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
subscription_id = notification.params.subscription,
|
||||||
|
slot = notification.params.result.slot,
|
||||||
|
parent = notification.params.result.parent,
|
||||||
|
root = notification.params.result.root,
|
||||||
|
"parsed slot notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
"failed to parse slot notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("logsNotification") => {
|
||||||
|
let parse_result =
|
||||||
|
crate::solana_rpc_ws::parse_logs_notification(&message_text);
|
||||||
|
match parse_result {
|
||||||
|
Ok(notification) => {
|
||||||
|
tracing::trace!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
subscription_id = notification.params.subscription,
|
||||||
|
signature = %notification.params.result.value.signature,
|
||||||
|
"parsed logs notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
"failed to parse logs notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("programNotification") => {
|
||||||
|
let parse_result =
|
||||||
|
crate::solana_rpc_ws::parse_program_notification(&message_text);
|
||||||
|
match parse_result {
|
||||||
|
Ok(notification) => {
|
||||||
|
tracing::trace!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
subscription_id = notification.params.subscription,
|
||||||
|
program_pubkey = %notification.params.result.value.pubkey,
|
||||||
|
"parsed program notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
"failed to parse program notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(other_method) => {
|
||||||
|
tracing::trace!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
method = %other_method,
|
||||||
|
"received unsupported websocket notification method"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::trace!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
"received websocket json message without notification method"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
listener_session_id = session.id,
|
||||||
|
error = %error,
|
||||||
|
"failed to decode websocket message as json value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -250,11 +471,12 @@ pub async fn run_listener_runtime(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for subscription_handle in &ws_subscription_handles {
|
||||||
let unsubscribe_result = ws_client
|
let unsubscribe_result = ws_client
|
||||||
.unsubscribe(
|
.unsubscribe(
|
||||||
slot_subscription_handle.kind,
|
subscription_handle.kind,
|
||||||
slot_subscription_handle.subscription_id,
|
subscription_handle.subscription_id,
|
||||||
tick_count.saturating_add(10),
|
tick_count.saturating_add(subscription_handle.request_id),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match unsubscribe_result {
|
match unsubscribe_result {
|
||||||
@@ -262,19 +484,22 @@ pub async fn run_listener_runtime(
|
|||||||
tracing::info!(
|
tracing::info!(
|
||||||
listener_session_id = session.id,
|
listener_session_id = session.id,
|
||||||
unsubscribed = value,
|
unsubscribed = value,
|
||||||
subscription_id = slot_subscription_handle.subscription_id,
|
subscription_id = subscription_handle.subscription_id,
|
||||||
"slot websocket subscription cancelled"
|
kind = ?subscription_handle.kind,
|
||||||
|
"websocket subscription cancelled"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
listener_session_id = session.id,
|
listener_session_id = session.id,
|
||||||
error = %error,
|
error = %error,
|
||||||
subscription_id = slot_subscription_handle.subscription_id,
|
subscription_id = subscription_handle.subscription_id,
|
||||||
"failed to cancel slot websocket subscription"
|
kind = ?subscription_handle.kind,
|
||||||
|
"failed to cancel websocket subscription"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let ws_close_result = ws_client.close().await;
|
let ws_close_result = ws_client.close().await;
|
||||||
match ws_close_result {
|
match ws_close_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// file: khbb_lib/src/solana_rpc_ws.rs
|
// file: khbb_lib/src/solana_rpc_ws.rs
|
||||||
|
|
||||||
//! Minimal Solana WebSocket JSON-RPC client.
|
//! Minimal Solana WebSocket JSON-RPC client.
|
||||||
//!
|
//!
|
||||||
//! This module keeps full control over the WebSocket transport and JSON-RPC
|
//! This module keeps full control over the WebSocket transport and JSON-RPC
|
||||||
@@ -412,6 +413,7 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
loop {
|
||||||
let response_body_result = self.read_next_text_message().await;
|
let response_body_result = self.read_next_text_message().await;
|
||||||
let response_body_option = match response_body_result {
|
let response_body_option = match response_body_result {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@@ -424,12 +426,28 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
None => {
|
None => {
|
||||||
return Err(crate::KhbbError::Runtime {
|
return Err(crate::KhbbError::Runtime {
|
||||||
context: "read websocket unsubscribe response",
|
context: "read websocket unsubscribe response",
|
||||||
message: std::string::String::from("websocket stream ended before response"),
|
message: std::string::String::from(
|
||||||
|
"websocket stream ended before unsubscribe response",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let parse_result = parse_json_rpc_response::<bool>(&response_body);
|
let json_value_result = serde_json::from_str::<serde_json::Value>(&response_body);
|
||||||
let parsed_response = match parse_result {
|
let json_value = match json_value_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(crate::KhbbError::Json {
|
||||||
|
context: "decode websocket unsubscribe response as json value",
|
||||||
|
message: error.to_string(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let id_value_option = json_value.get("id");
|
||||||
|
if id_value_option.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parsed_response_result = parse_json_rpc_response::<bool>(&response_body);
|
||||||
|
let parsed_response = match parsed_response_result {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
@@ -444,7 +462,7 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
context: "serialize websocket unsubscribe rpc error",
|
context: "serialize websocket unsubscribe rpc error",
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
return Err(crate::KhbbError::Runtime {
|
return Err(crate::KhbbError::Runtime {
|
||||||
context: "websocket unsubscribe returned rpc error",
|
context: "websocket unsubscribe returned rpc error",
|
||||||
@@ -452,11 +470,14 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
match parsed_response.result {
|
match parsed_response.result {
|
||||||
Some(value) => Ok(value),
|
Some(value) => return Ok(value),
|
||||||
None => Err(crate::KhbbError::Runtime {
|
None => {
|
||||||
|
return Err(crate::KhbbError::Runtime {
|
||||||
context: "websocket unsubscribe returned empty result",
|
context: "websocket unsubscribe returned empty result",
|
||||||
message: response_body,
|
message: response_body,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,6 +498,7 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
loop {
|
||||||
let response_body_result = self.read_next_text_message().await;
|
let response_body_result = self.read_next_text_message().await;
|
||||||
let response_body_option = match response_body_result {
|
let response_body_option = match response_body_result {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@@ -489,10 +511,26 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
None => {
|
None => {
|
||||||
return Err(crate::KhbbError::Runtime {
|
return Err(crate::KhbbError::Runtime {
|
||||||
context: "read websocket subscribe response",
|
context: "read websocket subscribe response",
|
||||||
message: std::string::String::from("websocket stream ended before response"),
|
message: std::string::String::from(
|
||||||
|
"websocket stream ended before subscribe response",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
let json_value_result = serde_json::from_str::<serde_json::Value>(&response_body);
|
||||||
|
let json_value = match json_value_result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(crate::KhbbError::Json {
|
||||||
|
context: "decode websocket subscribe response as json value",
|
||||||
|
message: error.to_string(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let id_value_option = json_value.get("id");
|
||||||
|
if id_value_option.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let subscription_id_result = parse_subscription_id_response(&response_body);
|
let subscription_id_result = parse_subscription_id_response(&response_body);
|
||||||
let subscription_id = match subscription_id_result {
|
let subscription_id = match subscription_id_result {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@@ -500,13 +538,14 @@ impl KhbbSolanaWsRpcClient {
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Ok(KhbbWsSubscribeCallOutput {
|
return Ok(KhbbWsSubscribeCallOutput {
|
||||||
request_id: id,
|
request_id: id,
|
||||||
method: std::string::String::from(kind.subscribe_method_name()),
|
method: std::string::String::from(kind.subscribe_method_name()),
|
||||||
subscription_id,
|
subscription_id,
|
||||||
request_body,
|
request_body,
|
||||||
response_body,
|
response_body,
|
||||||
})
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +558,6 @@ where
|
|||||||
{
|
{
|
||||||
let parse_result =
|
let parse_result =
|
||||||
serde_json::from_str::<KhbbWsJsonRpcResponseEnvelope<TResult>>(response_body);
|
serde_json::from_str::<KhbbWsJsonRpcResponseEnvelope<TResult>>(response_body);
|
||||||
|
|
||||||
match parse_result {
|
match parse_result {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
Err(error) => Err(crate::KhbbError::Json {
|
Err(error) => Err(crate::KhbbError::Json {
|
||||||
@@ -618,7 +656,7 @@ pub(crate) fn parse_subscription_id_response(
|
|||||||
context: "serialize websocket subscribe rpc error",
|
context: "serialize websocket subscribe rpc error",
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
return Err(crate::KhbbError::Runtime {
|
return Err(crate::KhbbError::Runtime {
|
||||||
context: "websocket subscribe returned rpc error",
|
context: "websocket subscribe returned rpc error",
|
||||||
|
|||||||
@@ -374,6 +374,10 @@ mod tests {
|
|||||||
log_filter: std::string::String::from("info"),
|
log_filter: std::string::String::from("info"),
|
||||||
bootstrap_database: true,
|
bootstrap_database: true,
|
||||||
listener_poll_interval_ms: 1000,
|
listener_poll_interval_ms: 1000,
|
||||||
|
enable_ws_slot_subscribe: true,
|
||||||
|
enable_ws_logs_subscribe: true,
|
||||||
|
enable_ws_program_subscribe: false,
|
||||||
|
ws_program_subscribe_program_ids: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,6 +501,10 @@ WHERE id = ?1;
|
|||||||
log_filter: "info".into(),
|
log_filter: "info".into(),
|
||||||
bootstrap_database: false,
|
bootstrap_database: false,
|
||||||
listener_poll_interval_ms: 1000,
|
listener_poll_interval_ms: 1000,
|
||||||
|
enable_ws_slot_subscribe: true,
|
||||||
|
enable_ws_logs_subscribe: true,
|
||||||
|
enable_ws_program_subscribe: false,
|
||||||
|
ws_program_subscribe_program_ids: vec![],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
khbb_lib = { path = "../khbb_lib" }
|
khbb_lib = { path = "../khbb_lib" }
|
||||||
|
rustls.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -10,6 +10,16 @@
|
|||||||
/// Entrypoint of the khbb listener binary.
|
/// Entrypoint of the khbb listener binary.
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::process::ExitCode {
|
async fn main() -> std::process::ExitCode {
|
||||||
|
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
||||||
|
let provider_result = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||||
|
match provider_result {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("khbb_listener_app rustls provider init error: {:?}", error);
|
||||||
|
return std::process::ExitCode::FAILURE;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
let args = std::env::args().collect::<std::vec::Vec<std::string::String>>();
|
let args = std::env::args().collect::<std::vec::Vec<std::string::String>>();
|
||||||
let config_path = if args.len() >= 2 { args[1].as_str() } else { "config.json" };
|
let config_path = if args.len() >= 2 { args[1].as_str() } else { "config.json" };
|
||||||
let run_result = khbb_lib::run_listener_app(config_path).await;
|
let run_result = khbb_lib::run_listener_app(config_path).await;
|
||||||
|
|||||||
Reference in New Issue
Block a user