signer.rs (29101B)
1 use crate::runtime::RuntimeError; 2 use crate::runtime::account::AccountRuntimeFailure; 3 use crate::runtime::account::{SHARED_ACCOUNT_STORE_SOURCE, empty_account_resolution_view}; 4 use crate::runtime::config::{ 5 CapabilityBindingTargetKind, RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend, 6 }; 7 use crate::runtime::sdk::{MYC_NIP46_SESSION_SECRET_SERVICE, myc_managed_account_ref_matches}; 8 use crate::view::runtime::{ 9 IdentityPublicView, LocalSignerStatusView, MycStatusView, SignerBindingStatusView, 10 SignerStatusView, SignerWriteKindReadinessView, 11 }; 12 use radroots_events::kinds::{ 13 KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, 14 KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_PROFILE, 15 }; 16 use radroots_nostr_accounts::prelude::RadrootsNostrAccountStatus; 17 use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions; 18 use radroots_nostr_signer::prelude::{ 19 RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, 20 RadrootsNostrSignerCapability, 21 }; 22 use radroots_sdk::radroots_sdk_myc_nip46_product_permission_strings; 23 use std::str::FromStr; 24 use url::Url; 25 26 const SIGNER_BINDING_PROVIDER_RUNTIME_ID: &str = "myc"; 27 const SIGNER_BINDING_MODEL: &str = "session_authorized_remote_signer"; 28 29 #[derive(Debug, Clone, Copy)] 30 struct CliWriteKind { 31 command: &'static str, 32 event_kind: u32, 33 } 34 35 #[derive(Debug, Clone)] 36 pub enum ActorWriteBindingError { 37 Unconfigured(String), 38 Account(AccountRuntimeFailure), 39 } 40 41 impl ActorWriteBindingError { 42 pub fn from_runtime(error: RuntimeError) -> Self { 43 match error { 44 RuntimeError::Account(failure) => Self::Account(failure), 45 other => Self::Unconfigured(other.to_string()), 46 } 47 } 48 49 pub fn reason(self) -> String { 50 match self { 51 Self::Unconfigured(reason) => reason, 52 Self::Account(failure) => failure.to_string(), 53 } 54 } 55 } 56 57 pub fn resolve_signer_status(config: &RuntimeConfig) -> SignerStatusView { 58 match config.signer.backend { 59 SignerBackend::Local => resolve_local_signer_status(config), 60 SignerBackend::Myc => resolve_myc_signer_status(config), 61 } 62 } 63 64 fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { 65 let (account_resolution, resolved_account_id) = 66 match crate::runtime::account::resolve_account_resolution(config) { 67 Ok(resolution) => ( 68 crate::runtime::account::account_resolution_view(&resolution), 69 resolution 70 .resolved_account 71 .as_ref() 72 .map(|account| account.record.account_id.to_string()), 73 ), 74 Err(error) => { 75 let reason = error.to_string(); 76 return SignerStatusView { 77 mode: config.signer.backend.as_str().to_owned(), 78 state: "error".to_owned(), 79 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 80 signer_account_id: None, 81 account_resolution: empty_account_resolution_view(), 82 reason: Some(reason.clone()), 83 binding: disabled_binding_status(), 84 write_kinds: local_write_kind_readiness(false, Some(reason)), 85 local: None, 86 myc: None, 87 }; 88 } 89 }; 90 let secret_backend = crate::runtime::account::secret_backend_status(config); 91 if secret_backend.state == "unavailable" { 92 let reason = secret_backend.reason.clone(); 93 return SignerStatusView { 94 mode: config.signer.backend.as_str().to_owned(), 95 state: "unavailable".to_owned(), 96 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 97 signer_account_id: resolved_account_id.clone(), 98 account_resolution: account_resolution.clone(), 99 reason: reason.clone(), 100 binding: disabled_binding_status(), 101 write_kinds: local_write_kind_readiness(false, reason), 102 local: None, 103 myc: None, 104 }; 105 } 106 107 if secret_backend.state == "error" { 108 let reason = secret_backend.reason.clone(); 109 return SignerStatusView { 110 mode: config.signer.backend.as_str().to_owned(), 111 state: "error".to_owned(), 112 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 113 signer_account_id: resolved_account_id.clone(), 114 account_resolution: account_resolution.clone(), 115 reason: reason.clone(), 116 binding: disabled_binding_status(), 117 write_kinds: local_write_kind_readiness(false, reason), 118 local: None, 119 myc: None, 120 }; 121 } 122 123 let backend = secret_backend 124 .active_backend 125 .unwrap_or_else(|| "unknown".to_owned()); 126 let used_fallback = secret_backend.used_fallback; 127 128 match crate::runtime::account::resolved_account_signing_status(config) { 129 Ok(RadrootsNostrAccountStatus::Ready { account }) => { 130 let capability = RadrootsNostrSignerCapability::LocalAccount(Box::new( 131 RadrootsNostrLocalSignerCapability::new( 132 account.account_id.clone(), 133 account.public_identity.clone(), 134 RadrootsNostrLocalSignerAvailability::SecretBacked, 135 ), 136 )); 137 let local = capability 138 .local_account() 139 .expect("local signer capability") 140 .clone(); 141 SignerStatusView { 142 mode: config.signer.backend.as_str().to_owned(), 143 state: "ready".to_owned(), 144 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 145 signer_account_id: Some(local.account_id.to_string()), 146 account_resolution: account_resolution.clone(), 147 reason: None, 148 binding: disabled_binding_status(), 149 write_kinds: local_write_kind_readiness(true, None), 150 local: Some(LocalSignerStatusView { 151 account_id: local.account_id.to_string(), 152 public_identity: IdentityPublicView::from_public_identity( 153 &local.public_identity, 154 ), 155 availability: local_availability(local.availability).to_owned(), 156 secret_backed: local.is_secret_backed(), 157 backend: backend.clone(), 158 used_fallback, 159 }), 160 myc: None, 161 } 162 } 163 Ok(RadrootsNostrAccountStatus::PublicOnly { account }) => { 164 let reason = AccountRuntimeFailure::watch_only(&account.account_id).to_string(); 165 SignerStatusView { 166 mode: config.signer.backend.as_str().to_owned(), 167 state: "unconfigured".to_owned(), 168 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 169 signer_account_id: Some(account.account_id.to_string()), 170 account_resolution: account_resolution.clone(), 171 reason: Some(reason.clone()), 172 binding: disabled_binding_status(), 173 write_kinds: local_write_kind_readiness(false, Some(reason)), 174 local: Some(LocalSignerStatusView { 175 account_id: account.account_id.to_string(), 176 public_identity: IdentityPublicView::from_public_identity( 177 &account.public_identity, 178 ), 179 availability: local_availability( 180 RadrootsNostrLocalSignerAvailability::PublicOnly, 181 ) 182 .to_owned(), 183 secret_backed: false, 184 backend: backend.clone(), 185 used_fallback, 186 }), 187 myc: None, 188 } 189 } 190 Ok(RadrootsNostrAccountStatus::NotConfigured) => SignerStatusView { 191 mode: config.signer.backend.as_str().to_owned(), 192 state: "unconfigured".to_owned(), 193 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 194 signer_account_id: None, 195 account_resolution: account_resolution.clone(), 196 reason: crate::runtime::account::unresolved_account_reason(config).ok(), 197 binding: disabled_binding_status(), 198 write_kinds: local_write_kind_readiness( 199 false, 200 crate::runtime::account::unresolved_account_reason(config).ok(), 201 ), 202 local: None, 203 myc: None, 204 }, 205 Err(error) => { 206 let reason = error.to_string(); 207 SignerStatusView { 208 mode: config.signer.backend.as_str().to_owned(), 209 state: "error".to_owned(), 210 source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), 211 signer_account_id: resolved_account_id, 212 account_resolution, 213 reason: Some(reason.clone()), 214 binding: disabled_binding_status(), 215 write_kinds: local_write_kind_readiness(false, Some(reason)), 216 local: None, 217 myc: None, 218 } 219 } 220 } 221 } 222 223 fn resolve_myc_signer_status(config: &RuntimeConfig) -> SignerStatusView { 224 let (account_resolution, actor_account_id, actor_pubkey) = 225 match crate::runtime::account::resolve_account_resolution(config) { 226 Ok(resolution) => { 227 let actor_account_id = resolution 228 .resolved_account 229 .as_ref() 230 .map(|account| account.record.account_id.to_string()); 231 let actor_pubkey = resolution 232 .resolved_account 233 .as_ref() 234 .map(|account| account.record.public_identity.public_key_hex.clone()); 235 ( 236 crate::runtime::account::account_resolution_view(&resolution), 237 actor_account_id, 238 actor_pubkey, 239 ) 240 } 241 Err(_) => (empty_account_resolution_view(), None, None), 242 }; 243 let readiness = 244 myc_binding_readiness(config, actor_account_id.as_deref(), actor_pubkey.as_deref()); 245 SignerStatusView { 246 mode: config.signer.backend.as_str().to_owned(), 247 state: if readiness.ready { 248 "ready" 249 } else { 250 "unconfigured" 251 } 252 .to_owned(), 253 source: readiness.source.clone(), 254 signer_account_id: None, 255 account_resolution, 256 reason: readiness.reason.clone(), 257 binding: readiness.binding, 258 write_kinds: myc_write_kind_readiness(readiness.ready, readiness.reason.clone()), 259 local: None, 260 myc: Some(MycStatusView { 261 executable: config.myc.executable.display().to_string(), 262 state: if readiness.ready { 263 "ready" 264 } else { 265 "unconfigured" 266 } 267 .to_owned(), 268 source: readiness.source, 269 service_status: None, 270 ready: readiness.ready, 271 reason: readiness.reason, 272 reasons: readiness.reasons, 273 remote_session_count: usize::from(readiness.signer_session_ref.is_some()), 274 local_signer: None, 275 remote_sessions: Vec::new(), 276 custody: None, 277 }), 278 } 279 } 280 281 fn disabled_binding_status() -> SignerBindingStatusView { 282 SignerBindingStatusView { 283 capability_id: SIGNER_REMOTE_NIP46_CAPABILITY.to_owned(), 284 provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(), 285 binding_model: SIGNER_BINDING_MODEL.to_owned(), 286 state: "disabled".to_owned(), 287 source: "independent local signer mode".to_owned(), 288 target_kind: None, 289 target: None, 290 managed_account_ref: None, 291 signer_session_ref: None, 292 resolved_session_ref: None, 293 matched_session_count: None, 294 reason: Some( 295 "remote myc signer binding is disabled while cli signer mode is `local`".to_owned(), 296 ), 297 } 298 } 299 300 fn cli_write_kinds() -> [CliWriteKind; 12] { 301 [ 302 CliWriteKind { 303 command: "sync.push", 304 event_kind: KIND_PROFILE, 305 }, 306 CliWriteKind { 307 command: "farm.publish", 308 event_kind: KIND_FARM, 309 }, 310 CliWriteKind { 311 command: "listing.publish", 312 event_kind: KIND_LISTING, 313 }, 314 CliWriteKind { 315 command: "listing.update", 316 event_kind: KIND_LISTING, 317 }, 318 CliWriteKind { 319 command: "listing.archive", 320 event_kind: KIND_LISTING, 321 }, 322 CliWriteKind { 323 command: "order.submit", 324 event_kind: KIND_ORDER_REQUEST, 325 }, 326 CliWriteKind { 327 command: "order.accept", 328 event_kind: KIND_ORDER_DECISION, 329 }, 330 CliWriteKind { 331 command: "order.decline", 332 event_kind: KIND_ORDER_DECISION, 333 }, 334 CliWriteKind { 335 command: "order.cancel", 336 event_kind: KIND_ORDER_CANCELLATION, 337 }, 338 CliWriteKind { 339 command: "order.revision.propose", 340 event_kind: KIND_ORDER_REVISION_PROPOSAL, 341 }, 342 CliWriteKind { 343 command: "order.revision.accept", 344 event_kind: KIND_ORDER_REVISION_DECISION, 345 }, 346 CliWriteKind { 347 command: "order.revision.decline", 348 event_kind: KIND_ORDER_REVISION_DECISION, 349 }, 350 ] 351 } 352 353 fn local_write_kind_readiness( 354 ready: bool, 355 reason: Option<String>, 356 ) -> Vec<SignerWriteKindReadinessView> { 357 cli_write_kinds() 358 .iter() 359 .map(|kind| SignerWriteKindReadinessView { 360 command: kind.command.to_owned(), 361 event_kind: kind.event_kind, 362 permission: "local_account_secret".to_owned(), 363 ready, 364 reason: if ready { None } else { reason.clone() }, 365 }) 366 .collect() 367 } 368 369 fn local_availability(value: RadrootsNostrLocalSignerAvailability) -> &'static str { 370 match value { 371 RadrootsNostrLocalSignerAvailability::PublicOnly => "public_only", 372 RadrootsNostrLocalSignerAvailability::SecretBacked => "secret_backed", 373 } 374 } 375 376 #[derive(Debug, Clone)] 377 struct MycBindingReadiness { 378 binding: SignerBindingStatusView, 379 ready: bool, 380 source: String, 381 reason: Option<String>, 382 reasons: Vec<String>, 383 signer_session_ref: Option<String>, 384 } 385 386 fn myc_binding_readiness( 387 config: &RuntimeConfig, 388 actor_account_id: Option<&str>, 389 actor_pubkey: Option<&str>, 390 ) -> MycBindingReadiness { 391 let Some(binding) = config.capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY) else { 392 let reason = "signer.remote_nip46 binding is missing".to_owned(); 393 return MycBindingReadiness { 394 binding: missing_myc_binding_status(reason.clone()), 395 ready: false, 396 source: "no explicit capability binding".to_owned(), 397 reason: Some(reason.clone()), 398 reasons: vec![reason], 399 signer_session_ref: None, 400 }; 401 }; 402 403 let mut reasons = Vec::new(); 404 if binding.target_kind != CapabilityBindingTargetKind::ExplicitEndpoint { 405 reasons.push(format!( 406 "signer.remote_nip46 binding target_kind `{}` is not supported for CLI Myc signing; use `explicit_endpoint`", 407 binding.target_kind.as_str() 408 )); 409 } 410 if let Err(reason) = validate_myc_target(binding.target.as_str()) { 411 reasons.push(reason); 412 } 413 if let Some(managed_account_ref) = binding.managed_account_ref.as_deref() { 414 let managed_account_matches = actor_pubkey 415 .map(|actor_pubkey| { 416 myc_managed_account_ref_matches(managed_account_ref, actor_account_id, actor_pubkey) 417 }) 418 .unwrap_or_else(|| { 419 actor_account_id.is_some_and(|account_id| managed_account_ref == account_id) 420 }); 421 if !managed_account_matches { 422 let reason = if actor_account_id.is_none() && actor_pubkey.is_none() { 423 format!( 424 "signer.remote_nip46 managed_account_ref `{managed_account_ref}` cannot be evaluated because no actor account or pubkey resolved" 425 ) 426 } else { 427 format!( 428 "signer.remote_nip46 managed_account_ref `{managed_account_ref}` does not match actor account or pubkey" 429 ) 430 }; 431 reasons.push(reason); 432 } 433 } 434 let signer_session_ref = binding.signer_session_ref.clone(); 435 if let Some(session_ref) = signer_session_ref.as_deref() { 436 match crate::runtime::account::load_secret_backend_secret( 437 config, 438 session_ref, 439 MYC_NIP46_SESSION_SECRET_SERVICE, 440 ) { 441 Ok(Some(secret)) if secret.trim().is_empty() => { 442 reasons.push(format!( 443 "signer.remote_nip46 signer_session_ref `{session_ref}` resolved to an empty client secret" 444 )); 445 } 446 Ok(Some(_)) => {} 447 Ok(None) => { 448 reasons.push(format!( 449 "signer.remote_nip46 signer_session_ref `{session_ref}` was not found in the account secret backend" 450 )); 451 } 452 Err(error) => reasons.push(error.to_string()), 453 } 454 } else { 455 reasons.push("signer.remote_nip46 signer_session_ref is missing".to_owned()); 456 } 457 458 let ready = reasons.is_empty(); 459 let reason = reasons.first().cloned(); 460 let source = binding.source.as_str().to_owned(); 461 MycBindingReadiness { 462 binding: SignerBindingStatusView { 463 capability_id: binding.capability_id.clone(), 464 provider_runtime_id: binding.provider_runtime_id.clone(), 465 binding_model: binding.binding_model.clone(), 466 state: if ready { "ready" } else { "unconfigured" }.to_owned(), 467 source: source.clone(), 468 target_kind: Some(binding.target_kind.as_str().to_owned()), 469 target: Some(binding.target.clone()), 470 managed_account_ref: binding.managed_account_ref.clone(), 471 signer_session_ref: binding.signer_session_ref.clone(), 472 resolved_session_ref: binding.signer_session_ref.clone().filter(|_| ready), 473 matched_session_count: Some(usize::from(ready)), 474 reason: reason.clone(), 475 }, 476 ready, 477 source, 478 reason, 479 reasons, 480 signer_session_ref, 481 } 482 } 483 484 fn missing_myc_binding_status(reason: String) -> SignerBindingStatusView { 485 SignerBindingStatusView { 486 capability_id: SIGNER_REMOTE_NIP46_CAPABILITY.to_owned(), 487 provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(), 488 binding_model: SIGNER_BINDING_MODEL.to_owned(), 489 state: "unconfigured".to_owned(), 490 source: "no explicit capability binding".to_owned(), 491 target_kind: None, 492 target: None, 493 managed_account_ref: None, 494 signer_session_ref: None, 495 resolved_session_ref: None, 496 matched_session_count: Some(0), 497 reason: Some(reason), 498 } 499 } 500 501 fn validate_myc_target(value: &str) -> Result<(), String> { 502 let trimmed = value.trim(); 503 if trimmed.starts_with("nostrconnect://") { 504 return Err( 505 "signer.remote_nip46 target must be a bunker URI or discovery URL; raw nostrconnect client URIs are signer-side only" 506 .to_owned(), 507 ); 508 } 509 let bunker_uri = if trimmed.starts_with("bunker://") { 510 trimmed.to_owned() 511 } else { 512 let url = Url::parse(trimmed) 513 .map_err(|error| format!("signer.remote_nip46 target is invalid: {error}"))?; 514 url.query_pairs() 515 .find(|(key, _)| key == "uri") 516 .map(|(_, uri)| uri.into_owned()) 517 .ok_or_else(|| { 518 "signer.remote_nip46 discovery target is missing `uri` query parameter".to_owned() 519 })? 520 }; 521 match radroots_nostr_connect::prelude::RadrootsNostrConnectUri::parse(bunker_uri.as_str()) 522 .map_err(|error| format!("signer.remote_nip46 target is invalid: {error}"))? 523 { 524 radroots_nostr_connect::prelude::RadrootsNostrConnectUri::Bunker(_) => Ok(()), 525 radroots_nostr_connect::prelude::RadrootsNostrConnectUri::Client(_) => Err( 526 "signer.remote_nip46 target must resolve to a bunker URI; raw nostrconnect client URIs are signer-side only" 527 .to_owned(), 528 ), 529 } 530 } 531 532 fn myc_write_kind_readiness( 533 ready: bool, 534 reason: Option<String>, 535 ) -> Vec<SignerWriteKindReadinessView> { 536 myc_write_kind_readiness_for_permissions(ready, reason, sdk_myc_nip46_product_permissions()) 537 } 538 539 fn sdk_myc_nip46_product_permissions() -> Result<RadrootsNostrConnectPermissions, String> { 540 RadrootsNostrConnectPermissions::from_str( 541 radroots_sdk_myc_nip46_product_permission_strings() 542 .join(",") 543 .as_str(), 544 ) 545 .map_err(|error| format!("SDK Myc signer permissions are invalid: {error}")) 546 } 547 548 fn myc_write_kind_readiness_for_permissions( 549 ready: bool, 550 reason: Option<String>, 551 permissions: Result<RadrootsNostrConnectPermissions, String>, 552 ) -> Vec<SignerWriteKindReadinessView> { 553 let permissions = match permissions { 554 Ok(permissions) => permissions, 555 Err(error) => { 556 return cli_write_kinds() 557 .iter() 558 .map(|kind| SignerWriteKindReadinessView { 559 command: kind.command.to_owned(), 560 event_kind: kind.event_kind, 561 permission: sign_event_permission_for_kind(kind.event_kind), 562 ready: false, 563 reason: Some(error.clone()), 564 }) 565 .collect(); 566 } 567 }; 568 cli_write_kinds() 569 .iter() 570 .map(|kind| { 571 let permission = sign_event_permission_for_kind(kind.event_kind); 572 let permission_ready = ready && permissions.allows_sign_event_kind(kind.event_kind); 573 SignerWriteKindReadinessView { 574 command: kind.command.to_owned(), 575 event_kind: kind.event_kind, 576 permission, 577 ready: permission_ready, 578 reason: if permission_ready { 579 None 580 } else { 581 reason.clone().or_else(|| { 582 Some( 583 "SDK Myc signer permission is not configured for this event kind" 584 .to_owned(), 585 ) 586 }) 587 }, 588 } 589 }) 590 .collect() 591 } 592 593 fn sign_event_permission_for_kind(event_kind: u32) -> String { 594 format!("sign_event:{event_kind}") 595 } 596 597 #[cfg(test)] 598 mod tests { 599 use super::{ 600 KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, 601 KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_PROFILE, cli_write_kinds, 602 myc_managed_account_ref_matches, myc_write_kind_readiness, 603 myc_write_kind_readiness_for_permissions, sign_event_permission_for_kind, 604 }; 605 use radroots_nostr_connect::prelude::{ 606 RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, 607 }; 608 609 const RESERVED_ORDER_KIND_3431: u32 = 3431; 610 611 #[test] 612 fn write_kind_readiness_matches_active_signed_mutations() { 613 let commands: Vec<&str> = cli_write_kinds() 614 .iter() 615 .map(|write_kind| write_kind.command) 616 .collect(); 617 618 assert_eq!( 619 commands, 620 [ 621 "sync.push", 622 "farm.publish", 623 "listing.publish", 624 "listing.update", 625 "listing.archive", 626 "order.submit", 627 "order.accept", 628 "order.decline", 629 "order.cancel", 630 "order.revision.propose", 631 "order.revision.accept", 632 "order.revision.decline", 633 ] 634 ); 635 assert!(!commands.contains(&"signer.status.get")); 636 } 637 638 #[test] 639 fn order_submit_readiness_uses_active_order_request_kind() { 640 let write_kind = cli_write_kinds() 641 .into_iter() 642 .find(|kind| kind.command == "order.submit") 643 .expect("order submit readiness"); 644 645 assert_eq!(write_kind.event_kind, KIND_ORDER_REQUEST); 646 assert_ne!(write_kind.event_kind, RESERVED_ORDER_KIND_3431); 647 } 648 649 #[test] 650 fn order_decision_readiness_uses_active_order_decision_kind() { 651 for command in ["order.accept", "order.decline"] { 652 let write_kind = cli_write_kinds() 653 .into_iter() 654 .find(|kind| kind.command == command) 655 .expect("order decision readiness"); 656 657 assert_eq!(write_kind.event_kind, KIND_ORDER_DECISION); 658 assert_ne!(write_kind.event_kind, RESERVED_ORDER_KIND_3431); 659 } 660 } 661 662 #[test] 663 fn order_revision_readiness_uses_active_revision_kinds() { 664 let proposal = cli_write_kinds() 665 .into_iter() 666 .find(|kind| kind.command == "order.revision.propose") 667 .expect("order revision propose readiness"); 668 669 assert_eq!(proposal.event_kind, KIND_ORDER_REVISION_PROPOSAL); 670 assert_ne!(proposal.event_kind, RESERVED_ORDER_KIND_3431); 671 672 for command in ["order.revision.accept", "order.revision.decline"] { 673 let write_kind = cli_write_kinds() 674 .into_iter() 675 .find(|kind| kind.command == command) 676 .expect("order revision decision readiness"); 677 678 assert_eq!(write_kind.event_kind, KIND_ORDER_REVISION_DECISION); 679 assert_ne!(write_kind.event_kind, RESERVED_ORDER_KIND_3431); 680 } 681 } 682 683 #[test] 684 fn order_cancel_readiness_uses_order_cancellation_kind() { 685 let cancel = cli_write_kinds() 686 .into_iter() 687 .find(|kind| kind.command == "order.cancel") 688 .expect("order cancel readiness"); 689 assert_eq!(cancel.event_kind, KIND_ORDER_CANCELLATION); 690 assert_ne!(cancel.event_kind, RESERVED_ORDER_KIND_3431); 691 } 692 693 #[test] 694 fn myc_write_readiness_requires_exact_permissions() { 695 let readiness = myc_write_kind_readiness(true, None); 696 let sync = readiness 697 .iter() 698 .find(|kind| kind.command == "sync.push") 699 .expect("sync readiness"); 700 701 assert_eq!(sync.event_kind, KIND_PROFILE); 702 assert_eq!(sync.permission, "sign_event:0"); 703 assert!(!sync.ready); 704 assert_eq!( 705 sync.reason.as_deref(), 706 Some("SDK Myc signer permission is not configured for this event kind") 707 ); 708 709 for (command, event_kind) in [ 710 ("farm.publish", KIND_FARM), 711 ("listing.publish", KIND_LISTING), 712 ("order.submit", KIND_ORDER_REQUEST), 713 ] { 714 let entry = readiness 715 .iter() 716 .find(|kind| kind.command == command) 717 .expect("product write readiness"); 718 719 assert_eq!(entry.permission, sign_event_permission_for_kind(event_kind)); 720 assert!(entry.ready, "{command} should be ready"); 721 assert_eq!(entry.reason, None); 722 } 723 } 724 725 #[test] 726 fn myc_write_readiness_uses_typed_kind_permissions() { 727 let readiness = myc_write_kind_readiness_for_permissions( 728 true, 729 None, 730 Ok(RadrootsNostrConnectPermissions::from(vec![ 731 RadrootsNostrConnectPermission::with_parameter( 732 RadrootsNostrConnectMethod::SignEvent, 733 format!("kind:{KIND_LISTING}"), 734 ), 735 ])), 736 ); 737 let listing = readiness 738 .iter() 739 .find(|kind| kind.command == "listing.publish") 740 .expect("listing readiness"); 741 let farm = readiness 742 .iter() 743 .find(|kind| kind.command == "farm.publish") 744 .expect("farm readiness"); 745 746 assert!(listing.ready); 747 assert!(!farm.ready); 748 } 749 750 #[test] 751 fn myc_managed_account_ref_matches_actor_account_id_or_pubkey() { 752 let actor_account_id = Some("acct_farmer_market"); 753 let actor_pubkey = "02d67b520cb0b835a5ca6ddf78bf3bbfe636d31a523050efc01bf8cb0c680da09e"; 754 755 assert!(myc_managed_account_ref_matches( 756 "acct_farmer_market", 757 actor_account_id, 758 actor_pubkey, 759 )); 760 assert!(myc_managed_account_ref_matches( 761 actor_pubkey, 762 actor_account_id, 763 actor_pubkey, 764 )); 765 assert!(!myc_managed_account_ref_matches( 766 "acct_other", 767 actor_account_id, 768 actor_pubkey, 769 )); 770 } 771 }