1use 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::bitcoin::bip32::DerivationPath;
11use cdk_common::database::{validate_kvstore_params, WalletDatabase};
12use cdk_common::mint_url::MintUrl;
13use cdk_common::nut00::KnownMethod;
14use cdk_common::util::unix_time;
15use cdk_common::wallet::{
16 self, MintQuote, ProofInfo, Transaction, TransactionDirection, TransactionId,
17};
18use cdk_common::{
19 database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod,
20 PublicKey, SpendingConditions, State,
21};
22use redb::{Database, MultimapTableDefinition, ReadableDatabase, ReadableTable, TableDefinition};
23use tracing::instrument;
24
25use crate::error::Error;
26use crate::migrations::migrate_00_to_01;
27use crate::wallet::migrations::{
28 migrate_01_to_02, migrate_02_to_03, migrate_03_to_04, migrate_04_to_05, migrate_05_to_06,
29};
30
31mod migrations;
32
33const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
35const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
37 MultimapTableDefinition::new("mint_keysets");
38const KEYSETS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("keysets");
40const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
42const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
44const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_keys");
45const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
47const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
48const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
49const DERIVATION_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("derivation_counter");
50const TRANSACTIONS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("transactions");
52const SAGAS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("wallet_sagas");
54
55const P2PK_SIGNING_KEYS_TABLE: TableDefinition<&[u8], &str> =
57 TableDefinition::new("p2pk_signing_keys");
58
59const KEYSET_U32_MAPPING: TableDefinition<u32, &str> = TableDefinition::new("keyset_u32_mapping");
60const KV_STORE_TABLE: TableDefinition<(&str, &str, &str), &[u8]> = TableDefinition::new("kv_store");
62
63const DATABASE_VERSION: u32 = 6;
64
65#[derive(Debug, Clone)]
67pub struct WalletRedbDatabase {
68 db: Arc<Database>,
69}
70
71impl WalletRedbDatabase {
72 pub fn new(path: &Path) -> Result<Self, Error> {
74 {
75 if let Some(parent) = path.parent() {
77 if !parent.exists() {
78 return Err(Error::Io(std::io::Error::new(
79 std::io::ErrorKind::NotFound,
80 format!("Parent directory does not exist: {parent:?}"),
81 )));
82 }
83 }
84
85 let db = Arc::new(Database::create(path)?);
86
87 let db_version: Option<String>;
88 {
89 let read_txn = db.begin_read()?;
91 let table = read_txn.open_table(CONFIG_TABLE);
92
93 db_version = match table {
94 Ok(table) => table.get("db_version")?.map(|v| v.value().to_string()),
95 Err(_) => None,
96 };
97 }
98
99 match db_version {
100 Some(db_version) => {
101 let mut current_file_version = u32::from_str(&db_version)?;
102 tracing::info!("Current file version {}", current_file_version);
103
104 match current_file_version.cmp(&DATABASE_VERSION) {
105 Ordering::Less => {
106 tracing::info!(
107 "Database needs to be upgraded at {} current is {}",
108 current_file_version,
109 DATABASE_VERSION
110 );
111 if current_file_version == 0 {
112 current_file_version = migrate_00_to_01(Arc::clone(&db))?;
113 }
114
115 if current_file_version == 1 {
116 current_file_version = migrate_01_to_02(Arc::clone(&db))?;
117 }
118
119 if current_file_version == 2 {
120 current_file_version = migrate_02_to_03(Arc::clone(&db))?;
121 }
122
123 if current_file_version == 3 {
124 current_file_version = migrate_03_to_04(Arc::clone(&db))?;
125 }
126
127 if current_file_version == 4 {
128 current_file_version = migrate_04_to_05(Arc::clone(&db))?;
129 }
130
131 if current_file_version == 5 {
132 current_file_version = migrate_05_to_06(Arc::clone(&db))?;
133 }
134
135 if current_file_version != DATABASE_VERSION {
136 tracing::warn!(
137 "Database upgrade did not complete at {} current is {}",
138 current_file_version,
139 DATABASE_VERSION
140 );
141 return Err(Error::UnknownDatabaseVersion);
142 }
143
144 let write_txn = db.begin_write()?;
145 {
146 let mut table = write_txn.open_table(CONFIG_TABLE)?;
147
148 table
149 .insert("db_version", DATABASE_VERSION.to_string().as_str())?;
150 }
151
152 write_txn.commit()?;
153 }
154 Ordering::Equal => {
155 tracing::info!("Database is at current version {}", DATABASE_VERSION);
156 }
157 Ordering::Greater => {
158 tracing::warn!(
159 "Database upgrade did not complete at {} current is {}",
160 current_file_version,
161 DATABASE_VERSION
162 );
163 return Err(Error::UnknownDatabaseVersion);
164 }
165 }
166 }
167 None => {
168 let write_txn = db.begin_write()?;
169 {
170 let mut table = write_txn.open_table(CONFIG_TABLE)?;
171 let _ = write_txn.open_table(MINTS_TABLE)?;
173 let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?;
174 let _ = write_txn.open_table(KEYSETS_TABLE)?;
175 let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
176 let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
177 let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
178 let _ = write_txn.open_table(PROOFS_TABLE)?;
179 let _ = write_txn.open_table(KEYSET_COUNTER)?;
180 let _ = write_txn.open_table(TRANSACTIONS_TABLE)?;
181 let _ = write_txn.open_table(KEYSET_U32_MAPPING)?;
182 let _ = write_txn.open_table(KV_STORE_TABLE)?;
183 let _ = write_txn.open_table(P2PK_SIGNING_KEYS_TABLE)?;
184 table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
185 }
186
187 write_txn.commit()?;
188 }
189 }
190 drop(db);
191 }
192
193 if let Some(parent) = path.parent() {
195 if !parent.exists() {
196 return Err(Error::Io(std::io::Error::new(
197 std::io::ErrorKind::NotFound,
198 format!("Parent directory does not exist: {parent:?}"),
199 )));
200 }
201 }
202
203 let db = Database::create(path)?;
204
205 Ok(Self { db: Arc::new(db) })
206 }
207}
208
209#[async_trait]
210impl WalletDatabase<database::Error> for WalletRedbDatabase {
211 #[instrument(skip(self))]
212 async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, database::Error> {
213 let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
214 let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
215
216 if let Some(mint_info) = table
217 .get(mint_url.to_string().as_str())
218 .map_err(Error::from)?
219 {
220 return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
221 }
222
223 Ok(None)
224 }
225
226 #[instrument(skip(self))]
227 async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, database::Error> {
228 let read_txn = self.db.begin_read().map_err(Error::from)?;
229 let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
230 let mints = table
231 .iter()
232 .map_err(Error::from)?
233 .flatten()
234 .filter_map(|(mint, mint_info)| {
235 MintUrl::from_str(mint.value())
236 .ok()
237 .map(|url| (url, serde_json::from_str(mint_info.value()).ok()))
238 })
239 .collect();
240
241 Ok(mints)
242 }
243
244 #[instrument(skip(self))]
245 async fn get_mint_keysets(
246 &self,
247 mint_url: MintUrl,
248 ) -> Result<Option<Vec<KeySetInfo>>, database::Error> {
249 let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
250 let table = read_txn
251 .open_multimap_table(MINT_KEYSETS_TABLE)
252 .map_err(Error::from)?;
253
254 let keyset_ids = table
255 .get(mint_url.to_string().as_str())
256 .map_err(Error::from)?
257 .flatten()
258 .map(|k| Id::from_bytes(k.value()))
259 .collect::<Result<Vec<_>, _>>()?;
260
261 let mut keysets = vec![];
262
263 let keysets_t = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
264
265 for keyset_id in keyset_ids {
266 if let Some(keyset) = keysets_t
267 .get(keyset_id.to_bytes().as_slice())
268 .map_err(Error::from)?
269 {
270 let keyset = serde_json::from_str(keyset.value()).map_err(Error::from)?;
271
272 keysets.push(keyset);
273 }
274 }
275
276 match keysets.is_empty() {
277 true => Ok(None),
278 false => Ok(Some(keysets)),
279 }
280 }
281
282 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
283 async fn get_keyset_by_id(
284 &self,
285 keyset_id: &Id,
286 ) -> Result<Option<KeySetInfo>, database::Error> {
287 let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
288 let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
289
290 match table
291 .get(keyset_id.to_bytes().as_slice())
292 .map_err(Error::from)?
293 {
294 Some(keyset) => {
295 let keyset: KeySetInfo =
296 serde_json::from_str(keyset.value()).map_err(Error::from)?;
297
298 Ok(Some(keyset))
299 }
300 None => Ok(None),
301 }
302 }
303
304 #[instrument(skip_all)]
305 async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, database::Error> {
306 let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
307 let table = read_txn
308 .open_table(MINT_QUOTES_TABLE)
309 .map_err(Error::from)?;
310
311 if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? {
312 return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
313 }
314
315 Ok(None)
316 }
317
318 #[instrument(skip_all)]
319 async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
320 let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
321 let table = read_txn
322 .open_table(MINT_QUOTES_TABLE)
323 .map_err(Error::from)?;
324
325 Ok(table
326 .iter()
327 .map_err(Error::from)?
328 .flatten()
329 .flat_map(|(_id, quote)| serde_json::from_str(quote.value()))
330 .collect())
331 }
332
333 async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
334 let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
335 let table = read_txn
336 .open_table(MINT_QUOTES_TABLE)
337 .map_err(Error::from)?;
338
339 Ok(table
340 .iter()
341 .map_err(Error::from)?
342 .flatten()
343 .flat_map(|(_id, quote)| serde_json::from_str::<MintQuote>(quote.value()).ok())
344 .filter(|quote| {
345 quote.amount_issued == Amount::ZERO
346 || quote.payment_method == PaymentMethod::Known(KnownMethod::Bolt12)
347 })
348 .collect())
349 }
350
351 #[instrument(skip_all)]
352 async fn get_melt_quote(
353 &self,
354 quote_id: &str,
355 ) -> Result<Option<wallet::MeltQuote>, database::Error> {
356 let read_txn = self.db.begin_read().map_err(Error::from)?;
357 let table = read_txn
358 .open_table(MELT_QUOTES_TABLE)
359 .map_err(Error::from)?;
360
361 if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? {
362 return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
363 }
364
365 Ok(None)
366 }
367
368 #[instrument(skip_all)]
369 async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, database::Error> {
370 let read_txn = self.db.begin_read().map_err(Error::from)?;
371 let table = read_txn
372 .open_table(MELT_QUOTES_TABLE)
373 .map_err(Error::from)?;
374
375 Ok(table
376 .iter()
377 .map_err(Error::from)?
378 .flatten()
379 .flat_map(|(_id, quote)| serde_json::from_str(quote.value()))
380 .collect())
381 }
382
383 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
384 async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, database::Error> {
385 let read_txn = self.db.begin_read().map_err(Error::from)?;
386 let table = read_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
387
388 if let Some(mint_info) = table
389 .get(keyset_id.to_string().as_str())
390 .map_err(Error::from)?
391 {
392 return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
393 }
394
395 Ok(None)
396 }
397
398 #[instrument(skip_all)]
399 async fn get_proofs(
400 &self,
401 mint_url: Option<MintUrl>,
402 unit: Option<CurrencyUnit>,
403 state: Option<Vec<State>>,
404 spending_conditions: Option<Vec<SpendingConditions>>,
405 ) -> Result<Vec<ProofInfo>, database::Error> {
406 let read_txn = self.db.begin_read().map_err(Error::from)?;
407
408 let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
409
410 let proofs: Vec<ProofInfo> = table
411 .iter()
412 .map_err(Error::from)?
413 .flatten()
414 .filter_map(|(_k, v)| {
415 let mut proof = None;
416
417 if let Ok(proof_info) = serde_json::from_str::<ProofInfo>(v.value()) {
418 if proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions)
419 {
420 proof = Some(proof_info)
421 }
422 }
423
424 proof
425 })
426 .collect();
427
428 Ok(proofs)
429 }
430
431 #[instrument(skip(self, ys))]
432 async fn get_proofs_by_ys(
433 &self,
434 ys: Vec<PublicKey>,
435 ) -> Result<Vec<ProofInfo>, database::Error> {
436 if ys.is_empty() {
437 return Ok(Vec::new());
438 }
439
440 let read_txn = self.db.begin_read().map_err(Error::from)?;
441 let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
442
443 let mut proofs = Vec::new();
444
445 for y in ys {
446 if let Some(proof) = table.get(y.to_bytes().as_slice()).map_err(Error::from)? {
447 let proof_info =
448 serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
449 proofs.push(proof_info);
450 }
451 }
452
453 Ok(proofs)
454 }
455
456 async fn get_balance(
457 &self,
458 mint_url: Option<MintUrl>,
459 unit: Option<CurrencyUnit>,
460 state: Option<Vec<State>>,
461 ) -> Result<u64, database::Error> {
462 let proofs = self.get_proofs(mint_url, unit, state, None).await?;
465 Ok(proofs.iter().map(|p| u64::from(p.proof.amount)).sum())
466 }
467
468 #[instrument(skip(self))]
469 async fn get_transaction(
470 &self,
471 transaction_id: TransactionId,
472 ) -> Result<Option<Transaction>, database::Error> {
473 let read_txn = self.db.begin_read().map_err(Error::from)?;
474 let table = read_txn
475 .open_table(TRANSACTIONS_TABLE)
476 .map_err(Error::from)?;
477
478 if let Some(transaction) = table.get(transaction_id.as_slice()).map_err(Error::from)? {
479 return Ok(serde_json::from_str(transaction.value()).map_err(Error::from)?);
480 }
481
482 Ok(None)
483 }
484
485 #[instrument(skip(self))]
486 async fn list_transactions(
487 &self,
488 mint_url: Option<MintUrl>,
489 direction: Option<TransactionDirection>,
490 unit: Option<CurrencyUnit>,
491 ) -> Result<Vec<Transaction>, database::Error> {
492 let read_txn = self.db.begin_read().map_err(Error::from)?;
493
494 let table = read_txn
495 .open_table(TRANSACTIONS_TABLE)
496 .map_err(Error::from)?;
497
498 let transactions: Vec<Transaction> = table
499 .iter()
500 .map_err(Error::from)?
501 .flatten()
502 .filter_map(|(_k, v)| {
503 let mut transaction = None;
504
505 if let Ok(tx) = serde_json::from_str::<Transaction>(v.value()) {
506 if tx.matches_conditions(&mint_url, &direction, &unit) {
507 transaction = Some(tx)
508 }
509 }
510
511 transaction
512 })
513 .collect();
514
515 Ok(transactions)
516 }
517
518 #[instrument(skip(self, added, removed_ys))]
519 async fn update_proofs(
520 &self,
521 added: Vec<ProofInfo>,
522 removed_ys: Vec<PublicKey>,
523 ) -> Result<(), database::Error> {
524 let write_txn = self.db.begin_write().map_err(Error::from)?;
525 {
526 let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
527
528 for mut proof_info in added {
529 if proof_info.derivation_index.is_none() {
530 proof_info.derivation_index = match table
531 .get(proof_info.y.to_bytes().as_slice())
532 .map_err(Error::from)?
533 {
534 Some(stored) => {
535 serde_json::from_str::<ProofInfo>(stored.value())
536 .map_err(Error::from)?
537 .derivation_index
538 }
539 None => None,
540 };
541 }
542
543 table
544 .insert(
545 proof_info.y.to_bytes().as_slice(),
546 serde_json::to_string(&proof_info)
547 .map_err(Error::from)?
548 .as_str(),
549 )
550 .map_err(Error::from)?;
551 }
552
553 for y in removed_ys.iter() {
554 table.remove(y.to_bytes().as_slice()).map_err(Error::from)?;
555 }
556 }
557 write_txn.commit().map_err(Error::from)?;
558 Ok(())
559 }
560
561 async fn update_proofs_state(
562 &self,
563 ys: Vec<PublicKey>,
564 state: State,
565 ) -> Result<(), database::Error> {
566 let write_txn = self.db.begin_write().map_err(Error::from)?;
567 {
568 let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
569
570 for y in ys {
571 let y_slice = y.to_bytes();
572 let proof = table
573 .get(y_slice.as_slice())
574 .map_err(Error::from)?
575 .ok_or(Error::UnknownY)?;
576
577 let mut proof_info =
578 serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
579 drop(proof);
580
581 proof_info.state = state;
582
583 table
584 .insert(
585 y_slice.as_slice(),
586 serde_json::to_string(&proof_info)
587 .map_err(Error::from)?
588 .as_str(),
589 )
590 .map_err(Error::from)?;
591 }
592 }
593 write_txn.commit().map_err(Error::from)?;
594 Ok(())
595 }
596
597 #[instrument(
598 skip(self, transaction),
599 fields(
600 direction = %transaction.direction,
601 amount = %transaction.amount,
602 unit = %transaction.unit,
603 quote_id = ?transaction.quote_id,
604 saga_id = ?transaction.saga_id,
605 )
606 )]
607 async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> {
608 let id = transaction.id();
609 let write_txn = self.db.begin_write().map_err(Error::from)?;
610 {
611 let mut table = write_txn
612 .open_table(TRANSACTIONS_TABLE)
613 .map_err(Error::from)?;
614 table
615 .insert(
616 id.as_slice(),
617 serde_json::to_string(&transaction)
618 .map_err(Error::from)?
619 .as_str(),
620 )
621 .map_err(Error::from)?;
622 }
623 write_txn.commit().map_err(Error::from)?;
624 Ok(())
625 }
626
627 #[instrument(skip(self))]
628 async fn update_mint_url(
629 &self,
630 old_mint_url: MintUrl,
631 new_mint_url: MintUrl,
632 ) -> Result<(), database::Error> {
633 let write_txn = self.db.begin_write().map_err(Error::from)?;
634
635 {
637 let read_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
638 let proofs: Vec<ProofInfo> = read_table
639 .iter()
640 .map_err(Error::from)?
641 .flatten()
642 .filter_map(|(_k, v)| {
643 let proof_info = serde_json::from_str::<ProofInfo>(v.value()).ok()?;
644 if proof_info.mint_url == old_mint_url {
645 Some(proof_info)
646 } else {
647 None
648 }
649 })
650 .collect();
651 drop(read_table);
652
653 if !proofs.is_empty() {
654 let mut write_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
655 for mut proof_info in proofs {
656 proof_info.mint_url = new_mint_url.clone();
657 write_table
658 .insert(
659 proof_info.y.to_bytes().as_slice(),
660 serde_json::to_string(&proof_info)
661 .map_err(Error::from)?
662 .as_str(),
663 )
664 .map_err(Error::from)?;
665 }
666 }
667 }
668
669 {
671 let mut table = write_txn
672 .open_table(MINT_QUOTES_TABLE)
673 .map_err(Error::from)?;
674
675 let unix_time = unix_time();
676
677 let quotes: Vec<MintQuote> = table
678 .iter()
679 .map_err(Error::from)?
680 .flatten()
681 .filter_map(|(_, quote)| {
682 let mut q: MintQuote = serde_json::from_str(quote.value()).ok()?;
683 if q.mint_url == old_mint_url && q.expiry >= unix_time {
684 q.mint_url = new_mint_url.clone();
685 Some(q)
686 } else {
687 None
688 }
689 })
690 .collect();
691
692 for quote in quotes {
693 table
694 .insert(
695 quote.id.as_str(),
696 serde_json::to_string("e).map_err(Error::from)?.as_str(),
697 )
698 .map_err(Error::from)?;
699 }
700 }
701
702 write_txn.commit().map_err(Error::from)?;
703 Ok(())
704 }
705
706 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
707 async fn increment_keyset_counter(
708 &self,
709 keyset_id: &Id,
710 count: u32,
711 ) -> Result<u32, database::Error> {
712 let write_txn = self.db.begin_write().map_err(Error::from)?;
713 let new_counter = {
714 let mut table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
715 let current_counter = table
716 .get(keyset_id.to_string().as_str())
717 .map_err(Error::from)?
718 .map(|x| x.value())
719 .unwrap_or_default();
720
721 let new_counter = current_counter
722 .checked_add(count)
723 .ok_or(database::Error::AmountOverflow)?;
724
725 table
726 .insert(keyset_id.to_string().as_str(), new_counter)
727 .map_err(Error::from)?;
728
729 new_counter
730 };
731 write_txn.commit().map_err(Error::from)?;
732 Ok(new_counter)
733 }
734
735 #[instrument(skip(self))]
736 async fn increment_derivation_counter(
737 &self,
738 namespace: &str,
739 count: u32,
740 ) -> Result<u32, database::Error> {
741 let write_txn = self.db.begin_write().map_err(Error::from)?;
742 let new_counter = {
743 let mut table = write_txn
744 .open_table(DERIVATION_COUNTER)
745 .map_err(Error::from)?;
746 let current_counter = table
747 .get(namespace)
748 .map_err(Error::from)?
749 .map(|value| value.value())
750 .unwrap_or_default();
751 let new_counter = current_counter
752 .checked_add(count)
753 .ok_or(database::Error::AmountOverflow)?;
754
755 table.insert(namespace, new_counter).map_err(Error::from)?;
756 new_counter
757 };
758 write_txn.commit().map_err(Error::from)?;
759
760 Ok(new_counter)
761 }
762
763 #[instrument(skip(self))]
764 async fn add_mint(
765 &self,
766 mint_url: MintUrl,
767 mint_info: Option<MintInfo>,
768 ) -> Result<(), database::Error> {
769 let write_txn = self.db.begin_write().map_err(Error::from)?;
770 {
771 let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
772 table
773 .insert(
774 mint_url.to_string().as_str(),
775 serde_json::to_string(&mint_info)
776 .map_err(Error::from)?
777 .as_str(),
778 )
779 .map_err(Error::from)?;
780 }
781 write_txn.commit().map_err(Error::from)?;
782 Ok(())
783 }
784
785 #[instrument(skip(self))]
786 async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
787 let write_txn = self.db.begin_write().map_err(Error::from)?;
788 {
789 let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
790 table
791 .remove(mint_url.to_string().as_str())
792 .map_err(Error::from)?;
793 }
794 write_txn.commit().map_err(Error::from)?;
795 Ok(())
796 }
797
798 #[instrument(skip(self))]
799 async fn add_mint_keysets(
800 &self,
801 mint_url: MintUrl,
802 keysets: Vec<KeySetInfo>,
803 ) -> Result<(), database::Error> {
804 let write_txn = self.db.begin_write().map_err(Error::from)?;
805 {
806 let mut table = write_txn
807 .open_multimap_table(MINT_KEYSETS_TABLE)
808 .map_err(Error::from)?;
809 let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
810 let mut u32_table = write_txn
811 .open_table(KEYSET_U32_MAPPING)
812 .map_err(Error::from)?;
813
814 let mut existing_u32 = false;
815
816 for keyset in keysets {
817 let existing_keyset = {
819 let existing_keyset = keysets_table
820 .get(keyset.id.to_bytes().as_slice())
821 .map_err(Error::from)?;
822
823 existing_keyset.map(|r| r.value().to_string())
824 };
825
826 let existing = u32_table
827 .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
828 .map_err(Error::from)?;
829
830 match existing {
831 None => existing_u32 = false,
832 Some(id) => {
833 let id = Id::from_str(id.value())?;
834
835 if id == keyset.id {
836 existing_u32 = false;
837 } else {
838 existing_u32 = true;
839 break;
840 }
841 }
842 }
843
844 let keyset = if let Some(existing_keyset) = existing_keyset {
845 let mut existing_keyset: KeySetInfo = serde_json::from_str(&existing_keyset)?;
846
847 existing_keyset.active = keyset.active;
848 existing_keyset.input_fee_ppk = keyset.input_fee_ppk;
849
850 existing_keyset
851 } else {
852 table
853 .insert(
854 mint_url.to_string().as_str(),
855 keyset.id.to_bytes().as_slice(),
856 )
857 .map_err(Error::from)?;
858
859 keyset
860 };
861
862 keysets_table
863 .insert(
864 keyset.id.to_bytes().as_slice(),
865 serde_json::to_string(&keyset)
866 .map_err(Error::from)?
867 .as_str(),
868 )
869 .map_err(Error::from)?;
870 }
871
872 if existing_u32 {
873 tracing::warn!("Keyset already exists for keyset id");
874 return Err(database::Error::Duplicate);
875 }
876 }
877 write_txn.commit().map_err(Error::from)?;
878 Ok(())
879 }
880
881 #[instrument(skip_all)]
882 async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
883 let write_txn = self.db.begin_write().map_err(Error::from)?;
884 {
885 let mut table = write_txn
886 .open_table(MINT_QUOTES_TABLE)
887 .map_err(Error::from)?;
888
889 let existing_quote_json = table
891 .get(quote.id.as_str())
892 .map_err(Error::from)?
893 .map(|v| v.value().to_string());
894
895 let mut quote_to_save = quote.clone();
896
897 if let Some(json) = existing_quote_json {
898 let existing_quote: MintQuote = serde_json::from_str(&json).map_err(Error::from)?;
899
900 if existing_quote.version != quote.version {
901 return Err(database::Error::ConcurrentUpdate);
902 }
903
904 quote_to_save.version = quote.version.wrapping_add(1);
906 }
907
908 table
909 .insert(
910 quote_to_save.id.as_str(),
911 serde_json::to_string("e_to_save)
912 .map_err(Error::from)?
913 .as_str(),
914 )
915 .map_err(Error::from)?;
916 }
917 write_txn.commit().map_err(Error::from)?;
918 Ok(())
919 }
920
921 #[instrument(skip_all)]
922 async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
923 let write_txn = self.db.begin_write().map_err(Error::from)?;
924 {
925 let mut table = write_txn
926 .open_table(MINT_QUOTES_TABLE)
927 .map_err(Error::from)?;
928 table.remove(quote_id).map_err(Error::from)?;
929 }
930 write_txn.commit().map_err(Error::from)?;
931 Ok(())
932 }
933
934 #[instrument(skip_all)]
935 async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
936 let write_txn = self.db.begin_write().map_err(Error::from)?;
937 {
938 let mut table = write_txn
939 .open_table(MELT_QUOTES_TABLE)
940 .map_err(Error::from)?;
941
942 let existing_quote_json = table
944 .get(quote.id.as_str())
945 .map_err(Error::from)?
946 .map(|v| v.value().to_string());
947
948 let mut quote_to_save = quote.clone();
949
950 if let Some(json) = existing_quote_json {
951 let existing_quote: wallet::MeltQuote =
952 serde_json::from_str(&json).map_err(Error::from)?;
953
954 if existing_quote.version != quote.version {
955 return Err(database::Error::ConcurrentUpdate);
956 }
957
958 quote_to_save.version = quote.version.wrapping_add(1);
960 }
961
962 table
963 .insert(
964 quote_to_save.id.as_str(),
965 serde_json::to_string("e_to_save)
966 .map_err(Error::from)?
967 .as_str(),
968 )
969 .map_err(Error::from)?;
970 }
971 write_txn.commit().map_err(Error::from)?;
972 Ok(())
973 }
974
975 #[instrument(skip_all)]
976 async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
977 let write_txn = self.db.begin_write().map_err(Error::from)?;
978 {
979 let mut table = write_txn
980 .open_table(MELT_QUOTES_TABLE)
981 .map_err(Error::from)?;
982 table.remove(quote_id).map_err(Error::from)?;
983 }
984 write_txn.commit().map_err(Error::from)?;
985 Ok(())
986 }
987
988 #[instrument(skip_all)]
989 async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
990 let write_txn = self.db.begin_write().map_err(Error::from)?;
991
992 keyset.verify_id()?;
993
994 {
995 let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
996
997 let existing_keys = table
998 .insert(
999 keyset.id.to_string().as_str(),
1000 serde_json::to_string(&keyset.keys)
1001 .map_err(Error::from)?
1002 .as_str(),
1003 )
1004 .map_err(Error::from)?
1005 .is_some();
1006
1007 let mut table = write_txn
1008 .open_table(KEYSET_U32_MAPPING)
1009 .map_err(Error::from)?;
1010
1011 let existing = table
1012 .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
1013 .map_err(Error::from)?;
1014
1015 let existing_u32 = match existing {
1016 None => false,
1017 Some(id) => {
1018 let id = Id::from_str(id.value())?;
1019 id != keyset.id
1020 }
1021 };
1022
1023 if existing_keys || existing_u32 {
1024 tracing::warn!("Keys already exist for keyset id");
1025 return Err(database::Error::Duplicate);
1026 }
1027 }
1028 write_txn.commit().map_err(Error::from)?;
1029 Ok(())
1030 }
1031
1032 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
1033 async fn remove_keys(&self, keyset_id: &Id) -> Result<(), database::Error> {
1034 let write_txn = self.db.begin_write().map_err(Error::from)?;
1035 {
1036 let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
1037
1038 table
1039 .remove(keyset_id.to_string().as_str())
1040 .map_err(Error::from)?;
1041 }
1042 write_txn.commit().map_err(Error::from)?;
1043 Ok(())
1044 }
1045
1046 #[instrument(skip(self))]
1047 async fn remove_transaction(
1048 &self,
1049 transaction_id: TransactionId,
1050 ) -> Result<(), database::Error> {
1051 let write_txn = self.db.begin_write().map_err(Error::from)?;
1052 {
1053 let mut table = write_txn
1054 .open_table(TRANSACTIONS_TABLE)
1055 .map_err(Error::from)?;
1056 table
1057 .remove(transaction_id.as_slice())
1058 .map_err(Error::from)?;
1059 }
1060 write_txn.commit().map_err(Error::from)?;
1061 Ok(())
1062 }
1063
1064 #[instrument(skip(self))]
1065 async fn add_saga(&self, saga: wallet::WalletSaga) -> Result<(), database::Error> {
1066 let saga_json = serde_json::to_string(&saga).map_err(Error::from)?;
1067 let id_str = saga.id.to_string();
1068
1069 let write_txn = self.db.begin_write().map_err(Error::from)?;
1070 {
1071 let mut table = write_txn.open_table(SAGAS_TABLE).map_err(Error::from)?;
1072 table
1073 .insert(id_str.as_str(), saga_json.as_str())
1074 .map_err(Error::from)?;
1075 }
1076 write_txn.commit().map_err(Error::from)?;
1077 Ok(())
1078 }
1079
1080 #[instrument(skip(self))]
1081 async fn get_saga(
1082 &self,
1083 id: &uuid::Uuid,
1084 ) -> Result<Option<wallet::WalletSaga>, database::Error> {
1085 let read_txn = self.db.begin_read().map_err(Error::from)?;
1086 let table = read_txn.open_table(SAGAS_TABLE).map_err(Error::from)?;
1087 let id_str = id.to_string();
1088
1089 let result = table
1090 .get(id_str.as_str())
1091 .map_err(Error::from)?
1092 .map(|saga| serde_json::from_str(saga.value()).map_err(Error::from))
1093 .transpose()?;
1094
1095 Ok(result)
1096 }
1097
1098 #[instrument(skip(self))]
1099 async fn update_saga(&self, saga: wallet::WalletSaga) -> Result<bool, database::Error> {
1100 let id_str = saga.id.to_string();
1101
1102 let expected_version = saga.version.saturating_sub(1);
1105
1106 let write_txn = self.db.begin_write().map_err(Error::from)?;
1107 let updated = {
1108 let mut table = write_txn.open_table(SAGAS_TABLE).map_err(Error::from)?;
1109
1110 let existing_saga_json = table
1112 .get(id_str.as_str())
1113 .map_err(Error::from)?
1114 .map(|v| v.value().to_string());
1115
1116 match existing_saga_json {
1117 Some(json) => {
1118 let existing_saga: wallet::WalletSaga =
1119 serde_json::from_str(&json).map_err(Error::from)?;
1120
1121 if existing_saga.version != expected_version {
1123 false
1125 } else {
1126 let saga_json = serde_json::to_string(&saga).map_err(Error::from)?;
1128 table
1129 .insert(id_str.as_str(), saga_json.as_str())
1130 .map_err(Error::from)?;
1131 true
1132 }
1133 }
1134 None => {
1135 false
1137 }
1138 }
1139 };
1140 write_txn.commit().map_err(Error::from)?;
1141 Ok(updated)
1142 }
1143
1144 #[instrument(skip(self))]
1145 async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), database::Error> {
1146 let write_txn = self.db.begin_write().map_err(Error::from)?;
1147 let id_str = id.to_string();
1148 {
1149 let mut table = write_txn.open_table(SAGAS_TABLE).map_err(Error::from)?;
1150 table.remove(id_str.as_str()).map_err(Error::from)?;
1151 }
1152 write_txn.commit().map_err(Error::from)?;
1153 Ok(())
1154 }
1155
1156 #[instrument(skip(self))]
1157 async fn get_incomplete_sagas(&self) -> Result<Vec<wallet::WalletSaga>, database::Error> {
1158 let read_txn = self.db.begin_read().map_err(Error::from)?;
1159 let table = read_txn.open_table(SAGAS_TABLE).map_err(Error::from)?;
1160
1161 let mut sagas: Vec<wallet::WalletSaga> = table
1162 .iter()
1163 .map_err(Error::from)?
1164 .flatten()
1165 .filter_map(|(_, saga_json)| {
1166 serde_json::from_str::<wallet::WalletSaga>(saga_json.value()).ok()
1167 })
1168 .collect();
1169
1170 sagas.sort_by_key(|saga| saga.created_at);
1172
1173 Ok(sagas)
1174 }
1175
1176 #[instrument(skip(self))]
1177 async fn reserve_proofs(
1178 &self,
1179 ys: Vec<PublicKey>,
1180 operation_id: &uuid::Uuid,
1181 ) -> Result<(), database::Error> {
1182 let write_txn = self.db.begin_write().map_err(Error::from)?;
1183
1184 {
1185 let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
1186
1187 for y in ys {
1188 let y_bytes = y.to_bytes();
1189
1190 let proof_json_str = {
1192 let proof_json_opt = table.get(y_bytes.as_slice()).map_err(Error::from)?;
1193 proof_json_opt.map(|proof_json| proof_json.value().to_string())
1194 };
1195
1196 let Some(proof_json_str) = proof_json_str else {
1197 return Err(database::Error::ProofNotUnspent);
1198 };
1199
1200 let mut proof: ProofInfo =
1201 serde_json::from_str(&proof_json_str).map_err(Error::from)?;
1202
1203 if proof.state != State::Unspent {
1204 return Err(database::Error::ProofNotUnspent);
1205 }
1206
1207 proof.state = State::Reserved;
1208 proof.used_by_operation = Some(*operation_id);
1209
1210 let updated_json = serde_json::to_string(&proof).map_err(Error::from)?;
1211 table
1212 .insert(y_bytes.as_slice(), updated_json.as_str())
1213 .map_err(Error::from)?;
1214 }
1215 }
1216
1217 write_txn.commit().map_err(Error::from)?;
1218 Ok(())
1219 }
1220
1221 #[instrument(skip(self))]
1222 async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1223 let write_txn = self.db.begin_write().map_err(Error::from)?;
1224
1225 {
1226 let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
1227
1228 let all_proofs: Vec<(Vec<u8>, ProofInfo)> = table
1230 .iter()
1231 .map_err(Error::from)?
1232 .flatten()
1233 .filter_map(|(y, proof_json)| {
1234 let proof: ProofInfo = serde_json::from_str(proof_json.value()).ok()?;
1235 Some((y.value().to_vec(), proof))
1236 })
1237 .collect();
1238
1239 for (y_bytes, mut proof) in all_proofs {
1241 if proof.used_by_operation == Some(*operation_id)
1242 && matches!(proof.state, State::Reserved | State::Pending)
1243 {
1244 proof.state = State::Unspent;
1245 proof.used_by_operation = None;
1246
1247 let updated_json = serde_json::to_string(&proof).map_err(Error::from)?;
1248 table
1249 .insert(y_bytes.as_slice(), updated_json.as_str())
1250 .map_err(Error::from)?;
1251 }
1252 }
1253 }
1254
1255 write_txn.commit().map_err(Error::from)?;
1256 Ok(())
1257 }
1258
1259 #[instrument(skip(self))]
1260 async fn get_reserved_proofs(
1261 &self,
1262 operation_id: &uuid::Uuid,
1263 ) -> Result<Vec<ProofInfo>, database::Error> {
1264 let read_txn = self.db.begin_read().map_err(Error::from)?;
1265 let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
1266
1267 let proofs: Vec<ProofInfo> = table
1268 .iter()
1269 .map_err(Error::from)?
1270 .flatten()
1271 .filter_map(|(_, proof_json)| {
1272 serde_json::from_str::<ProofInfo>(proof_json.value()).ok()
1273 })
1274 .filter(|proof| proof.used_by_operation == Some(*operation_id))
1275 .collect();
1276
1277 Ok(proofs)
1278 }
1279
1280 #[instrument(skip(self))]
1281 async fn reserve_melt_quote(
1282 &self,
1283 quote_id: &str,
1284 operation_id: &uuid::Uuid,
1285 ) -> Result<(), database::Error> {
1286 let write_txn = self.db.begin_write().map_err(Error::from)?;
1287 let operation_id_str = operation_id.to_string();
1288
1289 {
1290 let mut table = write_txn
1291 .open_table(MELT_QUOTES_TABLE)
1292 .map_err(Error::from)?;
1293
1294 let quote_json = table
1296 .get(quote_id)
1297 .map_err(Error::from)?
1298 .map(|v| v.value().to_string());
1299
1300 match quote_json {
1301 Some(json) => {
1302 let mut quote: wallet::MeltQuote =
1303 serde_json::from_str(&json).map_err(Error::from)?;
1304
1305 if quote.used_by_operation.is_some() {
1307 return Err(database::Error::QuoteAlreadyInUse);
1308 }
1309
1310 quote.used_by_operation = Some(operation_id_str);
1312 let updated_json = serde_json::to_string("e).map_err(Error::from)?;
1313 table
1314 .insert(quote_id, updated_json.as_str())
1315 .map_err(Error::from)?;
1316 }
1317 None => {
1318 return Err(database::Error::UnknownQuote);
1319 }
1320 }
1321 }
1322
1323 write_txn.commit().map_err(Error::from)?;
1324 Ok(())
1325 }
1326
1327 #[instrument(skip(self))]
1328 async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1329 let write_txn = self.db.begin_write().map_err(Error::from)?;
1330 let operation_id_str = operation_id.to_string();
1331
1332 {
1333 let mut table = write_txn
1334 .open_table(MELT_QUOTES_TABLE)
1335 .map_err(Error::from)?;
1336
1337 let all_quotes: Vec<(String, wallet::MeltQuote)> = table
1339 .iter()
1340 .map_err(Error::from)?
1341 .flatten()
1342 .filter_map(|(id, quote_json)| {
1343 let quote: wallet::MeltQuote = serde_json::from_str(quote_json.value()).ok()?;
1344 Some((id.value().to_string(), quote))
1345 })
1346 .collect();
1347
1348 for (quote_id, mut quote) in all_quotes {
1350 if quote.used_by_operation.as_deref() == Some(&operation_id_str) {
1351 quote.used_by_operation = None;
1352 let updated_json = serde_json::to_string("e).map_err(Error::from)?;
1353 table
1354 .insert(quote_id.as_str(), updated_json.as_str())
1355 .map_err(Error::from)?;
1356 }
1357 }
1358 }
1359
1360 write_txn.commit().map_err(Error::from)?;
1361 Ok(())
1362 }
1363
1364 #[instrument(skip(self))]
1365 async fn reserve_mint_quote(
1366 &self,
1367 quote_id: &str,
1368 operation_id: &uuid::Uuid,
1369 ) -> Result<(), database::Error> {
1370 let write_txn = self.db.begin_write().map_err(Error::from)?;
1371 let operation_id_str = operation_id.to_string();
1372
1373 {
1374 let mut table = write_txn
1375 .open_table(MINT_QUOTES_TABLE)
1376 .map_err(Error::from)?;
1377
1378 let quote_json = table
1380 .get(quote_id)
1381 .map_err(Error::from)?
1382 .map(|v| v.value().to_string());
1383
1384 match quote_json {
1385 Some(json) => {
1386 let mut quote: MintQuote = serde_json::from_str(&json).map_err(Error::from)?;
1387
1388 if quote.used_by_operation.is_some() {
1390 return Err(database::Error::QuoteAlreadyInUse);
1391 }
1392
1393 quote.used_by_operation = Some(operation_id_str);
1395 let updated_json = serde_json::to_string("e).map_err(Error::from)?;
1396 table
1397 .insert(quote_id, updated_json.as_str())
1398 .map_err(Error::from)?;
1399 }
1400 None => {
1401 return Err(database::Error::UnknownQuote);
1402 }
1403 }
1404 }
1405
1406 write_txn.commit().map_err(Error::from)?;
1407 Ok(())
1408 }
1409
1410 #[instrument(skip(self))]
1411 async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1412 let write_txn = self.db.begin_write().map_err(Error::from)?;
1413 let operation_id_str = operation_id.to_string();
1414
1415 {
1416 let mut table = write_txn
1417 .open_table(MINT_QUOTES_TABLE)
1418 .map_err(Error::from)?;
1419
1420 let all_quotes: Vec<(String, MintQuote)> = table
1422 .iter()
1423 .map_err(Error::from)?
1424 .flatten()
1425 .filter_map(|(id, quote_json)| {
1426 let quote: MintQuote = serde_json::from_str(quote_json.value()).ok()?;
1427 Some((id.value().to_string(), quote))
1428 })
1429 .collect();
1430
1431 for (quote_id, mut quote) in all_quotes {
1433 if quote.used_by_operation.as_deref() == Some(&operation_id_str) {
1434 quote.used_by_operation = None;
1435 let updated_json = serde_json::to_string("e).map_err(Error::from)?;
1436 table
1437 .insert(quote_id.as_str(), updated_json.as_str())
1438 .map_err(Error::from)?;
1439 }
1440 }
1441 }
1442
1443 write_txn.commit().map_err(Error::from)?;
1444 Ok(())
1445 }
1446
1447 #[instrument(skip(self, value))]
1448 async fn kv_write(
1449 &self,
1450 primary_namespace: &str,
1451 secondary_namespace: &str,
1452 key: &str,
1453 value: &[u8],
1454 ) -> Result<(), database::Error> {
1455 validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
1457
1458 let write_txn = self.db.begin_write().map_err(Error::from)?;
1459 {
1460 let mut table = write_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
1461 table
1462 .insert((primary_namespace, secondary_namespace, key), value)
1463 .map_err(Error::from)?;
1464 }
1465 write_txn.commit().map_err(Error::from)?;
1466
1467 Ok(())
1468 }
1469
1470 #[instrument(skip(self))]
1471 async fn kv_read(
1472 &self,
1473 primary_namespace: &str,
1474 secondary_namespace: &str,
1475 key: &str,
1476 ) -> Result<Option<Vec<u8>>, database::Error> {
1477 validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
1479
1480 let read_txn = self.db.begin_read().map_err(Error::from)?;
1481 let table = read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
1482
1483 let result = table
1484 .get((primary_namespace, secondary_namespace, key))
1485 .map_err(Error::from)?
1486 .map(|v| v.value().to_vec());
1487
1488 Ok(result)
1489 }
1490
1491 #[instrument(skip(self))]
1492 async fn kv_list(
1493 &self,
1494 primary_namespace: &str,
1495 secondary_namespace: &str,
1496 ) -> Result<Vec<String>, database::Error> {
1497 validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
1499
1500 let read_txn = self.db.begin_read().map_err(Error::from)?;
1501 let table = read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
1502
1503 let start = (primary_namespace, secondary_namespace, "");
1504 let iter = table.range(start..).map_err(Error::from)?;
1505
1506 let mut keys = Vec::new();
1507
1508 for item in iter {
1509 let (key, _) = item.map_err(Error::from)?;
1510 let (p, s, k) = key.value();
1511 if p == primary_namespace && s == secondary_namespace {
1512 keys.push(k.to_string());
1513 } else {
1514 break;
1515 }
1516 }
1517
1518 Ok(keys)
1519 }
1520
1521 #[instrument(skip(self))]
1522 async fn kv_remove(
1523 &self,
1524 primary_namespace: &str,
1525 secondary_namespace: &str,
1526 key: &str,
1527 ) -> Result<(), database::Error> {
1528 validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
1530
1531 let write_txn = self.db.begin_write().map_err(Error::from)?;
1532 {
1533 let mut table = write_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
1534 table
1535 .remove((primary_namespace, secondary_namespace, key))
1536 .map_err(Error::from)?;
1537 }
1538 write_txn.commit().map_err(Error::from)?;
1539
1540 Ok(())
1541 }
1542
1543 #[instrument(skip(self))]
1544 async fn add_p2pk_key(
1545 &self,
1546 pubkey: &PublicKey,
1547 derivation_path: DerivationPath,
1548 derivation_index: u32,
1549 ) -> Result<(), database::Error> {
1550 let write_txn = self.db.begin_write().map_err(Error::from)?;
1551 {
1552 let mut table = write_txn
1553 .open_table(P2PK_SIGNING_KEYS_TABLE)
1554 .map_err(Error::from)?;
1555 table
1556 .insert(
1557 pubkey.to_bytes().as_slice(),
1558 serde_json::to_string(&wallet::P2PKSigningKey {
1559 pubkey: *pubkey,
1560 derivation_path,
1561 derivation_index,
1562 created_time: unix_time(),
1563 })
1564 .map_err(Error::from)?
1565 .as_str(),
1566 )
1567 .map_err(Error::from)?;
1568 }
1569 write_txn.commit().map_err(Error::from)?;
1570 Ok(())
1571 }
1572
1573 #[instrument(skip(self))]
1574 async fn get_p2pk_key(
1575 &self,
1576 pubkey: &PublicKey,
1577 ) -> Result<Option<wallet::P2PKSigningKey>, database::Error> {
1578 let read_txn = self.db.begin_read().map_err(Error::from)?;
1579 let table = read_txn
1580 .open_table(P2PK_SIGNING_KEYS_TABLE)
1581 .map_err(Error::from)?;
1582
1583 if let Some(key) = table
1584 .get(pubkey.to_bytes().as_slice())
1585 .map_err(Error::from)?
1586 {
1587 return Ok(Some(
1588 serde_json::from_str(key.value()).map_err(Error::from)?,
1589 ));
1590 }
1591
1592 Ok(None)
1593 }
1594
1595 #[instrument(skip(self))]
1596 async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, database::Error> {
1597 let read_txn = self.db.begin_read().map_err(Error::from)?;
1598 let table = read_txn
1599 .open_table(P2PK_SIGNING_KEYS_TABLE)
1600 .map_err(Error::from)?;
1601
1602 let keys: Vec<wallet::P2PKSigningKey> = table
1603 .iter()
1604 .map_err(Error::from)?
1605 .flatten()
1606 .filter_map(|(_k, v)| {
1607 if let Ok(key) = serde_json::from_str::<wallet::P2PKSigningKey>(v.value()) {
1608 return Some(key);
1609 }
1610
1611 None
1612 })
1613 .collect();
1614
1615 Ok(keys)
1616 }
1617
1618 #[instrument(skip(self))]
1619 async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, database::Error> {
1620 let read_txn = self.db.begin_read().map_err(Error::from)?;
1621 let table = read_txn
1622 .open_table(P2PK_SIGNING_KEYS_TABLE)
1623 .map_err(Error::from)?;
1624
1625 let latest_key = table
1626 .iter()
1627 .map_err(Error::from)?
1628 .flatten()
1629 .filter_map(|(_k, v)| serde_json::from_str::<wallet::P2PKSigningKey>(v.value()).ok())
1630 .max_by_key(|key| key.derivation_index);
1631
1632 Ok(latest_key)
1633 }
1634}
1635
1636#[cfg(test)]
1637mod test {
1638 use std::path::PathBuf;
1639 use std::str::FromStr;
1640
1641 use cdk_common::database::{self, WalletDatabase};
1642 use cdk_common::{wallet_db_test, Id};
1643
1644 use super::WalletRedbDatabase;
1645
1646 async fn provide_db(test_id: String) -> WalletRedbDatabase {
1647 let path = PathBuf::from(format!("/tmp/cdk-test-{}.redb", test_id));
1648 WalletRedbDatabase::new(&path).expect("database")
1649 }
1650
1651 wallet_db_test!(provide_db);
1652
1653 #[tokio::test]
1654 async fn increment_keyset_counter_returns_error_on_overflow() {
1655 let db = provide_db(format!("counter-overflow-{}", uuid::Uuid::new_v4())).await;
1656 let keyset_id = Id::from_str("00916bbf7ef91a36").expect("valid keyset id");
1657
1658 let first = db
1659 .increment_keyset_counter(&keyset_id, u32::MAX)
1660 .await
1661 .expect("first increment should fit");
1662 assert_eq!(first, u32::MAX);
1663
1664 match db.increment_keyset_counter(&keyset_id, 1).await {
1665 Err(database::Error::AmountOverflow) => {}
1666 Ok(counter) => panic!("counter should not wrap, got {counter}"),
1667 Err(err) => panic!("expected amount overflow, got {err}"),
1668 }
1669 }
1670}