authored.rs (5984B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{string::ToString, vec, vec::Vec}; 3 4 use radroots_event::{ 5 envelope::kind::KIND_DELETION_REQUEST, post::deletion::AuthoredNip09DeletionRequest, 6 wire::Nip01EventWireParts, 7 }; 8 9 /// Builds deterministic unsigned kind-5 wire parts for a strict NIP-09 10 /// deletion request. 11 /// 12 /// Every tag has exactly two elements. Canonical event targets precede 13 /// canonical address targets, followed by the complete unique ascending set of 14 /// target-kind advisories. 15 pub fn authored_nip09_deletion_request_to_wire_parts( 16 request: &AuthoredNip09DeletionRequest, 17 ) -> Nip01EventWireParts { 18 let mut tags = Vec::with_capacity( 19 request 20 .target_count() 21 .saturating_add(request.kind_hints().len()), 22 ); 23 tags.extend( 24 request 25 .event_targets() 26 .iter() 27 .map(|target| vec!["e".to_string(), target.event_id().to_hex()]), 28 ); 29 tags.extend( 30 request 31 .address_targets() 32 .iter() 33 .map(|target| vec!["a".to_string(), target.coordinate().as_str().to_string()]), 34 ); 35 tags.extend( 36 request 37 .kind_hints() 38 .iter() 39 .map(|kind| vec!["k".to_string(), kind.to_string()]), 40 ); 41 42 Nip01EventWireParts { 43 kind: KIND_DELETION_REQUEST, 44 content: request.content().to_string(), 45 tags, 46 } 47 } 48 49 #[cfg(test)] 50 mod tests { 51 use super::*; 52 use radroots_event::post::deletion::{ 53 Nip09DeletionAddressTarget, Nip09DeletionEventTarget, 54 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES, 55 }; 56 57 fn h(character: char) -> String { 58 crate::test_fixtures::fixture_public_key_hex(character) 59 } 60 61 #[test] 62 fn emits_exact_canonical_two_element_target_and_kind_tags() { 63 let request = AuthoredNip09DeletionRequest::new( 64 "superseded", 65 vec![ 66 Nip09DeletionEventTarget::parse(h('f'), 30_402).expect("event target"), 67 Nip09DeletionEventTarget::parse(h('a'), 1).expect("event target"), 68 ], 69 vec![ 70 Nip09DeletionAddressTarget::parse(format!("31923:{}:market", h('e'))) 71 .expect("address target"), 72 Nip09DeletionAddressTarget::parse(format!("30402:{}:produce", h('b'))) 73 .expect("address target"), 74 ], 75 ) 76 .expect("deletion request"); 77 78 let parts = authored_nip09_deletion_request_to_wire_parts(&request); 79 assert_eq!(parts.kind, KIND_DELETION_REQUEST); 80 assert_eq!(parts.content, "superseded"); 81 assert_eq!( 82 parts.tags, 83 vec![ 84 vec!["e".to_string(), h('a')], 85 vec!["e".to_string(), h('f')], 86 vec!["a".to_string(), format!("30402:{}:produce", h('b'))], 87 vec!["a".to_string(), format!("31923:{}:market", h('e'))], 88 vec!["k".to_string(), "1".to_string()], 89 vec!["k".to_string(), "30402".to_string()], 90 vec!["k".to_string(), "31923".to_string()], 91 ] 92 ); 93 assert!(parts.tags.iter().all(|tag| tag.len() == 2)); 94 } 95 96 #[test] 97 fn core_wire_estimator_matches_emitted_json_at_the_strict_boundary() { 98 let target = Nip09DeletionEventTarget::parse(h('a'), 1).expect("event target"); 99 let mut lower = 0usize; 100 let mut upper = radroots_event::post::deletion::RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES; 101 while lower < upper { 102 let candidate = lower + (upper - lower).div_ceil(2); 103 if AuthoredNip09DeletionRequest::new( 104 "\u{0001}".repeat(candidate), 105 vec![target.clone()], 106 Vec::new(), 107 ) 108 .is_ok() 109 { 110 lower = candidate; 111 } else { 112 upper = candidate - 1; 113 } 114 } 115 116 let request = 117 AuthoredNip09DeletionRequest::new("\u{0001}".repeat(lower), vec![target], Vec::new()) 118 .expect("largest content fitting strict signed wire"); 119 let parts = authored_nip09_deletion_request_to_wire_parts(&request); 120 let json = serde_json::json!({ 121 "id": "0".repeat(64), 122 "pubkey": "0".repeat(64), 123 "created_at": u64::MAX, 124 "kind": KIND_DELETION_REQUEST, 125 "tags": parts.tags, 126 "content": parts.content, 127 "sig": "0".repeat(128), 128 }) 129 .to_string(); 130 assert!(json.len() <= RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES); 131 assert_eq!(json.len(), request.maximum_signed_event_wire_bytes()); 132 } 133 134 #[test] 135 fn strict_wire_size_equals_independent_max_u64_serialization_with_escaping() { 136 let request = AuthoredNip09DeletionRequest::new( 137 "reason \"quoted\" \\\n\u{0001} 🌱", 138 vec![Nip09DeletionEventTarget::parse(h('A'), 5).expect("event target")], 139 vec![ 140 Nip09DeletionAddressTarget::parse(format!( 141 "30000:{}:victoria:\"crop\\row:\u{0002}:雪", 142 h('B') 143 )) 144 .expect("address target"), 145 ], 146 ) 147 .expect("escaped deletion request"); 148 let parts = authored_nip09_deletion_request_to_wire_parts(&request); 149 let serialized = format!( 150 "{{\"id\":{},\"pubkey\":{},\"created_at\":{},\"kind\":5,\"tags\":{},\"content\":{},\"sig\":{}}}", 151 serde_json::to_string(&"0".repeat(64)).expect("id JSON"), 152 serde_json::to_string(&"0".repeat(64)).expect("pubkey JSON"), 153 u64::MAX, 154 serde_json::to_string(&parts.tags).expect("tags JSON"), 155 serde_json::to_string(&parts.content).expect("content JSON"), 156 serde_json::to_string(&"0".repeat(128)).expect("signature JSON"), 157 ); 158 159 assert_eq!(request.maximum_signed_event_wire_bytes(), serialized.len()); 160 } 161 }