draft.rs (24506B)
1 #![forbid(unsafe_code)] 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 borrow::ToOwned, 6 string::{String, ToString}, 7 vec::Vec, 8 }; 9 10 #[cfg(feature = "std")] 11 use std::{ 12 borrow::ToOwned, 13 string::{String, ToString}, 14 vec::Vec, 15 }; 16 17 use crate::RadrootsNostrEvent; 18 use crate::contract::{RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, event_contract}; 19 use crate::ids::{ 20 RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, 21 }; 22 use core::fmt; 23 use sha2::{Digest, Sha256}; 24 25 #[derive(Clone, Debug, PartialEq, Eq)] 26 pub enum RadrootsDraftError { 27 UnknownContract(String), 28 ContractKindMismatch { 29 contract_id: String, 30 expected_kind: u32, 31 actual_kind: u32, 32 }, 33 SignedEventPubkeyMismatch { 34 expected_pubkey: String, 35 actual_pubkey: String, 36 }, 37 SignedEventIdMismatch { 38 expected_event_id: String, 39 actual_event_id: String, 40 }, 41 SignedEventCreatedAtMismatch { 42 expected_created_at: u32, 43 actual_created_at: u32, 44 }, 45 SignedEventKindMismatch { 46 expected_kind: u32, 47 actual_kind: u32, 48 }, 49 SignedEventTagsMismatch { 50 expected_len: usize, 51 actual_len: usize, 52 }, 53 SignedEventContentMismatch { 54 expected_len: usize, 55 actual_len: usize, 56 }, 57 SignedEventComputedIdMismatch { 58 expected_event_id: String, 59 computed_event_id: String, 60 }, 61 IdParse(RadrootsIdParseError), 62 JsonString(String), 63 } 64 65 impl fmt::Display for RadrootsDraftError { 66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 match self { 68 Self::UnknownContract(contract_id) => { 69 write!(f, "unknown event contract `{contract_id}`") 70 } 71 Self::ContractKindMismatch { 72 contract_id, 73 expected_kind, 74 actual_kind, 75 } => write!( 76 f, 77 "event contract `{contract_id}` expects kind {expected_kind}, got {actual_kind}" 78 ), 79 Self::SignedEventPubkeyMismatch { 80 expected_pubkey, 81 actual_pubkey, 82 } => write!( 83 f, 84 "signed event pubkey mismatch: expected {expected_pubkey}, got {actual_pubkey}" 85 ), 86 Self::SignedEventIdMismatch { 87 expected_event_id, 88 actual_event_id, 89 } => write!( 90 f, 91 "signed event id mismatch: expected {expected_event_id}, got {actual_event_id}" 92 ), 93 Self::SignedEventCreatedAtMismatch { 94 expected_created_at, 95 actual_created_at, 96 } => write!( 97 f, 98 "signed event created_at mismatch: expected {expected_created_at}, got {actual_created_at}" 99 ), 100 Self::SignedEventKindMismatch { 101 expected_kind, 102 actual_kind, 103 } => write!( 104 f, 105 "signed event kind mismatch: expected {expected_kind}, got {actual_kind}" 106 ), 107 Self::SignedEventTagsMismatch { 108 expected_len, 109 actual_len, 110 } => write!( 111 f, 112 "signed event tags mismatch: expected {expected_len} tags, got {actual_len} tags" 113 ), 114 Self::SignedEventContentMismatch { 115 expected_len, 116 actual_len, 117 } => write!( 118 f, 119 "signed event content mismatch: expected {expected_len} bytes, got {actual_len} bytes" 120 ), 121 Self::SignedEventComputedIdMismatch { 122 expected_event_id, 123 computed_event_id, 124 } => write!( 125 f, 126 "signed event computed id mismatch: expected {expected_event_id}, computed {computed_event_id}" 127 ), 128 Self::IdParse(error) => write!(f, "{error}"), 129 Self::JsonString(error) => write!(f, "json string serialization failed: {error}"), 130 } 131 } 132 } 133 134 #[cfg(feature = "std")] 135 impl std::error::Error for RadrootsDraftError {} 136 137 impl From<RadrootsIdParseError> for RadrootsDraftError { 138 fn from(value: RadrootsIdParseError) -> Self { 139 Self::IdParse(value) 140 } 141 } 142 143 impl From<serde_json::Error> for RadrootsDraftError { 144 fn from(value: serde_json::Error) -> Self { 145 Self::JsonString(value.to_string()) 146 } 147 } 148 149 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 150 #[derive(Clone, Debug, PartialEq, Eq)] 151 pub struct RadrootsFrozenEventDraft { 152 pub contract_id: String, 153 pub contract_registry_version: u32, 154 pub kind: u32, 155 pub created_at: u32, 156 pub tags: Vec<Vec<String>>, 157 pub content: String, 158 pub expected_pubkey: String, 159 pub expected_event_id: String, 160 } 161 162 impl RadrootsFrozenEventDraft { 163 pub fn new( 164 contract_id: impl Into<String>, 165 kind: u32, 166 created_at: u32, 167 tags: Vec<Vec<String>>, 168 content: impl Into<String>, 169 expected_pubkey: impl AsRef<str>, 170 ) -> Result<Self, RadrootsDraftError> { 171 let contract_id = contract_id.into(); 172 let contract = event_contract(&contract_id) 173 .ok_or_else(|| RadrootsDraftError::UnknownContract(contract_id.clone()))?; 174 if contract.kind != kind { 175 return Err(RadrootsDraftError::ContractKindMismatch { 176 contract_id, 177 expected_kind: contract.kind, 178 actual_kind: kind, 179 }); 180 } 181 let expected_pubkey = RadrootsPublicKey::parse(expected_pubkey.as_ref())?.into_string(); 182 let content = content.into(); 183 let expected_event_id = 184 compute_nip01_event_id(expected_pubkey.as_str(), created_at, kind, &tags, &content)? 185 .into_string(); 186 Ok(Self { 187 contract_id: contract.id.to_owned(), 188 contract_registry_version: RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, 189 kind, 190 created_at, 191 tags, 192 content, 193 expected_pubkey, 194 expected_event_id, 195 }) 196 } 197 198 pub fn nip01_preimage(&self) -> Result<String, RadrootsDraftError> { 199 nip01_event_id_preimage( 200 self.expected_pubkey.as_str(), 201 self.created_at, 202 self.kind, 203 &self.tags, 204 self.content.as_str(), 205 ) 206 } 207 } 208 209 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 210 #[derive(Clone, Debug, PartialEq, Eq)] 211 pub struct RadrootsSignedNostrEventParts { 212 pub id: String, 213 pub pubkey: String, 214 pub created_at: u32, 215 pub kind: u32, 216 pub tags: Vec<Vec<String>>, 217 pub content: String, 218 pub sig: String, 219 pub raw_json: String, 220 } 221 222 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 223 #[derive(Clone, Debug, PartialEq, Eq)] 224 pub struct RadrootsSignedNostrEvent { 225 pub id: String, 226 pub pubkey: String, 227 pub created_at: u32, 228 pub kind: u32, 229 pub tags: Vec<Vec<String>>, 230 pub content: String, 231 pub sig: String, 232 pub raw_json: String, 233 } 234 235 impl RadrootsSignedNostrEvent { 236 pub fn new(parts: RadrootsSignedNostrEventParts) -> Result<Self, RadrootsDraftError> { 237 let id = RadrootsEventId::parse(parts.id)?.into_string(); 238 let pubkey = RadrootsPublicKey::parse(parts.pubkey)?.into_string(); 239 let sig = RadrootsEventSignature::parse(parts.sig)?.into_string(); 240 Ok(Self { 241 id, 242 pubkey, 243 created_at: parts.created_at, 244 kind: parts.kind, 245 tags: parts.tags, 246 content: parts.content, 247 sig, 248 raw_json: parts.raw_json, 249 }) 250 } 251 252 pub fn from_event( 253 event: RadrootsNostrEvent, 254 raw_json: impl Into<String>, 255 ) -> Result<Self, RadrootsDraftError> { 256 Self::new(RadrootsSignedNostrEventParts { 257 id: event.id, 258 pubkey: event.author, 259 created_at: event.created_at, 260 kind: event.kind, 261 tags: event.tags, 262 content: event.content, 263 sig: event.sig, 264 raw_json: raw_json.into(), 265 }) 266 } 267 } 268 269 pub fn validate_signed_nostr_event_matches_draft( 270 signed_event: &RadrootsSignedNostrEvent, 271 draft: &RadrootsFrozenEventDraft, 272 ) -> Result<(), RadrootsDraftError> { 273 if signed_event.pubkey.as_str() != draft.expected_pubkey.as_str() { 274 return Err(RadrootsDraftError::SignedEventPubkeyMismatch { 275 expected_pubkey: draft.expected_pubkey.clone(), 276 actual_pubkey: signed_event.pubkey.clone(), 277 }); 278 } 279 if signed_event.id.as_str() != draft.expected_event_id.as_str() { 280 return Err(RadrootsDraftError::SignedEventIdMismatch { 281 expected_event_id: draft.expected_event_id.clone(), 282 actual_event_id: signed_event.id.clone(), 283 }); 284 } 285 if signed_event.created_at != draft.created_at { 286 return Err(RadrootsDraftError::SignedEventCreatedAtMismatch { 287 expected_created_at: draft.created_at, 288 actual_created_at: signed_event.created_at, 289 }); 290 } 291 if signed_event.kind != draft.kind { 292 return Err(RadrootsDraftError::SignedEventKindMismatch { 293 expected_kind: draft.kind, 294 actual_kind: signed_event.kind, 295 }); 296 } 297 if signed_event.tags != draft.tags { 298 return Err(RadrootsDraftError::SignedEventTagsMismatch { 299 expected_len: draft.tags.len(), 300 actual_len: signed_event.tags.len(), 301 }); 302 } 303 if signed_event.content != draft.content { 304 return Err(RadrootsDraftError::SignedEventContentMismatch { 305 expected_len: draft.content.len(), 306 actual_len: signed_event.content.len(), 307 }); 308 } 309 let computed_event_id = compute_nip01_event_id( 310 signed_event.pubkey.as_str(), 311 signed_event.created_at, 312 signed_event.kind, 313 &signed_event.tags, 314 signed_event.content.as_str(), 315 )? 316 .into_string(); 317 if computed_event_id.as_str() != signed_event.id.as_str() { 318 return Err(RadrootsDraftError::SignedEventComputedIdMismatch { 319 expected_event_id: signed_event.id.clone(), 320 computed_event_id, 321 }); 322 } 323 Ok(()) 324 } 325 326 pub fn compute_nip01_event_id( 327 pubkey: &str, 328 created_at: u32, 329 kind: u32, 330 tags: &[Vec<String>], 331 content: &str, 332 ) -> Result<RadrootsEventId, RadrootsDraftError> { 333 let pubkey = RadrootsPublicKey::parse(pubkey)?; 334 let preimage = nip01_event_id_preimage(pubkey.as_str(), created_at, kind, tags, content)?; 335 let digest = Sha256::digest(preimage.as_bytes()); 336 let event_id = hex::encode(digest); 337 Ok(RadrootsEventId::parse(event_id)?) 338 } 339 340 pub fn nip01_event_id_preimage( 341 pubkey: &str, 342 created_at: u32, 343 kind: u32, 344 tags: &[Vec<String>], 345 content: &str, 346 ) -> Result<String, RadrootsDraftError> { 347 let mut preimage = String::new(); 348 preimage.push_str("[0,"); 349 push_json_string(&mut preimage, pubkey)?; 350 preimage.push(','); 351 preimage.push_str(created_at.to_string().as_str()); 352 preimage.push(','); 353 preimage.push_str(kind.to_string().as_str()); 354 preimage.push_str(",["); 355 for (tag_index, tag) in tags.iter().enumerate() { 356 if tag_index > 0 { 357 preimage.push(','); 358 } 359 preimage.push('['); 360 for (value_index, value) in tag.iter().enumerate() { 361 if value_index > 0 { 362 preimage.push(','); 363 } 364 push_json_string(&mut preimage, value)?; 365 } 366 preimage.push(']'); 367 } 368 preimage.push_str("],"); 369 push_json_string(&mut preimage, content)?; 370 preimage.push(']'); 371 Ok(preimage) 372 } 373 374 fn push_json_string(target: &mut String, value: &str) -> Result<(), RadrootsDraftError> { 375 target.push_str(serde_json::to_string(value)?.as_str()); 376 Ok(()) 377 } 378 379 #[cfg(test)] 380 mod tests { 381 use super::*; 382 use crate::kinds::{KIND_POST, KIND_PROFILE}; 383 384 fn hex_64(character: char) -> String { 385 core::iter::repeat_n(character, 64).collect() 386 } 387 388 fn signed_event_for_draft(draft: &RadrootsFrozenEventDraft) -> RadrootsSignedNostrEvent { 389 RadrootsSignedNostrEvent::new(RadrootsSignedNostrEventParts { 390 id: draft.expected_event_id.clone(), 391 pubkey: draft.expected_pubkey.clone(), 392 created_at: draft.created_at, 393 kind: draft.kind, 394 tags: draft.tags.clone(), 395 content: draft.content.clone(), 396 sig: "b".repeat(128), 397 raw_json: "{}".to_owned(), 398 }) 399 .expect("signed event") 400 } 401 402 fn post_draft() -> RadrootsFrozenEventDraft { 403 RadrootsFrozenEventDraft::new( 404 "radroots.social.post.v1", 405 KIND_POST, 406 1_700_000_000, 407 vec![vec!["t".to_owned(), "soil".to_owned()]], 408 "hello", 409 "a".repeat(64), 410 ) 411 .expect("draft") 412 } 413 414 #[test] 415 fn frozen_draft_computes_expected_event_id() { 416 let draft = RadrootsFrozenEventDraft::new( 417 "radroots.social.post.v1", 418 KIND_POST, 419 1_700_000_000, 420 vec![ 421 vec!["t".to_owned(), "soil".to_owned()], 422 vec!["p".to_owned(), hex_64('b')], 423 ], 424 "hello", 425 hex_64('a'), 426 ) 427 .expect("draft"); 428 429 assert_eq!( 430 draft.nip01_preimage().expect("preimage"), 431 "[0,\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",1700000000,1,[[\"t\",\"soil\"],[\"p\",\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"]],\"hello\"]" 432 ); 433 assert_eq!( 434 draft.expected_event_id, 435 "59d2486ef5557e0e317127de55005f2863361ad4041277ae523a869f2294cf9c" 436 ); 437 } 438 439 #[test] 440 fn deterministic_event_id_changes_when_preimage_changes() { 441 let tags = vec![vec!["t".to_owned(), "soil".to_owned()]]; 442 let base = compute_nip01_event_id(hex_64('a').as_str(), 1, KIND_POST, &tags, "hello") 443 .expect("base"); 444 let pubkey_changed = 445 compute_nip01_event_id(hex_64('b').as_str(), 1, KIND_POST, &tags, "hello") 446 .expect("pubkey"); 447 let time_changed = 448 compute_nip01_event_id(hex_64('a').as_str(), 2, KIND_POST, &tags, "hello") 449 .expect("time"); 450 let kind_changed = 451 compute_nip01_event_id(hex_64('a').as_str(), 1, KIND_PROFILE, &tags, "hello") 452 .expect("kind"); 453 let tag_order_changed = compute_nip01_event_id( 454 hex_64('a').as_str(), 455 1, 456 KIND_POST, 457 &[ 458 vec!["p".to_owned(), hex_64('c')], 459 vec!["t".to_owned(), "soil".to_owned()], 460 ], 461 "hello", 462 ) 463 .expect("tag order"); 464 let content_changed = 465 compute_nip01_event_id(hex_64('a').as_str(), 1, KIND_POST, &tags, "hello!") 466 .expect("content"); 467 468 assert_ne!(base, pubkey_changed); 469 assert_ne!(base, time_changed); 470 assert_ne!(base, kind_changed); 471 assert_ne!(base, tag_order_changed); 472 assert_ne!(base, content_changed); 473 } 474 475 #[test] 476 fn profile_golden_event_id_is_stable() { 477 let event_id = compute_nip01_event_id(hex_64('c').as_str(), 1_700_000_001, 0, &[], "{}") 478 .expect("event id"); 479 480 assert_eq!( 481 event_id.as_str(), 482 "2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde" 483 ); 484 } 485 486 #[test] 487 fn draft_constructor_rejects_unknown_contract_and_kind_mismatch() { 488 let unknown = 489 RadrootsFrozenEventDraft::new("missing", KIND_POST, 1, Vec::new(), "", hex_64('a')) 490 .expect_err("unknown contract"); 491 assert!(matches!(unknown, RadrootsDraftError::UnknownContract(_))); 492 493 let mismatch = RadrootsFrozenEventDraft::new( 494 "radroots.social.post.v1", 495 KIND_PROFILE, 496 1, 497 Vec::new(), 498 "", 499 hex_64('a'), 500 ) 501 .expect_err("kind mismatch"); 502 assert!(matches!( 503 mismatch, 504 RadrootsDraftError::ContractKindMismatch { .. } 505 )); 506 507 let invalid_pubkey = RadrootsFrozenEventDraft::new( 508 "radroots.social.post.v1", 509 KIND_POST, 510 1, 511 Vec::new(), 512 "", 513 "not-hex", 514 ) 515 .expect_err("invalid pubkey"); 516 assert!(matches!(invalid_pubkey, RadrootsDraftError::IdParse(_))); 517 } 518 519 #[test] 520 fn signed_event_validates_ids_and_roundtrips_with_serde() { 521 let signed = RadrootsSignedNostrEvent::new(RadrootsSignedNostrEventParts { 522 id: hex_64('d'), 523 pubkey: hex_64('e'), 524 created_at: 10, 525 kind: KIND_POST, 526 tags: Vec::new(), 527 content: "hello".to_owned(), 528 sig: "f".repeat(128), 529 raw_json: "{\"id\":\"fixture\"}".to_owned(), 530 }) 531 .expect("signed event"); 532 let json = serde_json::to_string(&signed).expect("serialize"); 533 let decoded: RadrootsSignedNostrEvent = serde_json::from_str(&json).expect("deserialize"); 534 535 assert_eq!(decoded, signed); 536 assert_eq!(decoded.pubkey, hex_64('e')); 537 } 538 539 #[test] 540 fn signed_event_from_nostr_event_validates_parts() { 541 let event = RadrootsNostrEvent { 542 id: hex_64('1'), 543 author: hex_64('2'), 544 created_at: 42, 545 kind: KIND_POST, 546 tags: vec![vec!["t".to_owned(), "soil".to_owned()]], 547 content: "hello".to_owned(), 548 sig: "3".repeat(128), 549 }; 550 let signed = RadrootsSignedNostrEvent::from_event(event, "{\"id\":\"fixture\"}") 551 .expect("signed event"); 552 553 assert_eq!(signed.id, hex_64('1')); 554 assert_eq!(signed.pubkey, hex_64('2')); 555 assert_eq!(signed.sig, "3".repeat(128)); 556 assert_eq!(signed.raw_json, "{\"id\":\"fixture\"}"); 557 558 let invalid = RadrootsSignedNostrEvent::new(RadrootsSignedNostrEventParts { 559 id: "not-hex".to_owned(), 560 pubkey: hex_64('e'), 561 created_at: 10, 562 kind: KIND_POST, 563 tags: Vec::new(), 564 content: String::new(), 565 sig: "f".repeat(128), 566 raw_json: "{}".to_owned(), 567 }) 568 .expect_err("invalid id"); 569 assert!(matches!(invalid, RadrootsDraftError::IdParse(_))); 570 571 let invalid = RadrootsSignedNostrEvent::new(RadrootsSignedNostrEventParts { 572 id: hex_64('d'), 573 pubkey: "not-hex".to_owned(), 574 created_at: 10, 575 kind: KIND_POST, 576 tags: Vec::new(), 577 content: String::new(), 578 sig: "f".repeat(128), 579 raw_json: "{}".to_owned(), 580 }) 581 .expect_err("invalid pubkey"); 582 assert!(matches!(invalid, RadrootsDraftError::IdParse(_))); 583 584 let invalid = RadrootsSignedNostrEvent::new(RadrootsSignedNostrEventParts { 585 id: hex_64('d'), 586 pubkey: hex_64('e'), 587 created_at: 10, 588 kind: KIND_POST, 589 tags: Vec::new(), 590 content: String::new(), 591 sig: "not-hex".to_owned(), 592 raw_json: "{}".to_owned(), 593 }) 594 .expect_err("invalid sig"); 595 assert!(matches!(invalid, RadrootsDraftError::IdParse(_))); 596 } 597 598 #[test] 599 fn signed_event_validation_accepts_exact_draft_match() { 600 let draft = post_draft(); 601 let signed = signed_event_for_draft(&draft); 602 603 validate_signed_nostr_event_matches_draft(&signed, &draft).expect("valid signed event"); 604 } 605 606 #[test] 607 fn signed_event_validation_rejects_draft_mismatches() { 608 let draft = post_draft(); 609 610 let mut signed = signed_event_for_draft(&draft); 611 signed.pubkey = hex_64('c'); 612 let error = 613 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 614 assert!(matches!( 615 error, 616 RadrootsDraftError::SignedEventPubkeyMismatch { .. } 617 )); 618 619 let mut signed = signed_event_for_draft(&draft); 620 signed.id = hex_64('d'); 621 let error = 622 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 623 assert!(matches!( 624 error, 625 RadrootsDraftError::SignedEventIdMismatch { .. } 626 )); 627 628 let mut signed = signed_event_for_draft(&draft); 629 signed.created_at += 1; 630 let error = 631 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 632 assert!(matches!( 633 error, 634 RadrootsDraftError::SignedEventCreatedAtMismatch { .. } 635 )); 636 637 let mut signed = signed_event_for_draft(&draft); 638 signed.kind = KIND_PROFILE; 639 let error = 640 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 641 assert!(matches!( 642 error, 643 RadrootsDraftError::SignedEventKindMismatch { .. } 644 )); 645 646 let mut signed = signed_event_for_draft(&draft); 647 signed.tags.push(vec!["p".to_owned(), hex_64('e')]); 648 let error = 649 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 650 assert!(matches!( 651 error, 652 RadrootsDraftError::SignedEventTagsMismatch { .. } 653 )); 654 655 let mut signed = signed_event_for_draft(&draft); 656 signed.content = "changed".to_owned(); 657 let error = 658 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 659 assert!(matches!( 660 error, 661 RadrootsDraftError::SignedEventContentMismatch { .. } 662 )); 663 664 let mut draft = post_draft(); 665 draft.expected_event_id = hex_64('f'); 666 let signed = signed_event_for_draft(&draft); 667 let error = 668 validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); 669 assert!(matches!( 670 error, 671 RadrootsDraftError::SignedEventComputedIdMismatch { .. } 672 )); 673 } 674 675 #[test] 676 fn draft_errors_format_all_variants() { 677 let errors = [ 678 RadrootsDraftError::UnknownContract("missing".to_owned()), 679 RadrootsDraftError::ContractKindMismatch { 680 contract_id: "radroots.social.post.v1".to_owned(), 681 expected_kind: KIND_POST, 682 actual_kind: KIND_PROFILE, 683 }, 684 RadrootsDraftError::SignedEventPubkeyMismatch { 685 expected_pubkey: hex_64('a'), 686 actual_pubkey: hex_64('b'), 687 }, 688 RadrootsDraftError::SignedEventIdMismatch { 689 expected_event_id: hex_64('c'), 690 actual_event_id: hex_64('d'), 691 }, 692 RadrootsDraftError::SignedEventCreatedAtMismatch { 693 expected_created_at: 1, 694 actual_created_at: 2, 695 }, 696 RadrootsDraftError::SignedEventKindMismatch { 697 expected_kind: KIND_POST, 698 actual_kind: KIND_PROFILE, 699 }, 700 RadrootsDraftError::SignedEventTagsMismatch { 701 expected_len: 1, 702 actual_len: 2, 703 }, 704 RadrootsDraftError::SignedEventContentMismatch { 705 expected_len: 5, 706 actual_len: 7, 707 }, 708 RadrootsDraftError::SignedEventComputedIdMismatch { 709 expected_event_id: hex_64('e'), 710 computed_event_id: hex_64('f'), 711 }, 712 RadrootsDraftError::from(RadrootsIdParseError::Empty), 713 ]; 714 715 for error in errors { 716 assert!(!error.to_string().is_empty()); 717 } 718 719 let json_error = serde_json::from_str::<String>("{").expect_err("json error"); 720 let error = RadrootsDraftError::from(json_error); 721 assert!( 722 error 723 .to_string() 724 .contains("json string serialization failed") 725 ); 726 } 727 728 #[test] 729 fn event_id_computation_rejects_invalid_pubkeys() { 730 let error = 731 compute_nip01_event_id("not-hex", 1, KIND_POST, &[], "").expect_err("invalid pubkey"); 732 assert!(matches!(error, RadrootsDraftError::IdParse(_))); 733 } 734 }