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