0.7.27 +Refactor

This commit is contained in:
2026-05-10 00:33:01 +02:00
parent cb2e8e7096
commit 1f0137b9de
261 changed files with 12308 additions and 8928 deletions

View File

@@ -17,7 +17,7 @@ pub struct WsManagedEndpointSnapshot {
/// Endpoint provider name.
pub provider: std::string::String,
/// Current connection state.
pub state: crate::KbConnectionState,
pub state: crate::ConnectionState,
/// Number of active subscriptions currently tracked by the client.
pub active_subscription_count: usize,
}
@@ -45,11 +45,11 @@ pub struct WsManager {
clients: tokio::sync::Mutex<std::collections::BTreeMap<std::string::String, WsManagedClient>>,
event_tx: tokio::sync::broadcast::Sender<crate::WsEvent>,
detection_relay_sender: tokio::sync::Mutex<
std::option::Option<tokio::sync::mpsc::Sender<crate::KbWsDetectionNotificationEnvelope>>,
std::option::Option<tokio::sync::mpsc::Sender<crate::WsDetectionNotificationEnvelope>>,
>,
detection_relay_abort_handle: tokio::sync::Mutex<std::option::Option<tokio::task::AbortHandle>>,
transaction_resolution_relay_sender: tokio::sync::Mutex<
std::option::Option<tokio::sync::mpsc::Sender<crate::KbWsTransactionResolutionEnvelope>>,
std::option::Option<tokio::sync::mpsc::Sender<crate::WsTransactionResolutionEnvelope>>,
>,
transaction_resolution_relay_abort_handle:
tokio::sync::Mutex<std::option::Option<tokio::task::AbortHandle>>,
@@ -61,9 +61,7 @@ impl WsManager {
/// Builds one manager from a slice of WebSocket endpoint configurations.
///
/// Only enabled endpoints are retained.
pub fn from_ws_endpoints(
endpoints: &[crate::KbWsEndpointConfig],
) -> Result<Self, crate::KbError> {
pub fn from_ws_endpoints(endpoints: &[crate::WsEndpointConfig]) -> Result<Self, crate::Error> {
let mut selected_endpoints = std::vec::Vec::new();
let mut max_event_capacity = 1_usize;
for endpoint in endpoints {
@@ -79,7 +77,7 @@ impl WsManager {
let mut clients = std::collections::BTreeMap::new();
for endpoint in selected_endpoints {
if clients.contains_key(endpoint.name.as_str()) {
return Err(crate::KbError::Config(format!(
return Err(crate::Error::Config(format!(
"duplicate websocket endpoint name '{}'",
endpoint.name
)));
@@ -108,7 +106,7 @@ impl WsManager {
}
/// Builds one manager from the application configuration.
pub fn from_config(config: &crate::KbConfig) -> Result<Self, crate::KbError> {
pub fn from_config(config: &crate::Config) -> Result<Self, crate::Error> {
return Self::from_ws_endpoints(&config.solana.ws_endpoints);
}
@@ -142,7 +140,7 @@ impl WsManager {
}
/// Starts all managed endpoints having the requested role.
pub async fn start_role(&self, role: &str) -> Result<usize, crate::KbError> {
pub async fn start_role(&self, role: &str) -> Result<usize, crate::Error> {
let endpoint_names = self.endpoint_names_for_role(role).await;
let mut started_count = 0_usize;
for endpoint_name in endpoint_names {
@@ -159,7 +157,7 @@ impl WsManager {
}
/// Stops all managed endpoints having the requested role.
pub async fn stop_role(&self, role: &str) -> Result<usize, crate::KbError> {
pub async fn stop_role(&self, role: &str) -> Result<usize, crate::Error> {
let endpoint_names = self.endpoint_names_for_role(role).await;
let mut stopped_count = 0_usize;
for endpoint_name in endpoint_names {
@@ -185,20 +183,19 @@ impl WsManager {
}
}
async fn start_endpoint_inner(&self, endpoint_name: &str) -> Result<bool, crate::KbError> {
async fn start_endpoint_inner(&self, endpoint_name: &str) -> Result<bool, crate::Error> {
let client_option = self.client(endpoint_name).await;
let client = match client_option {
Some(client) => client,
None => {
return Err(crate::KbError::InvalidState(format!(
return Err(crate::Error::InvalidState(format!(
"unknown managed websocket endpoint '{}'",
endpoint_name
)));
},
};
let state = client.connection_state().await;
if state == crate::KbConnectionState::Connected
|| state == crate::KbConnectionState::Connecting
if state == crate::ConnectionState::Connected || state == crate::ConnectionState::Connecting
{
return Ok(false);
}
@@ -224,7 +221,7 @@ impl WsManager {
}
/// Starts one managed endpoint.
pub async fn start_endpoint(&self, endpoint_name: &str) -> Result<(), crate::KbError> {
pub async fn start_endpoint(&self, endpoint_name: &str) -> Result<(), crate::Error> {
let start_result = self.start_endpoint_inner(endpoint_name).await;
match start_result {
Ok(_) => return Ok(()),
@@ -232,20 +229,20 @@ impl WsManager {
}
}
async fn stop_endpoint_inner(&self, endpoint_name: &str) -> Result<bool, crate::KbError> {
async fn stop_endpoint_inner(&self, endpoint_name: &str) -> Result<bool, crate::Error> {
let client_option = self.client(endpoint_name).await;
let client = match client_option {
Some(client) => client,
None => {
return Err(crate::KbError::InvalidState(format!(
return Err(crate::Error::InvalidState(format!(
"unknown managed websocket endpoint '{}'",
endpoint_name
)));
},
};
let state = client.connection_state().await;
if state == crate::KbConnectionState::Disconnected
|| state == crate::KbConnectionState::Disconnecting
if state == crate::ConnectionState::Disconnected
|| state == crate::ConnectionState::Disconnecting
{
return Ok(false);
}
@@ -259,7 +256,7 @@ impl WsManager {
}
/// Stops one managed endpoint.
pub async fn stop_endpoint(&self, endpoint_name: &str) -> Result<(), crate::KbError> {
pub async fn stop_endpoint(&self, endpoint_name: &str) -> Result<(), crate::Error> {
let stop_result = self.stop_endpoint_inner(endpoint_name).await;
match stop_result {
Ok(_) => return Ok(()),
@@ -268,7 +265,7 @@ impl WsManager {
}
/// Starts all managed endpoints.
pub async fn start_all(&self) -> Result<usize, crate::KbError> {
pub async fn start_all(&self) -> Result<usize, crate::Error> {
let endpoint_names = self.endpoint_names().await;
let mut started_count = 0_usize;
for endpoint_name in endpoint_names {
@@ -285,7 +282,7 @@ impl WsManager {
}
/// Stops all managed endpoints.
pub async fn stop_all(&self) -> Result<usize, crate::KbError> {
pub async fn stop_all(&self) -> Result<usize, crate::Error> {
let endpoint_names = self.endpoint_names().await;
let mut stopped_count = 0_usize;
for endpoint_name in endpoint_names {
@@ -305,12 +302,12 @@ impl WsManager {
pub async fn active_subscription_count(
&self,
endpoint_name: &str,
) -> Result<usize, crate::KbError> {
) -> Result<usize, crate::Error> {
let client_option = self.client(endpoint_name).await;
let client = match client_option {
Some(client) => client,
None => {
return Err(crate::KbError::InvalidState(format!(
return Err(crate::Error::InvalidState(format!(
"unknown managed websocket endpoint '{}'",
endpoint_name
)));
@@ -320,7 +317,7 @@ impl WsManager {
}
/// Returns a consolidated snapshot of all managed endpoints.
pub async fn snapshot(&self) -> Result<WsManagerSnapshot, crate::KbError> {
pub async fn snapshot(&self) -> Result<WsManagerSnapshot, crate::Error> {
let clients_to_snapshot = {
let clients_guard = self.clients.lock().await;
let mut values = std::vec::Vec::new();
@@ -335,7 +332,7 @@ impl WsManager {
let state = client.connection_state().await;
let active_subscription_count = client.active_subscription_count().await;
if state != crate::KbConnectionState::Disconnected {
if state != crate::ConnectionState::Disconnected {
started_count += 1;
}
endpoints.push(WsManagedEndpointSnapshot {
@@ -356,21 +353,21 @@ impl WsManager {
/// Attaches one shared detection relay to all managed clients.
pub async fn attach_detection_relay(
&self,
database: std::sync::Arc<crate::KbDatabase>,
database: std::sync::Arc<crate::Database>,
queue_capacity: usize,
) -> Result<(), crate::KbError> {
) -> Result<(), crate::Error> {
{
let sender_guard = self.detection_relay_sender.lock().await;
if sender_guard.is_some() {
return Err(crate::KbError::InvalidState(
return Err(crate::Error::InvalidState(
"websocket detection relay is already attached".to_string(),
));
}
}
let persistence = crate::KbDetectionPersistenceService::new(database);
let detector = crate::KbSolanaWsDetectionService::new(persistence);
let relay = crate::KbWsDetectionRelay::new(detector);
let (sender, receiver) = crate::KbWsDetectionRelay::channel(queue_capacity);
let persistence = crate::DetectionPersistenceService::new(database);
let detector = crate::SolanaWsDetectionService::new(persistence);
let relay = crate::WsDetectionRelay::new(detector);
let (sender, receiver) = crate::WsDetectionRelay::channel(queue_capacity);
let relay_task = relay.spawn(receiver);
let relay_abort_handle = relay_task.abort_handle();
{
@@ -397,7 +394,7 @@ impl WsManager {
}
/// Detaches the shared detection relay from all managed clients.
pub async fn detach_detection_relay(&self) -> Result<(), crate::KbError> {
pub async fn detach_detection_relay(&self) -> Result<(), crate::Error> {
let clients = {
let clients_guard = self.clients.lock().await;
let mut values = std::vec::Vec::new();
@@ -433,12 +430,12 @@ impl WsManager {
pub async fn endpoint_state(
&self,
endpoint_name: &str,
) -> Result<crate::KbConnectionState, crate::KbError> {
) -> Result<crate::ConnectionState, crate::Error> {
let client_option = self.client(endpoint_name).await;
let client = match client_option {
Some(client) => client,
None => {
return Err(crate::KbError::InvalidState(format!(
return Err(crate::Error::InvalidState(format!(
"unknown managed websocket endpoint '{}'",
endpoint_name
)));
@@ -450,22 +447,22 @@ impl WsManager {
/// Attaches one shared transaction-resolution relay to all managed clients.
pub async fn attach_transaction_resolution_relay(
&self,
database: std::sync::Arc<crate::KbDatabase>,
database: std::sync::Arc<crate::Database>,
http_pool: std::sync::Arc<crate::HttpEndpointPool>,
http_role: std::string::String,
queue_capacity: usize,
) -> Result<(), crate::KbError> {
) -> Result<(), crate::Error> {
{
let sender_guard = self.transaction_resolution_relay_sender.lock().await;
if sender_guard.is_some() {
return Err(crate::KbError::InvalidState(
return Err(crate::Error::InvalidState(
"websocket transaction resolution relay is already attached".to_string(),
));
}
}
let resolver = crate::KbTransactionResolutionService::new(http_pool, database, http_role);
let relay = crate::KbWsTransactionResolutionRelay::new(resolver);
let (sender, receiver) = crate::KbWsTransactionResolutionRelay::channel(queue_capacity);
let resolver = crate::TransactionResolutionService::new(http_pool, database, http_role);
let relay = crate::WsTransactionResolutionRelay::new(resolver);
let (sender, receiver) = crate::WsTransactionResolutionRelay::channel(queue_capacity);
let relay_task = relay.spawn(receiver);
let relay_abort_handle = relay_task.abort_handle();
{
@@ -491,7 +488,7 @@ impl WsManager {
}
/// Detaches the shared transaction-resolution relay from all managed clients.
pub async fn detach_transaction_resolution_relay(&self) -> Result<(), crate::KbError> {
pub async fn detach_transaction_resolution_relay(&self) -> Result<(), crate::Error> {
let clients = {
let clients_guard = self.clients.lock().await;
let mut values = std::vec::Vec::new();
@@ -520,26 +517,26 @@ impl WsManager {
/// Collects the current hybrid WebSocket watch snapshot.
pub async fn collect_hybrid_watch_snapshot(
&self,
database: std::sync::Arc<crate::KbDatabase>,
) -> Result<crate::KbWsHybridWatchSnapshot, crate::KbError> {
let runtime = crate::KbWsHybridRuntimeService::new(database);
database: std::sync::Arc<crate::Database>,
) -> Result<crate::WsHybridWatchSnapshot, crate::Error> {
let runtime = crate::WsHybridRuntimeService::new(database);
return runtime.collect_watch_snapshot().await;
}
/// Attaches one shared hybrid observation relay to the manager event stream.
pub async fn attach_hybrid_observation_relay(
&self,
database: std::sync::Arc<crate::KbDatabase>,
) -> Result<(), crate::KbError> {
database: std::sync::Arc<crate::Database>,
) -> Result<(), crate::Error> {
{
let abort_guard = self.hybrid_observation_relay_abort_handle.lock().await;
if abort_guard.is_some() {
return Err(crate::KbError::InvalidState(
return Err(crate::Error::InvalidState(
"websocket hybrid observation relay is already attached".to_string(),
));
}
}
let runtime = crate::KbWsHybridRuntimeService::new(database);
let runtime = crate::WsHybridRuntimeService::new(database);
let receiver = self.subscribe_events();
let abort_handle = spawn_hybrid_observation_relay_task(receiver, runtime);
{
@@ -550,7 +547,7 @@ impl WsManager {
}
/// Detaches the shared hybrid observation relay from the manager event stream.
pub async fn detach_hybrid_observation_relay(&self) -> Result<(), crate::KbError> {
pub async fn detach_hybrid_observation_relay(&self) -> Result<(), crate::Error> {
let abort_handle_option = {
let mut abort_guard = self.hybrid_observation_relay_abort_handle.lock().await;
abort_guard.take()
@@ -615,7 +612,7 @@ fn spawn_event_forward_task(
fn spawn_hybrid_observation_relay_task(
mut receiver: tokio::sync::broadcast::Receiver<crate::WsEvent>,
runtime: crate::KbWsHybridRuntimeService,
runtime: crate::WsHybridRuntimeService,
) -> tokio::task::AbortHandle {
let task = tokio::spawn(async move {
loop {
@@ -635,7 +632,7 @@ fn spawn_hybrid_observation_relay_task(
}
async fn handle_hybrid_observation_manager_event(
runtime: &crate::KbWsHybridRuntimeService,
runtime: &crate::WsHybridRuntimeService,
event: crate::WsEvent,
) {
match event {
@@ -660,7 +657,7 @@ async fn handle_hybrid_observation_manager_event(
Ok(value) => value,
Err(error) => {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"cannot serialize logs notification for hybrid relay: {}",
error
);
@@ -671,7 +668,7 @@ async fn handle_hybrid_observation_manager_event(
runtime.record_logs_notification(Some(endpoint_name), &value).await;
if let Err(error) = record_result {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"hybrid logs observation failed: {}",
error
);
@@ -683,17 +680,17 @@ async fn handle_hybrid_observation_manager_event(
}
async fn handle_hybrid_subscription_notification(
runtime: &crate::KbWsHybridRuntimeService,
runtime: &crate::WsHybridRuntimeService,
endpoint_name: std::string::String,
subscription: crate::WsSubscriptionInfo,
notification: crate::KbJsonRpcWsNotification,
notification: crate::JsonRpcWsNotification,
) {
let value_result = serde_json::to_value(notification.clone());
let value = match value_result {
Ok(value) => value,
Err(error) => {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"cannot serialize subscription notification for hybrid relay: {}",
error
);
@@ -705,7 +702,7 @@ async fn handle_hybrid_subscription_notification(
let record_result = runtime.record_logs_notification(Some(endpoint_name), &value).await;
if let Err(error) = record_result {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"hybrid logs observation failed: {}",
error
);
@@ -713,12 +710,12 @@ async fn handle_hybrid_subscription_notification(
return;
}
if method == "programNotification" {
let watched_program_id = kb_first_subscription_param_as_string(&subscription);
let watched_program_id = first_subscription_param_as_string(&subscription);
let watched_program_id = match watched_program_id {
Some(watched_program_id) => watched_program_id,
None => {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"missing watched program id in subscription params"
);
return;
@@ -729,7 +726,7 @@ async fn handle_hybrid_subscription_notification(
.await;
if let Err(error) = record_result {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"hybrid program observation failed: {}",
error
);
@@ -737,12 +734,12 @@ async fn handle_hybrid_subscription_notification(
return;
}
if method == "accountNotification" {
let watched_account = kb_first_subscription_param_as_string(&subscription);
let watched_account = first_subscription_param_as_string(&subscription);
let watched_account = match watched_account {
Some(watched_account) => watched_account,
None => {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "ws_manager",
"missing watched account in subscription params"
);
return;
@@ -753,7 +750,7 @@ async fn handle_hybrid_subscription_notification(
.await;
if let Err(error) = record_result {
tracing::warn!(
target: "kb_lib::ws_manager",
target: "kws_manager",
"hybrid account observation failed: {}",
error
);
@@ -761,7 +758,7 @@ async fn handle_hybrid_subscription_notification(
}
}
fn kb_first_subscription_param_as_string(
fn first_subscription_param_as_string(
subscription: &crate::WsSubscriptionInfo,
) -> std::option::Option<std::string::String> {
let first_param_option = subscription.params.first();
@@ -1227,8 +1224,8 @@ mod tests {
name: &str,
url: std::string::String,
enabled: bool,
) -> crate::KbWsEndpointConfig {
return crate::KbWsEndpointConfig {
) -> crate::WsEndpointConfig {
return crate::WsEndpointConfig {
name: name.to_string(),
enabled,
provider: "test".to_string(),
@@ -1260,17 +1257,17 @@ mod tests {
}
}
async fn create_database() -> crate::KbDatabase {
async fn create_database() -> crate::Database {
let tempdir_result = tempfile::tempdir();
let tempdir = match tempdir_result {
Ok(tempdir) => tempdir,
Err(error) => panic!("tempdir failed: {error}"),
};
let database_path = tempdir.path().join("ws_manager.sqlite3");
let config = crate::KbDatabaseConfig {
let config = crate::DatabaseConfig {
enabled: true,
backend: crate::KbDatabaseBackend::Sqlite,
sqlite: crate::KbSqliteDatabaseConfig {
backend: crate::DatabaseBackend::Sqlite,
sqlite: crate::SqliteDatabaseConfig {
path: database_path.to_string_lossy().to_string(),
create_if_missing: true,
busy_timeout_ms: 5000,
@@ -1279,7 +1276,7 @@ mod tests {
use_wal: true,
},
};
let database_result = crate::KbDatabase::connect_and_initialize(&config).await;
let database_result = crate::Database::connect_and_initialize(&config).await;
match database_result {
Ok(database) => return database,
Err(error) => panic!("database init failed: {error}"),
@@ -1365,7 +1362,7 @@ mod tests {
assert_eq!(snapshot.started_count, 1);
assert_eq!(snapshot.endpoints.len(), 1);
assert_eq!(snapshot.endpoints[0].endpoint_name, "ws_a");
assert_eq!(snapshot.endpoints[0].state, crate::KbConnectionState::Connected);
assert_eq!(snapshot.endpoints[0].state, crate::ConnectionState::Connected);
let stop_result = manager.stop_all().await;
if let Err(error) = stop_result {
panic!("stop_all failed: {error}");
@@ -1405,7 +1402,7 @@ mod tests {
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
let observations_result =
crate::list_recent_onchain_observations(database.as_ref(), 10).await;
crate::query_onchain_observations_list_recent(database.as_ref(), 10).await;
let observations = match observations_result {
Ok(observations) => observations,
Err(error) => panic!("list_recent_onchain_observations failed: {error}"),
@@ -1425,13 +1422,13 @@ mod tests {
#[tokio::test]
async fn from_config_builds_only_enabled_clients() {
let config = crate::KbConfig {
app: crate::KbAppConfig {
let config = crate::Config {
app: crate::AppConfig {
name: "test".to_string(),
environment: "test".to_string(),
auto_reconnect_default: false,
},
logging: crate::KbLoggingConfig {
logging: crate::LoggingConfig {
level: "debug".to_string(),
console_enabled: true,
console_ansi: true,
@@ -1443,11 +1440,11 @@ mod tests {
time_format: "rfc3339_millis".to_string(),
target_filters: std::collections::BTreeMap::new(),
},
data: crate::KbDataConfig {
data: crate::DataConfig {
sqlite_path: "data/test.sqlite3".to_string(),
wallets_directory: "wallets".to_string(),
},
solana: crate::KbSolanaConfig {
solana: crate::SolanaConfig {
http_endpoints: std::vec::Vec::new(),
ws_endpoints: vec![
make_ws_endpoint("ws_a", "ws://127.0.0.1:1".to_string(), true),
@@ -1455,10 +1452,10 @@ mod tests {
make_ws_endpoint("ws_c", "ws://127.0.0.1:3".to_string(), true),
],
},
database: crate::KbDatabaseConfig {
database: crate::DatabaseConfig {
enabled: false,
backend: crate::KbDatabaseBackend::Sqlite,
sqlite: crate::KbSqliteDatabaseConfig {
backend: crate::DatabaseBackend::Sqlite,
sqlite: crate::SqliteDatabaseConfig {
path: "data/test.sqlite3".to_string(),
create_if_missing: true,
busy_timeout_ms: 5000,
@@ -1499,48 +1496,48 @@ mod tests {
assert!(unknown_endpoints.is_empty());
}
async fn seed_hybrid_watch_database(database: &crate::KbDatabase) {
let dex_a = crate::KbDexDto::new(
async fn seed_hybrid_watch_database(database: &crate::Database) {
let dex_a = crate::DexDto::new(
"raydium_amm_v4".to_string(),
"Raydium AmmV4".to_string(),
Some("HybridRaydiumProgram111".to_string()),
None,
true,
);
let dex_a_id_result = crate::upsert_dex(database, &dex_a).await;
let dex_a_id_result = crate::query_dexs_upsert(database, &dex_a).await;
let dex_a_id = match dex_a_id_result {
Ok(dex_a_id) => dex_a_id,
Err(error) => panic!("dex A upsert failed: {error}"),
};
let dex_b = crate::KbDexDto::new(
let dex_b = crate::DexDto::new(
"meteora_dbc".to_string(),
"Meteora DBC".to_string(),
Some("HybridMeteoraProgram111".to_string()),
Some("HybridSharedRouter111".to_string()),
true,
);
let dex_b_id_result = crate::upsert_dex(database, &dex_b).await;
let dex_b_id_result = crate::query_dexs_upsert(database, &dex_b).await;
let dex_b_id = match dex_b_id_result {
Ok(dex_b_id) => dex_b_id,
Err(error) => panic!("dex B upsert failed: {error}"),
};
let pool_a = crate::KbPoolDto::new(
let pool_a = crate::PoolDto::new(
dex_a_id,
"HybridPool111".to_string(),
crate::KbPoolKind::Amm,
crate::KbPoolStatus::Active,
crate::PoolKind::Amm,
crate::PoolStatus::Active,
);
let pool_a_result = crate::upsert_pool(database, &pool_a).await;
let pool_a_result = crate::query_pools_upsert(database, &pool_a).await;
if let Err(error) = pool_a_result {
panic!("pool A upsert failed: {error}");
}
let pool_b = crate::KbPoolDto::new(
let pool_b = crate::PoolDto::new(
dex_b_id,
"HybridPool222".to_string(),
crate::KbPoolKind::Amm,
crate::KbPoolStatus::Inactive,
crate::PoolKind::Amm,
crate::PoolStatus::Inactive,
);
let pool_b_result = crate::upsert_pool(database, &pool_b).await;
let pool_b_result = crate::query_pools_upsert(database, &pool_b).await;
if let Err(error) = pool_b_result {
panic!("pool B upsert failed: {error}");
}
@@ -1610,7 +1607,7 @@ mod tests {
}
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let observations_result =
crate::list_recent_onchain_observations(database.as_ref(), 20).await;
crate::query_onchain_observations_list_recent(database.as_ref(), 20).await;
let observations = match observations_result {
Ok(observations) => observations,
Err(error) => panic!("list_recent_onchain_observations failed: {error}"),