error.rs (13426B)
1 use std::net::SocketAddr; 2 use std::path::PathBuf; 3 4 use radroots_identity::IdentityError; 5 use radroots_nostr::prelude::RadrootsNostrError; 6 use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError; 7 use radroots_nostr_connect::prelude::RadrootsNostrConnectError; 8 use radroots_nostr_signer::prelude::RadrootsNostrSignerError; 9 use radroots_sql_core::error::SqlError; 10 use thiserror::Error; 11 12 use crate::config::MycTransportDeliveryPolicy; 13 14 #[derive(Debug, Error)] 15 pub enum MycError { 16 #[error("config io error at {path}: {source}")] 17 ConfigIo { 18 path: PathBuf, 19 #[source] 20 source: std::io::Error, 21 }, 22 #[error("config parse error at {path}:{line_number}: {message}")] 23 ConfigParse { 24 path: PathBuf, 25 line_number: usize, 26 message: String, 27 }, 28 #[error("invalid config: {0}")] 29 InvalidConfig(String), 30 #[error("invalid operation: {0}")] 31 InvalidOperation(String), 32 #[error("invalid log filter `{filter}`: {source}")] 33 InvalidLogFilter { 34 filter: String, 35 #[source] 36 source: tracing_subscriber::filter::ParseError, 37 }, 38 #[error("logging already initialized")] 39 LoggingAlreadyInitialized, 40 #[error("failed to create directory {path}: {source}")] 41 CreateDir { 42 path: PathBuf, 43 #[source] 44 source: std::io::Error, 45 }, 46 #[error("persistence io error at {path}: {source}")] 47 PersistenceIo { 48 path: PathBuf, 49 #[source] 50 source: std::io::Error, 51 }, 52 #[error("failed to serialize persistence data at {path}: {source}")] 53 PersistenceSerialize { 54 path: PathBuf, 55 #[source] 56 source: serde_json::Error, 57 }, 58 #[error("failed to parse persistence backup manifest at {path}: {source}")] 59 PersistenceManifestParse { 60 path: PathBuf, 61 #[source] 62 source: serde_json::Error, 63 }, 64 #[error("failed to bind observability server at {bind_addr}: {source}")] 65 ObservabilityBind { 66 bind_addr: SocketAddr, 67 #[source] 68 source: std::io::Error, 69 }, 70 #[error("observability server failed at {bind_addr}: {source}")] 71 ObservabilityServe { 72 bind_addr: SocketAddr, 73 #[source] 74 source: std::io::Error, 75 }, 76 #[error("audit io error at {path}: {source}")] 77 AuditIo { 78 path: PathBuf, 79 #[source] 80 source: std::io::Error, 81 }, 82 #[error("audit parse error at {path}:{line_number}: {source}")] 83 AuditParse { 84 path: PathBuf, 85 line_number: usize, 86 #[source] 87 source: serde_json::Error, 88 }, 89 #[error("failed to serialize audit record at {path}: {source}")] 90 AuditSerialize { 91 path: PathBuf, 92 #[source] 93 source: serde_json::Error, 94 }, 95 #[error("audit sqlite error at {path}: {source}")] 96 AuditSql { 97 path: PathBuf, 98 #[source] 99 source: SqlError, 100 }, 101 #[error("audit sqlite decode error at {path}: {source}")] 102 AuditSqlDecode { 103 path: PathBuf, 104 #[source] 105 source: serde_json::Error, 106 }, 107 #[error("delivery outbox sqlite error at {path}: {source}")] 108 DeliveryOutboxSql { 109 path: PathBuf, 110 #[source] 111 source: SqlError, 112 }, 113 #[error("delivery outbox sqlite decode error at {path}: {source}")] 114 DeliveryOutboxSqlDecode { 115 path: PathBuf, 116 #[source] 117 source: serde_json::Error, 118 }, 119 #[error("failed to serialize delivery outbox record at {path}: {source}")] 120 DeliveryOutboxSerialize { 121 path: PathBuf, 122 #[source] 123 source: serde_json::Error, 124 }, 125 #[error("invalid delivery outbox job id `{0}`")] 126 InvalidDeliveryOutboxJobId(String), 127 #[error("delivery outbox job not found: {0}")] 128 DeliveryOutboxJobNotFound(String), 129 #[error("discovery io error at {path}: {source}")] 130 DiscoveryIo { 131 path: PathBuf, 132 #[source] 133 source: std::io::Error, 134 }, 135 #[error("discovery parse error at {path}: {source}")] 136 DiscoveryParse { 137 path: PathBuf, 138 #[source] 139 source: serde_json::Error, 140 }, 141 #[error("invalid discovery bundle: {0}")] 142 InvalidDiscoveryBundle(String), 143 #[error("invalid discovery event: {0}")] 144 InvalidDiscoveryEvent(String), 145 #[error( 146 "failed to fetch discovery state from all configured relays ({relay_count}): {details}" 147 )] 148 DiscoveryFetchUnavailable { relay_count: usize, details: String }, 149 #[error("discovery refresh attempt {attempt_id} failed: {source}")] 150 DiscoveryRefreshFailed { 151 attempt_id: String, 152 #[source] 153 source: Box<MycError>, 154 }, 155 #[error("custody manager error for {role} identity: {source}")] 156 CustodyManager { 157 role: String, 158 #[source] 159 source: RadrootsNostrAccountsError, 160 }, 161 #[error("custody vault error for {role} identity: {source}")] 162 CustodyVault { 163 role: String, 164 #[source] 165 source: RadrootsNostrAccountsError, 166 }, 167 #[error( 168 "no secret found in custody vault service `{service_name}` for {role} identity `{account_id}`" 169 )] 170 CustodySecretNotFound { 171 role: String, 172 service_name: String, 173 account_id: String, 174 }, 175 #[error( 176 "custody vault service `{service_name}` resolved {role} identity `{resolved_identity_id}` but expected `{account_id}`" 177 )] 178 CustodySecretIdentityMismatch { 179 role: String, 180 service_name: String, 181 account_id: String, 182 resolved_identity_id: String, 183 }, 184 #[error( 185 "public identity file {path} resolved {role} identity `{profile_identity_id}` but expected `{account_id}`" 186 )] 187 CustodyProfileIdentityMismatch { 188 role: String, 189 path: PathBuf, 190 account_id: String, 191 profile_identity_id: String, 192 }, 193 #[error("no selected managed account configured for {role} identity store {path}")] 194 CustodyManagedAccountNotConfigured { role: String, path: PathBuf }, 195 #[error( 196 "selected managed account `{account_id}` in {path} for {role} identity has no secret in keyring service `{service_name}`" 197 )] 198 CustodyManagedAccountPublicOnly { 199 role: String, 200 path: PathBuf, 201 service_name: String, 202 account_id: String, 203 }, 204 #[error("external custody command io error for {role} identity at {path}: {source}")] 205 CustodyExternalCommandIo { 206 role: String, 207 path: PathBuf, 208 #[source] 209 source: std::io::Error, 210 }, 211 #[error( 212 "external custody command for {role} identity at {path} timed out after {timeout_secs}s" 213 )] 214 CustodyExternalCommandTimedOut { 215 role: String, 216 path: PathBuf, 217 timeout_secs: u64, 218 }, 219 #[error( 220 "external custody command for {role} identity at {path} failed with status {status}: {stderr}" 221 )] 222 CustodyExternalCommandFailed { 223 role: String, 224 path: PathBuf, 225 status: String, 226 stderr: String, 227 }, 228 #[error( 229 "external custody command response parse error for {role} identity at {path}: {source}" 230 )] 231 CustodyExternalCommandParse { 232 role: String, 233 path: PathBuf, 234 #[source] 235 source: serde_json::Error, 236 }, 237 #[error( 238 "external custody command returned invalid public identity for {role} at {path}: {message}" 239 )] 240 CustodyExternalCommandInvalidIdentity { 241 role: String, 242 path: PathBuf, 243 message: String, 244 }, 245 #[error(transparent)] 246 Identity(#[from] IdentityError), 247 #[error(transparent)] 248 Nostr(#[from] RadrootsNostrError), 249 #[error(transparent)] 250 NostrConnect(#[from] RadrootsNostrConnectError), 251 #[error(transparent)] 252 SignerState(#[from] RadrootsNostrSignerError), 253 #[error(transparent)] 254 Json(#[from] serde_json::Error), 255 #[error("NIP-46 decrypt failed: {0}")] 256 Nip46Decrypt(String), 257 #[error("NIP-46 encrypt failed: {0}")] 258 Nip46Encrypt(String), 259 #[error("NIP-46 listener notifications closed")] 260 Nip46ListenerClosed, 261 #[error( 262 "Nostr publish failed for {operation} after {attempt_count} attempt(s) with delivery policy {} requiring {required_acknowledged_relay_count} acknowledgements: {details}", 263 delivery_policy.as_str() 264 )] 265 PublishRejected { 266 operation: String, 267 relay_count: usize, 268 acknowledged_relay_count: usize, 269 required_acknowledged_relay_count: usize, 270 delivery_policy: MycTransportDeliveryPolicy, 271 attempt_count: usize, 272 details: String, 273 rejected_relays: Vec<String>, 274 }, 275 #[error( 276 "configured signer identity `{configured_identity_id}` at {identity_path} does not match persisted signer identity `{persisted_identity_id}` in {state_path}" 277 )] 278 SignerIdentityMismatch { 279 identity_path: PathBuf, 280 state_path: PathBuf, 281 configured_identity_id: String, 282 persisted_identity_id: String, 283 }, 284 #[error( 285 "configured signer identity `{configured_identity_id}` does not match imported signer identity `{imported_identity_id}` from {state_path}" 286 )] 287 SignerIdentityImportMismatch { 288 state_path: PathBuf, 289 configured_identity_id: String, 290 imported_identity_id: String, 291 }, 292 } 293 294 impl MycError { 295 pub fn with_discovery_refresh_attempt_id(self, attempt_id: impl Into<String>) -> Self { 296 match self { 297 Self::DiscoveryRefreshFailed { .. } => self, 298 source => Self::DiscoveryRefreshFailed { 299 attempt_id: attempt_id.into(), 300 source: Box::new(source), 301 }, 302 } 303 } 304 305 pub fn discovery_refresh_attempt_id(&self) -> Option<&str> { 306 match self { 307 Self::DiscoveryRefreshFailed { attempt_id, .. } => Some(attempt_id.as_str()), 308 _ => None, 309 } 310 } 311 312 pub fn publish_rejection_details(&self) -> Option<&str> { 313 match self { 314 Self::PublishRejected { details, .. } => Some(details.as_str()), 315 Self::DiscoveryRefreshFailed { source, .. } => source.publish_rejection_details(), 316 _ => None, 317 } 318 } 319 320 pub fn publish_rejection_counts(&self) -> Option<(usize, usize)> { 321 match self { 322 Self::PublishRejected { 323 relay_count, 324 acknowledged_relay_count, 325 .. 326 } => Some((*relay_count, *acknowledged_relay_count)), 327 Self::DiscoveryRefreshFailed { source, .. } => source.publish_rejection_counts(), 328 _ => None, 329 } 330 } 331 332 pub fn publish_rejected_relays(&self) -> Option<&[String]> { 333 match self { 334 Self::PublishRejected { 335 rejected_relays, .. 336 } => Some(rejected_relays.as_slice()), 337 Self::DiscoveryRefreshFailed { source, .. } => source.publish_rejected_relays(), 338 _ => None, 339 } 340 } 341 342 pub fn publish_delivery_policy(&self) -> Option<MycTransportDeliveryPolicy> { 343 match self { 344 Self::PublishRejected { 345 delivery_policy, .. 346 } => Some(*delivery_policy), 347 Self::DiscoveryRefreshFailed { source, .. } => source.publish_delivery_policy(), 348 _ => None, 349 } 350 } 351 352 pub fn publish_attempt_count(&self) -> Option<usize> { 353 match self { 354 Self::PublishRejected { attempt_count, .. } => Some(*attempt_count), 355 Self::DiscoveryRefreshFailed { source, .. } => source.publish_attempt_count(), 356 _ => None, 357 } 358 } 359 360 pub fn publish_required_acknowledged_relay_count(&self) -> Option<usize> { 361 match self { 362 Self::PublishRejected { 363 required_acknowledged_relay_count, 364 .. 365 } => Some(*required_acknowledged_relay_count), 366 Self::DiscoveryRefreshFailed { source, .. } => { 367 source.publish_required_acknowledged_relay_count() 368 } 369 _ => None, 370 } 371 } 372 } 373 374 #[cfg(test)] 375 mod tests { 376 use crate::config::MycTransportDeliveryPolicy; 377 378 use super::MycError; 379 380 #[test] 381 fn discovery_refresh_wrapper_preserves_attempt_id_and_publish_details() { 382 let wrapped = MycError::PublishRejected { 383 operation: "discovery refresh".to_owned(), 384 relay_count: 2, 385 acknowledged_relay_count: 0, 386 required_acknowledged_relay_count: 1, 387 delivery_policy: MycTransportDeliveryPolicy::Any, 388 attempt_count: 2, 389 details: "relay-a: blocked".to_owned(), 390 rejected_relays: vec!["wss://relay-a.example.com".to_owned()], 391 } 392 .with_discovery_refresh_attempt_id("attempt-1"); 393 394 assert_eq!(wrapped.discovery_refresh_attempt_id(), Some("attempt-1")); 395 assert_eq!( 396 wrapped.publish_rejection_details(), 397 Some("relay-a: blocked") 398 ); 399 assert_eq!(wrapped.publish_rejection_counts(), Some((2, 0))); 400 assert_eq!( 401 wrapped.publish_rejected_relays(), 402 Some(["wss://relay-a.example.com".to_owned()].as_slice()) 403 ); 404 assert_eq!( 405 wrapped.publish_delivery_policy(), 406 Some(MycTransportDeliveryPolicy::Any) 407 ); 408 assert_eq!(wrapped.publish_required_acknowledged_relay_count(), Some(1)); 409 assert_eq!(wrapped.publish_attempt_count(), Some(2)); 410 } 411 }