1use crate::event_ext::FinalizeUnsignedWithId;
14use nostr_sdk::prelude::{FinalizeEvent, FinalizeUnsignedEvent};
15use nostr_sdk::prelude::*;
16use rusqlite::{params, OptionalExtension};
17
18#[repr(i64)]
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub enum WrapRole {
24 Recipient = 0,
26 SelfSend = 1,
28 Retry = 2,
31}
32
33impl WrapRole {
34 fn from_i64(v: i64) -> Self {
35 match v {
36 1 => Self::SelfSend,
37 2 => Self::Retry,
38 _ => Self::Recipient,
39 }
40 }
41}
42
43#[derive(Clone, Debug)]
44pub struct StoredWrapKey {
45 pub wrap_event_id: EventId,
46 pub rumor_id: EventId,
47 pub recipient_pubkey: PublicKey,
48 pub role: WrapRole,
49 pub secret: SecretKey,
50 pub relay_urls: Vec<String>,
53}
54
55pub fn store_wrap_key(
58 wrap_event_id: &EventId,
59 rumor_id: &EventId,
60 recipient_pubkey: &PublicKey,
61 role: WrapRole,
62 secret: &SecretKey,
63 relay_urls: &[String],
64) -> Result<(), String> {
65 let conn = super::get_write_connection_guard_static()?;
66 let now = std::time::SystemTime::now()
67 .duration_since(std::time::UNIX_EPOCH)
68 .unwrap()
69 .as_secs() as i64;
70 let relays_json = serde_json::to_string(relay_urls)
71 .map_err(|e| format!("Failed to encode relay urls: {}", e))?;
72 conn.execute(
73 "INSERT OR REPLACE INTO nip17_wrap_keys
74 (wrap_event_id, rumor_id, recipient_pubkey, role, secret, relay_urls, created_at)
75 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
76 params![
77 wrap_event_id.to_hex(),
78 rumor_id.to_hex(),
79 recipient_pubkey.to_hex(),
80 role as i64,
81 secret.as_secret_bytes(),
82 relays_json,
83 now,
84 ],
85 )
86 .map_err(|e| format!("Failed to insert wrap key: {}", e))?;
87 crate::log_info!(
88 "[NIP-17 keys] stored {:?} key for rumor {} (wrap {})",
89 role,
90 rumor_id.to_hex(),
91 wrap_event_id.to_hex()
92 );
93 Ok(())
94}
95
96pub fn get_wrap_keys_for_rumor(rumor_id: &EventId) -> Result<Vec<StoredWrapKey>, String> {
99 let conn = super::get_db_connection_guard_static()?;
100 let mut stmt = conn
101 .prepare(
102 "SELECT wrap_event_id, rumor_id, recipient_pubkey, role, secret, relay_urls
103 FROM nip17_wrap_keys WHERE rumor_id = ?1",
104 )
105 .map_err(|e| format!("Failed to prepare query: {}", e))?;
106
107 let rows = stmt
108 .query_map(params![rumor_id.to_hex()], |row| {
109 let wrap_hex: String = row.get(0)?;
110 let rumor_hex: String = row.get(1)?;
111 let recipient_hex: String = row.get(2)?;
112 let role_i: i64 = row.get(3)?;
113 let secret_blob: Vec<u8> = row.get(4)?;
114 let relays_json: String = row.get(5)?;
115 Ok((wrap_hex, rumor_hex, recipient_hex, role_i, secret_blob, relays_json))
116 })
117 .map_err(|e| format!("Failed to query wrap keys: {}", e))?;
118
119 let mut out = Vec::new();
120 for row_res in rows {
121 let (wrap_hex, rumor_hex, recipient_hex, role_i, secret_blob, relays_json) =
122 row_res.map_err(|e| format!("Row read error: {}", e))?;
123 let wrap_event_id =
124 EventId::from_hex(&wrap_hex).map_err(|e| format!("Bad wrap id: {}", e))?;
125 let rumor_id_parsed =
126 EventId::from_hex(&rumor_hex).map_err(|e| format!("Bad rumor id: {}", e))?;
127 let recipient_pubkey =
128 PublicKey::from_hex(&recipient_hex).map_err(|e| format!("Bad pubkey: {}", e))?;
129 let secret =
130 SecretKey::from_slice(&secret_blob).map_err(|e| format!("Bad secret: {}", e))?;
131 let relay_urls: Vec<String> = serde_json::from_str(&relays_json)
132 .map_err(|e| format!("Bad relay urls: {}", e))?;
133 out.push(StoredWrapKey {
134 wrap_event_id,
135 rumor_id: rumor_id_parsed,
136 recipient_pubkey,
137 role: WrapRole::from_i64(role_i),
138 secret,
139 relay_urls,
140 });
141 }
142 Ok(out)
143}
144
145pub fn has_wrap_keys_for_rumor(rumor_id: &EventId) -> Result<bool, String> {
149 let conn = super::get_db_connection_guard_static()?;
150 conn.query_row(
151 "SELECT EXISTS(SELECT 1 FROM nip17_wrap_keys WHERE rumor_id = ?1)",
152 params![rumor_id.to_hex()],
153 |row| row.get::<_, bool>(0),
154 )
155 .map_err(|e| format!("Failed to check wrap keys: {}", e))
156}
157
158pub fn purge_wrap_keys(wrap_event_ids: &[EventId]) -> Result<(), String> {
163 if wrap_event_ids.is_empty() {
164 return Ok(());
165 }
166 let conn = super::get_write_connection_guard_static()?;
167 let placeholders = std::iter::repeat("?")
168 .take(wrap_event_ids.len())
169 .collect::<Vec<_>>()
170 .join(",");
171 let sql = format!(
172 "DELETE FROM nip17_wrap_keys WHERE wrap_event_id IN ({})",
173 placeholders,
174 );
175 let hex_strings: Vec<String> = wrap_event_ids.iter().map(|id| id.to_hex()).collect();
176 let params_dyn: Vec<&dyn rusqlite::ToSql> = hex_strings
177 .iter()
178 .map(|s| s as &dyn rusqlite::ToSql)
179 .collect();
180 conn.execute(&sql, params_dyn.as_slice())
181 .map_err(|e| format!("Failed to purge wrap keys: {}", e))?;
182 Ok(())
183}
184
185pub struct ResendPayload {
199 pub wrap_event: Event,
201 pub rumor: UnsignedEvent,
203 pub secret: SecretKey,
207 pub recipient_pubkey: PublicKey,
208 pub relay_urls: Vec<String>,
210 pub rumor_id: EventId,
211}
212
213pub fn stash_resend_payload(
216 wrap_event_id: &EventId,
217 pending_id: &str,
218 wrap_event: &Event,
219 rumor: &UnsignedEvent,
220) -> Result<(), String> {
221 let conn = super::get_write_connection_guard_static()?;
222 conn.execute(
223 "UPDATE nip17_wrap_keys SET wrap_json = ?1, rumor_json = ?2, pending_id = ?3
224 WHERE wrap_event_id = ?4",
225 params![
226 wrap_event.as_json(),
227 rumor.as_json(),
228 pending_id,
229 wrap_event_id.to_hex(),
230 ],
231 )
232 .map_err(|e| format!("Failed to stash resend payload: {}", e))?;
233 Ok(())
234}
235
236pub fn get_resend_payload_by_pending(pending_id: &str) -> Result<Option<ResendPayload>, String> {
240 let conn = super::get_db_connection_guard_static()?;
241 let row = conn
243 .query_row(
244 "SELECT rumor_id, recipient_pubkey, secret, relay_urls, wrap_json, rumor_json
245 FROM nip17_wrap_keys
246 WHERE pending_id = ?1 AND role = 0
247 AND wrap_json IS NOT NULL AND rumor_json IS NOT NULL
248 LIMIT 1",
249 params![pending_id],
250 |row| {
251 let rumor_hex: String = row.get(0)?;
252 let recipient_hex: String = row.get(1)?;
253 let secret_blob: Vec<u8> = row.get(2)?;
254 let relays_json: String = row.get(3)?;
255 let wrap_json: String = row.get(4)?;
256 let rumor_json: String = row.get(5)?;
257 Ok((rumor_hex, recipient_hex, secret_blob, relays_json, wrap_json, rumor_json))
258 },
259 )
260 .optional()
261 .map_err(|e| format!("Failed to query resend payload: {}", e))?;
262
263 let Some((rumor_hex, recipient_hex, secret_blob, relays_json, wrap_json, rumor_json)) = row
264 else {
265 return Ok(None);
266 };
267 let rumor_id = EventId::from_hex(&rumor_hex).map_err(|e| format!("Bad rumor id: {}", e))?;
268 let recipient_pubkey =
269 PublicKey::from_hex(&recipient_hex).map_err(|e| format!("Bad pubkey: {}", e))?;
270 let secret = SecretKey::from_slice(&secret_blob).map_err(|e| format!("Bad secret: {}", e))?;
271 let relay_urls: Vec<String> =
272 serde_json::from_str(&relays_json).map_err(|e| format!("Bad relay urls: {}", e))?;
273 let wrap_event = Event::from_json(&wrap_json).map_err(|e| format!("Bad wrap json: {}", e))?;
274 let rumor = UnsignedEvent::from_json(&rumor_json).map_err(|e| format!("Bad rumor json: {}", e))?;
275 Ok(Some(ResendPayload {
276 wrap_event,
277 rumor,
278 secret,
279 recipient_pubkey,
280 relay_urls,
281 rumor_id,
282 }))
283}
284
285pub fn clear_resend_payload(rumor_id: &EventId) -> Result<(), String> {
289 let conn = super::get_write_connection_guard_static()?;
290 conn.execute(
291 "UPDATE nip17_wrap_keys SET wrap_json = NULL, rumor_json = NULL WHERE rumor_id = ?1",
292 params![rumor_id.to_hex()],
293 )
294 .map_err(|e| format!("Failed to clear resend payload: {}", e))?;
295 Ok(())
296}
297
298pub fn prune_stale_resend_payloads(max_age_secs: i64) -> Result<usize, String> {
302 let conn = super::get_write_connection_guard_static()?;
303 let now = std::time::SystemTime::now()
304 .duration_since(std::time::UNIX_EPOCH)
305 .unwrap()
306 .as_secs() as i64;
307 let cutoff = now - max_age_secs;
308 let n = conn
309 .execute(
310 "UPDATE nip17_wrap_keys SET wrap_json = NULL, rumor_json = NULL
311 WHERE wrap_json IS NOT NULL AND created_at < ?1",
312 params![cutoff],
313 )
314 .map_err(|e| format!("Failed to prune resend payloads: {}", e))?;
315 Ok(n)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 static TEST_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(81000);
323
324 fn make_test_npub(n: u32) -> String {
325 const BECH32: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
326 let mut payload = vec![b'q'; 58];
327 let mut x = n as u64;
328 let mut i = 58;
329 while x > 0 && i > 0 {
330 i -= 1;
331 payload[i] = BECH32[(x as usize) % 32];
332 x /= 32;
333 }
334 format!("npub1{}", std::str::from_utf8(&payload).unwrap())
335 }
336
337 fn init_test_db() -> (tempfile::TempDir, std::sync::MutexGuard<'static, ()>) {
338 let guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
339 crate::db::close_database();
340 crate::db::clear_id_caches();
341 let tmp = tempfile::tempdir().unwrap();
342 let n = TEST_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
343 let account = make_test_npub(n);
344 std::fs::create_dir_all(tmp.path().join(&account)).unwrap();
345 crate::db::set_app_data_dir(tmp.path().to_path_buf());
346 crate::db::set_current_account(account.clone()).unwrap();
347 crate::db::init_database(&account).unwrap();
348 (tmp, guard)
349 }
350
351 fn sample() -> (Event, UnsignedEvent, Keys, PublicKey) {
352 let ephemeral = Keys::generate();
353 let sender = Keys::generate();
354 let recipient = Keys::generate();
355 let wrap = EventBuilder::text_note("wrap").finalize(&ephemeral).unwrap();
357 let rumor = EventBuilder::new(Kind::PrivateDirectMessage, "hello")
358 .tag(Tag::public_key(recipient.public_key()))
359 .finalize_unsigned_with_id(sender.public_key());
360 (wrap, rumor, ephemeral, recipient.public_key())
361 }
362
363 #[test]
364 fn resend_payload_round_trips_and_clears() {
365 let (_tmp, _guard) = init_test_db();
366 let (wrap, rumor, ephemeral, recipient) = sample();
367 let rumor_id = rumor.id.unwrap();
368 let relays = vec!["wss://relay.one".to_string(), "wss://relay.two".to_string()];
369
370 store_wrap_key(&wrap.id, &rumor_id, &recipient, WrapRole::Recipient,
371 ephemeral.secret_key(), &relays).unwrap();
372 stash_resend_payload(&wrap.id, "pending-42", &wrap, &rumor).unwrap();
373
374 let got = get_resend_payload_by_pending("pending-42").unwrap().expect("payload retained");
375 assert_eq!(got.wrap_event.id, wrap.id);
377 assert_eq!(got.rumor.id, Some(rumor_id));
378 assert_eq!(got.rumor_id, rumor_id);
379 assert_eq!(got.recipient_pubkey, recipient);
380 assert_eq!(got.relay_urls, relays);
381
382 clear_resend_payload(&rumor_id).unwrap();
384 assert!(get_resend_payload_by_pending("pending-42").unwrap().is_none());
385 assert!(has_wrap_keys_for_rumor(&rumor_id).unwrap(), "key row survives NIP-09");
386 }
387
388 #[test]
389 fn no_payload_for_unknown_pending() {
390 let (_tmp, _guard) = init_test_db();
391 assert!(get_resend_payload_by_pending("pending-none").unwrap().is_none());
392 }
393
394 #[test]
395 fn prune_reaps_stale_bodies_but_keeps_keys() {
396 let (_tmp, _guard) = init_test_db();
397 let (wrap, rumor, ephemeral, recipient) = sample();
398 let rumor_id = rumor.id.unwrap();
399 let relays = vec!["wss://relay.one".to_string()];
400 store_wrap_key(&wrap.id, &rumor_id, &recipient, WrapRole::Recipient,
401 ephemeral.secret_key(), &relays).unwrap();
402 stash_resend_payload(&wrap.id, "pending-stale", &wrap, &rumor).unwrap();
403 assert!(get_resend_payload_by_pending("pending-stale").unwrap().is_some());
404
405 assert_eq!(prune_stale_resend_payloads(-100).unwrap(), 1);
407 assert!(get_resend_payload_by_pending("pending-stale").unwrap().is_none());
408 assert!(has_wrap_keys_for_rumor(&rumor_id).unwrap(), "key row survives prune");
409 }
410}