cdk_redb/wallet/
mod.rs

1//! Redb Wallet
2
3use std::cmp::Ordering;
4use std::collections::HashMap;
5use std::path::Path;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use cdk_common::common::ProofInfo;
11use cdk_common::database::WalletDatabase;
12use cdk_common::mint_url::MintUrl;
13use cdk_common::util::unix_time;
14use cdk_common::wallet::{self, MintQuote};
15use cdk_common::{
16    database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
17};
18use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
19use tracing::instrument;
20
21use super::error::Error;
22use crate::migrations::migrate_00_to_01;
23use crate::wallet::migrations::migrate_01_to_02;
24
25mod migrations;
26
27// <Mint_url, Info>
28const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
29// <Mint_Url, Keyset_id>
30const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
31    MultimapTableDefinition::new("mint_keysets");
32// <Keyset_id, KeysetInfo>
33const KEYSETS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("keysets");
34// <Quote_id, quote>
35const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
36// <Quote_id, quote>
37const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
38const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_keys");
39// <Y, Proof Info>
40const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
41const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
42const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
43const NOSTR_LAST_CHECKED: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
44
45const DATABASE_VERSION: u32 = 2;
46
47/// Wallet Redb Database
48#[derive(Debug, Clone)]
49pub struct WalletRedbDatabase {
50    db: Arc<Database>,
51}
52
53impl WalletRedbDatabase {
54    /// Create new [`WalletRedbDatabase`]
55    pub fn new(path: &Path) -> Result<Self, Error> {
56        {
57            let db = Arc::new(Database::create(path)?);
58
59            let db_version: Option<String>;
60            {
61                // Check database version
62                let read_txn = db.begin_read()?;
63                let table = read_txn.open_table(CONFIG_TABLE);
64
65                db_version = match table {
66                    Ok(table) => table.get("db_version")?.map(|v| v.value().to_string()),
67                    Err(_) => None,
68                };
69            }
70
71            match db_version {
72                Some(db_version) => {
73                    let mut current_file_version = u32::from_str(&db_version)?;
74                    tracing::info!("Current file version {}", current_file_version);
75
76                    match current_file_version.cmp(&DATABASE_VERSION) {
77                        Ordering::Less => {
78                            tracing::info!(
79                                "Database needs to be upgraded at {} current is {}",
80                                current_file_version,
81                                DATABASE_VERSION
82                            );
83                            if current_file_version == 0 {
84                                current_file_version = migrate_00_to_01(Arc::clone(&db))?;
85                            }
86
87                            if current_file_version == 1 {
88                                current_file_version = migrate_01_to_02(Arc::clone(&db))?;
89                            }
90
91                            if current_file_version != DATABASE_VERSION {
92                                tracing::warn!(
93                                    "Database upgrade did not complete at {} current is {}",
94                                    current_file_version,
95                                    DATABASE_VERSION
96                                );
97                                return Err(Error::UnknownDatabaseVersion);
98                            }
99
100                            let write_txn = db.begin_write()?;
101                            {
102                                let mut table = write_txn.open_table(CONFIG_TABLE)?;
103
104                                table
105                                    .insert("db_version", DATABASE_VERSION.to_string().as_str())?;
106                            }
107
108                            write_txn.commit()?;
109                        }
110                        Ordering::Equal => {
111                            tracing::info!("Database is at current version {}", DATABASE_VERSION);
112                        }
113                        Ordering::Greater => {
114                            tracing::warn!(
115                                "Database upgrade did not complete at {} current is {}",
116                                current_file_version,
117                                DATABASE_VERSION
118                            );
119                            return Err(Error::UnknownDatabaseVersion);
120                        }
121                    }
122                }
123                None => {
124                    let write_txn = db.begin_write()?;
125                    {
126                        let mut table = write_txn.open_table(CONFIG_TABLE)?;
127                        // Open all tables to init a new db
128                        let _ = write_txn.open_table(MINTS_TABLE)?;
129                        let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?;
130                        let _ = write_txn.open_table(KEYSETS_TABLE)?;
131                        let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
132                        let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
133                        let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
134                        let _ = write_txn.open_table(PROOFS_TABLE)?;
135                        let _ = write_txn.open_table(KEYSET_COUNTER)?;
136                        let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
137                        table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
138                    }
139
140                    write_txn.commit()?;
141                }
142            }
143            drop(db);
144        }
145
146        let db = Database::create(path)?;
147
148        Ok(Self { db: Arc::new(db) })
149    }
150
151    async fn update_proof_states(
152        &self,
153        ys: Vec<PublicKey>,
154        state: State,
155    ) -> Result<(), database::Error> {
156        let read_txn = self.db.begin_read().map_err(Error::from)?;
157        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
158
159        let write_txn = self.db.begin_write().map_err(Error::from)?;
160
161        for y in ys {
162            let y_slice = y.to_bytes();
163            let proof = table
164                .get(y_slice.as_slice())
165                .map_err(Error::from)?
166                .ok_or(Error::UnknownY)?;
167
168            let mut proof_info =
169                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
170
171            proof_info.state = state;
172
173            {
174                let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
175                table
176                    .insert(
177                        y_slice.as_slice(),
178                        serde_json::to_string(&proof_info)
179                            .map_err(Error::from)?
180                            .as_str(),
181                    )
182                    .map_err(Error::from)?;
183            }
184        }
185
186        write_txn.commit().map_err(Error::from)?;
187
188        Ok(())
189    }
190}
191
192#[async_trait]
193impl WalletDatabase for WalletRedbDatabase {
194    type Err = database::Error;
195
196    #[instrument(skip(self))]
197    async fn add_mint(
198        &self,
199        mint_url: MintUrl,
200        mint_info: Option<MintInfo>,
201    ) -> Result<(), Self::Err> {
202        let write_txn = self.db.begin_write().map_err(Error::from)?;
203
204        {
205            let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
206            table
207                .insert(
208                    mint_url.to_string().as_str(),
209                    serde_json::to_string(&mint_info)
210                        .map_err(Error::from)?
211                        .as_str(),
212                )
213                .map_err(Error::from)?;
214        }
215        write_txn.commit().map_err(Error::from)?;
216
217        Ok(())
218    }
219
220    #[instrument(skip(self))]
221    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
222        let write_txn = self.db.begin_write().map_err(Error::from)?;
223
224        {
225            let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
226            table
227                .remove(mint_url.to_string().as_str())
228                .map_err(Error::from)?;
229        }
230        write_txn.commit().map_err(Error::from)?;
231
232        Ok(())
233    }
234
235    #[instrument(skip(self))]
236    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
237        let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
238        let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
239
240        if let Some(mint_info) = table
241            .get(mint_url.to_string().as_str())
242            .map_err(Error::from)?
243        {
244            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
245        }
246
247        Ok(None)
248    }
249
250    #[instrument(skip(self))]
251    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
252        let read_txn = self.db.begin_read().map_err(Error::from)?;
253        let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
254        let mints = table
255            .iter()
256            .map_err(Error::from)?
257            .flatten()
258            .map(|(mint, mint_info)| {
259                (
260                    MintUrl::from_str(mint.value()).unwrap(),
261                    serde_json::from_str(mint_info.value()).ok(),
262                )
263            })
264            .collect();
265
266        Ok(mints)
267    }
268
269    #[instrument(skip(self))]
270    async fn update_mint_url(
271        &self,
272        old_mint_url: MintUrl,
273        new_mint_url: MintUrl,
274    ) -> Result<(), Self::Err> {
275        // Update proofs table
276        {
277            let proofs = self
278                .get_proofs(Some(old_mint_url.clone()), None, None, None)
279                .await
280                .map_err(Error::from)?;
281
282            // Proofs with new url
283            let updated_proofs: Vec<ProofInfo> = proofs
284                .clone()
285                .into_iter()
286                .map(|mut p| {
287                    p.mint_url = new_mint_url.clone();
288                    p
289                })
290                .collect();
291
292            if !updated_proofs.is_empty() {
293                self.update_proofs(updated_proofs, vec![]).await?;
294            }
295        }
296
297        // Update mint quotes
298        {
299            let quotes = self.get_mint_quotes().await?;
300
301            let unix_time = unix_time();
302
303            let quotes: Vec<MintQuote> = quotes
304                .into_iter()
305                .filter_map(|mut q| {
306                    if q.expiry < unix_time {
307                        q.mint_url = new_mint_url.clone();
308                        Some(q)
309                    } else {
310                        None
311                    }
312                })
313                .collect();
314
315            for quote in quotes {
316                self.add_mint_quote(quote).await?;
317            }
318        }
319
320        Ok(())
321    }
322
323    #[instrument(skip(self))]
324    async fn add_mint_keysets(
325        &self,
326        mint_url: MintUrl,
327        keysets: Vec<KeySetInfo>,
328    ) -> Result<(), Self::Err> {
329        let write_txn = self.db.begin_write().map_err(Error::from)?;
330
331        {
332            let mut table = write_txn
333                .open_multimap_table(MINT_KEYSETS_TABLE)
334                .map_err(Error::from)?;
335            let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
336
337            for keyset in keysets {
338                table
339                    .insert(
340                        mint_url.to_string().as_str(),
341                        keyset.id.to_bytes().as_slice(),
342                    )
343                    .map_err(Error::from)?;
344
345                keysets_table
346                    .insert(
347                        keyset.id.to_bytes().as_slice(),
348                        serde_json::to_string(&keyset)
349                            .map_err(Error::from)?
350                            .as_str(),
351                    )
352                    .map_err(Error::from)?;
353            }
354        }
355        write_txn.commit().map_err(Error::from)?;
356
357        Ok(())
358    }
359
360    #[instrument(skip(self))]
361    async fn get_mint_keysets(
362        &self,
363        mint_url: MintUrl,
364    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
365        let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
366        let table = read_txn
367            .open_multimap_table(MINT_KEYSETS_TABLE)
368            .map_err(Error::from)?;
369
370        let keyset_ids = table
371            .get(mint_url.to_string().as_str())
372            .map_err(Error::from)?
373            .flatten()
374            .map(|k| Id::from_bytes(k.value()))
375            .collect::<Result<Vec<_>, _>>()?;
376
377        let mut keysets = vec![];
378
379        let keysets_t = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
380
381        for keyset_id in keyset_ids {
382            if let Some(keyset) = keysets_t
383                .get(keyset_id.to_bytes().as_slice())
384                .map_err(Error::from)?
385            {
386                let keyset = serde_json::from_str(keyset.value()).map_err(Error::from)?;
387
388                keysets.push(keyset);
389            }
390        }
391
392        match keysets.is_empty() {
393            true => Ok(None),
394            false => Ok(Some(keysets)),
395        }
396    }
397
398    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
399    async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err> {
400        let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
401        let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
402
403        match table
404            .get(keyset_id.to_bytes().as_slice())
405            .map_err(Error::from)?
406        {
407            Some(keyset) => {
408                let keyset: KeySetInfo =
409                    serde_json::from_str(keyset.value()).map_err(Error::from)?;
410
411                Ok(Some(keyset))
412            }
413            None => Ok(None),
414        }
415    }
416
417    #[instrument(skip_all)]
418    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
419        let write_txn = self.db.begin_write().map_err(Error::from)?;
420
421        {
422            let mut table = write_txn
423                .open_table(MINT_QUOTES_TABLE)
424                .map_err(Error::from)?;
425            table
426                .insert(
427                    quote.id.as_str(),
428                    serde_json::to_string(&quote).map_err(Error::from)?.as_str(),
429                )
430                .map_err(Error::from)?;
431        }
432
433        write_txn.commit().map_err(Error::from)?;
434
435        Ok(())
436    }
437
438    #[instrument(skip_all)]
439    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
440        let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
441        let table = read_txn
442            .open_table(MINT_QUOTES_TABLE)
443            .map_err(Error::from)?;
444
445        if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? {
446            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
447        }
448
449        Ok(None)
450    }
451
452    #[instrument(skip_all)]
453    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
454        let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
455        let table = read_txn
456            .open_table(MINT_QUOTES_TABLE)
457            .map_err(Error::from)?;
458
459        Ok(table
460            .iter()
461            .map_err(Error::from)?
462            .flatten()
463            .flat_map(|(_id, quote)| serde_json::from_str(quote.value()))
464            .collect())
465    }
466
467    #[instrument(skip_all)]
468    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
469        let write_txn = self.db.begin_write().map_err(Error::from)?;
470
471        {
472            let mut table = write_txn
473                .open_table(MINT_QUOTES_TABLE)
474                .map_err(Error::from)?;
475            table.remove(quote_id).map_err(Error::from)?;
476        }
477
478        write_txn.commit().map_err(Error::from)?;
479
480        Ok(())
481    }
482
483    #[instrument(skip_all)]
484    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
485        let write_txn = self.db.begin_write().map_err(Error::from)?;
486
487        {
488            let mut table = write_txn
489                .open_table(MELT_QUOTES_TABLE)
490                .map_err(Error::from)?;
491            table
492                .insert(
493                    quote.id.as_str(),
494                    serde_json::to_string(&quote).map_err(Error::from)?.as_str(),
495                )
496                .map_err(Error::from)?;
497        }
498
499        write_txn.commit().map_err(Error::from)?;
500
501        Ok(())
502    }
503
504    #[instrument(skip_all)]
505    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
506        let read_txn = self.db.begin_read().map_err(Error::from)?;
507        let table = read_txn
508            .open_table(MELT_QUOTES_TABLE)
509            .map_err(Error::from)?;
510
511        if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? {
512            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
513        }
514
515        Ok(None)
516    }
517
518    #[instrument(skip_all)]
519    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
520        let write_txn = self.db.begin_write().map_err(Error::from)?;
521
522        {
523            let mut table = write_txn
524                .open_table(MELT_QUOTES_TABLE)
525                .map_err(Error::from)?;
526            table.remove(quote_id).map_err(Error::from)?;
527        }
528
529        write_txn.commit().map_err(Error::from)?;
530
531        Ok(())
532    }
533
534    #[instrument(skip_all)]
535    async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
536        let write_txn = self.db.begin_write().map_err(Error::from)?;
537
538        {
539            let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
540            table
541                .insert(
542                    Id::from(&keys).to_string().as_str(),
543                    serde_json::to_string(&keys).map_err(Error::from)?.as_str(),
544                )
545                .map_err(Error::from)?;
546        }
547
548        write_txn.commit().map_err(Error::from)?;
549
550        Ok(())
551    }
552
553    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
554    async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, Self::Err> {
555        let read_txn = self.db.begin_read().map_err(Error::from)?;
556        let table = read_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
557
558        if let Some(mint_info) = table
559            .get(keyset_id.to_string().as_str())
560            .map_err(Error::from)?
561        {
562            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
563        }
564
565        Ok(None)
566    }
567
568    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
569    async fn remove_keys(&self, keyset_id: &Id) -> Result<(), Self::Err> {
570        let write_txn = self.db.begin_write().map_err(Error::from)?;
571
572        {
573            let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
574
575            table
576                .remove(keyset_id.to_string().as_str())
577                .map_err(Error::from)?;
578        }
579
580        write_txn.commit().map_err(Error::from)?;
581
582        Ok(())
583    }
584
585    #[instrument(skip(self, added, deleted_ys))]
586    async fn update_proofs(
587        &self,
588        added: Vec<ProofInfo>,
589        deleted_ys: Vec<PublicKey>,
590    ) -> Result<(), Self::Err> {
591        let write_txn = self.db.begin_write().map_err(Error::from)?;
592
593        {
594            let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
595
596            for proof_info in added.iter() {
597                table
598                    .insert(
599                        proof_info.y.to_bytes().as_slice(),
600                        serde_json::to_string(&proof_info)
601                            .map_err(Error::from)?
602                            .as_str(),
603                    )
604                    .map_err(Error::from)?;
605            }
606
607            for y in deleted_ys.iter() {
608                table.remove(y.to_bytes().as_slice()).map_err(Error::from)?;
609            }
610        }
611        write_txn.commit().map_err(Error::from)?;
612
613        Ok(())
614    }
615
616    #[instrument(skip(self, ys))]
617    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
618        self.update_proof_states(ys, State::Pending).await
619    }
620
621    #[instrument(skip(self, ys))]
622    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
623        self.update_proof_states(ys, State::Reserved).await
624    }
625
626    #[instrument(skip(self, ys))]
627    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
628        self.update_proof_states(ys, State::Unspent).await
629    }
630
631    #[instrument(skip_all)]
632    async fn get_proofs(
633        &self,
634        mint_url: Option<MintUrl>,
635        unit: Option<CurrencyUnit>,
636        state: Option<Vec<State>>,
637        spending_conditions: Option<Vec<SpendingConditions>>,
638    ) -> Result<Vec<ProofInfo>, Self::Err> {
639        let read_txn = self.db.begin_read().map_err(Error::from)?;
640
641        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
642
643        let proofs: Vec<ProofInfo> = table
644            .iter()
645            .map_err(Error::from)?
646            .flatten()
647            .filter_map(|(_k, v)| {
648                let mut proof = None;
649
650                if let Ok(proof_info) = serde_json::from_str::<ProofInfo>(v.value()) {
651                    if proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions)
652                    {
653                        proof = Some(proof_info)
654                    }
655                }
656
657                proof
658            })
659            .collect();
660
661        Ok(proofs)
662    }
663
664    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
665    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
666        let write_txn = self.db.begin_write().map_err(Error::from)?;
667
668        let current_counter;
669        {
670            let table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
671            let counter = table
672                .get(keyset_id.to_string().as_str())
673                .map_err(Error::from)?;
674
675            current_counter = match counter {
676                Some(c) => c.value(),
677                None => 0,
678            };
679        }
680
681        {
682            let mut table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
683            let new_counter = current_counter + count;
684
685            table
686                .insert(keyset_id.to_string().as_str(), new_counter)
687                .map_err(Error::from)?;
688        }
689        write_txn.commit().map_err(Error::from)?;
690
691        Ok(())
692    }
693
694    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
695    async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
696        let read_txn = self.db.begin_read().map_err(Error::from)?;
697        let table = read_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
698
699        let counter = table
700            .get(keyset_id.to_string().as_str())
701            .map_err(Error::from)?;
702
703        Ok(counter.map(|c| c.value()))
704    }
705
706    #[instrument(skip_all)]
707    async fn get_nostr_last_checked(
708        &self,
709        verifying_key: &PublicKey,
710    ) -> Result<Option<u32>, Self::Err> {
711        let read_txn = self.db.begin_read().map_err(Error::from)?;
712        let table = read_txn
713            .open_table(NOSTR_LAST_CHECKED)
714            .map_err(Error::from)?;
715
716        let last_checked = table
717            .get(verifying_key.to_string().as_str())
718            .map_err(Error::from)?;
719
720        Ok(last_checked.map(|c| c.value()))
721    }
722
723    #[instrument(skip(self, verifying_key))]
724    async fn add_nostr_last_checked(
725        &self,
726        verifying_key: PublicKey,
727        last_checked: u32,
728    ) -> Result<(), Self::Err> {
729        let write_txn = self.db.begin_write().map_err(Error::from)?;
730        {
731            let mut table = write_txn
732                .open_table(NOSTR_LAST_CHECKED)
733                .map_err(Error::from)?;
734
735            table
736                .insert(verifying_key.to_string().as_str(), last_checked)
737                .map_err(Error::from)?;
738        }
739        write_txn.commit().map_err(Error::from)?;
740
741        Ok(())
742    }
743}