refactor: database use one proofs table

This commit is contained in:
thesimplekid
2024-05-22 00:30:39 +01:00
parent 0f08bd43bb
commit 10a38247ce
10 changed files with 392 additions and 371 deletions

View File

@@ -8,6 +8,7 @@ pub enum JsState {
Spent,
Unspent,
Pending,
Reserved,
}
impl From<State> for JsState {
@@ -16,6 +17,7 @@ impl From<State> for JsState {
State::Spent => JsState::Spent,
State::Unspent => JsState::Unspent,
State::Pending => JsState::Pending,
State::Reserved => JsState::Reserved,
}
}
}
@@ -26,6 +28,7 @@ impl From<JsState> for State {
JsState::Spent => State::Spent,
JsState::Unspent => State::Unspent,
JsState::Pending => State::Pending,
JsState::Reserved => State::Reserved,
}
}
}

View File

@@ -17,7 +17,7 @@ nostr = ["cdk/nostr"]
[dependencies]
async-trait.workspace = true
cdk = { workspace = true, default-features = false }
redb = "2.0.0"
redb = "2.1.0"
tokio.workspace = true
thiserror.workspace = true
tracing.workspace = true

View File

@@ -43,6 +43,9 @@ pub enum Error {
/// Unknown Mint Info
#[error("Unknown Mint Info")]
UnknownMintInfo,
/// Unknown Proof Y
#[error("Unknown Proof Y")]
UnknownY,
}
impl From<Error> for cdk::cdk_database::Error {

View File

@@ -5,10 +5,8 @@ use std::sync::Arc;
use async_trait::async_trait;
use cdk::cdk_database;
use cdk::cdk_database::WalletDatabase;
#[cfg(feature = "nostr")]
use cdk::nuts::PublicKey;
use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs};
use cdk::types::{MeltQuote, MintQuote};
use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, State};
use cdk::types::{MeltQuote, MintQuote, ProofInfo};
use cdk::url::UncheckedUrl;
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
use tokio::sync::Mutex;
@@ -22,9 +20,8 @@ const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &str> =
const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_keys");
const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("proofs");
const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> =
MultimapTableDefinition::new("pending_proofs");
// <Y, (Proof, Status, Mint url)>
const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
#[cfg(feature = "nostr")]
@@ -66,7 +63,7 @@ impl RedbWalletDatabase {
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
let _ = write_txn.open_multimap_table(PROOFS_TABLE)?;
let _ = write_txn.open_table(PROOFS_TABLE)?;
let _ = write_txn.open_table(KEYSET_COUNTER)?;
#[cfg(feature = "nostr")]
let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
@@ -373,22 +370,22 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self, proofs))]
async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> {
#[instrument(skip(self, proofs_info))]
async fn add_proofs(&self, proofs_info: Vec<ProofInfo>) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
{
let mut table = write_txn
.open_multimap_table(PROOFS_TABLE)
.map_err(Error::from)?;
let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
for proof in proofs {
for proof_info in proofs_info.iter() {
table
.insert(
mint_url.to_string().as_str(),
serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
proof_info.y.to_bytes().as_slice(),
serde_json::to_string(&proof_info)
.map_err(Error::from)?
.as_str(),
)
.map_err(Error::from)?;
}
@@ -399,124 +396,69 @@ impl WalletDatabase for RedbWalletDatabase {
}
#[instrument(skip(self))]
async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn
.open_multimap_table(PROOFS_TABLE)
.map_err(Error::from)?;
let proofs = table
.get(mint_url.to_string().as_str())
.map_err(Error::from)?
.flatten()
.flat_map(|k| serde_json::from_str(k.value()))
.collect();
Ok(proofs)
}
#[instrument(skip(self, proofs))]
async fn remove_proofs(
async fn get_proofs(
&self,
mint_url: UncheckedUrl,
proofs: &Proofs,
) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
{
let mut table = write_txn
.open_multimap_table(PROOFS_TABLE)
.map_err(Error::from)?;
for proof in proofs {
table
.remove(
mint_url.to_string().as_str(),
serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
)
.map_err(Error::from)?;
}
}
write_txn.commit().map_err(Error::from)?;
Ok(())
}
#[instrument(skip(self, proofs))]
async fn add_pending_proofs(
&self,
mint_url: UncheckedUrl,
proofs: Proofs,
) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
{
let mut table = write_txn
.open_multimap_table(PENDING_PROOFS_TABLE)
.map_err(Error::from)?;
for proof in proofs {
table
.insert(
mint_url.to_string().as_str(),
serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
)
.map_err(Error::from)?;
}
}
write_txn.commit().map_err(Error::from)?;
Ok(())
}
#[instrument(skip(self))]
async fn get_pending_proofs(
&self,
mint_url: UncheckedUrl,
mint_url: Option<UncheckedUrl>,
state: Option<Vec<State>>,
) -> Result<Option<Proofs>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn
.open_multimap_table(PENDING_PROOFS_TABLE)
.map_err(Error::from)?;
let proofs = table
.get(mint_url.to_string().as_str())
let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
let proofs: Proofs = table
.iter()
.map_err(Error::from)?
.flatten()
.flat_map(|k| serde_json::from_str(k.value()))
.filter_map(|(_k, v)| {
let mut proof = None;
if let Ok(proof_info) = serde_json::from_str::<ProofInfo>(v.value()) {
match (&mint_url, &state) {
(Some(mint_url), Some(state)) => {
if state.contains(&proof_info.state)
&& mint_url.eq(&proof_info.mint_url)
{
proof = Some(proof_info.proof);
}
}
(Some(mint_url), None) => {
if mint_url.eq(&proof_info.mint_url) {
proof = Some(proof_info.proof);
}
}
(None, Some(state)) => {
if state.contains(&proof_info.state) {
proof = Some(proof_info.proof);
}
}
(None, None) => proof = Some(proof_info.proof),
}
}
proof
})
.collect();
Ok(proofs)
if proofs.is_empty() {
return Ok(None);
}
Ok(Some(proofs))
}
#[instrument(skip(self, proofs))]
async fn remove_pending_proofs(
&self,
mint_url: UncheckedUrl,
proofs: &Proofs,
) -> Result<(), Self::Err> {
async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
{
let mut table = write_txn
.open_multimap_table(PENDING_PROOFS_TABLE)
.map_err(Error::from)?;
let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
for proof in proofs {
table
.remove(
mint_url.to_string().as_str(),
serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
)
.map_err(Error::from)?;
let y_slice = proof.y().map_err(Error::from)?.to_bytes();
table.remove(y_slice.as_slice()).map_err(Error::from)?;
}
}
write_txn.commit().map_err(Error::from)?;
@@ -524,6 +466,41 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self))]
async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
let y_slice = y.to_bytes();
let proof = table.get(y_slice.as_slice()).map_err(Error::from)?;
let write_txn = db.begin_write().map_err(Error::from)?;
if let Some(proof) = proof {
let mut proof_info =
serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
proof_info.state = state;
{
let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
table
.insert(
y_slice.as_slice(),
serde_json::to_string(&proof_info)
.map_err(Error::from)?
.as_str(),
)
.map_err(Error::from)?;
}
}
write_txn.commit().map_err(Error::from)?;
Err(Error::UnknownY.into())
}
#[instrument(skip(self))]
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
let db = self.db.lock().await;

View File

@@ -4,8 +4,8 @@ use std::result::Result;
use async_trait::async_trait;
use cdk::cdk_database::WalletDatabase;
use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs};
use cdk::types::{MeltQuote, MintQuote};
use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, State};
use cdk::types::{MeltQuote, MintQuote, ProofInfo};
use cdk::url::UncheckedUrl;
use rexie::*;
use thiserror::Error;
@@ -18,7 +18,6 @@ const MINT_KEYS: &str = "mint_keys";
const MINT_QUOTES: &str = "mint_quotes";
const MELT_QUOTES: &str = "melt_quotes";
const PROOFS: &str = "proofs";
const PENDING_PROOFS: &str = "pending_proofs";
const CONFIG: &str = "config";
const KEYSET_COUNTER: &str = "keyset_counter";
@@ -35,6 +34,8 @@ pub enum Error {
/// Serde Wasm Error
#[error(transparent)]
SerdeBindgen(#[from] serde_wasm_bindgen::Error),
#[error(transparent)]
NUT00(cdk::nuts::nut00::Error),
}
impl From<Error> for cdk::cdk_database::Error {
fn from(e: Error) -> Self {
@@ -87,10 +88,6 @@ impl RexieWalletDatabase {
ObjectStore::new(MELT_QUOTES)
.add_index(Index::new("keyset_id", "keyset_id").unique(true)),
)
.add_object_store(
ObjectStore::new(PENDING_PROOFS)
.add_index(Index::new("keyset_id", "keyset_id").unique(true)),
)
.add_object_store(
ObjectStore::new(CONFIG)
.add_index(Index::new("keyset_id", "keyset_id").unique(true)),
@@ -423,7 +420,7 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}
async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> {
async fn add_proofs(&self, proofs: Vec<ProofInfo>) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
@@ -432,33 +429,27 @@ impl WalletDatabase for RexieWalletDatabase {
let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
for proof in proofs {
let y = proof.y;
let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?;
let proof = serde_wasm_bindgen::to_value(&proof).map_err(Error::from)?;
let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
let current_proofs: Proofs =
serde_wasm_bindgen::from_value(current_proofs).unwrap_or_default();
let all_proofs: Proofs = current_proofs
.into_iter()
.chain(proofs.into_iter())
.collect();
let all_proofs = serde_wasm_bindgen::to_value(&all_proofs).map_err(Error::from)?;
web_sys::console::log_1(&all_proofs);
proofs_store
.put(&all_proofs, Some(&mint_url))
.await
.map_err(Error::from)?;
proofs_store
.put(&proof, Some(&y))
.await
.map_err(Error::from)?;
}
transaction.done().await.map_err(Error::from)?;
Ok(())
}
async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err> {
async fn get_proofs(
&self,
mint_url: Option<UncheckedUrl>,
state: Option<Vec<State>>,
) -> Result<Option<Proofs>, Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
@@ -467,21 +458,53 @@ impl WalletDatabase for RexieWalletDatabase {
let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
let proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
let proofs = proofs_store
.get_all(None, None, None, None)
.await
.map_err(Error::from)?;
let proofs: Proofs = proofs
.into_iter()
.filter_map(|(_k, v)| {
let mut proof = None;
if let Ok(proof_info) = serde_wasm_bindgen::from_value::<ProofInfo>(v) {
match (&mint_url, &state) {
(Some(mint_url), Some(state)) => {
if state.contains(&proof_info.state)
&& mint_url.eq(&proof_info.mint_url)
{
proof = Some(proof_info.proof);
}
}
(Some(mint_url), None) => {
if mint_url.eq(&proof_info.mint_url) {
proof = Some(proof_info.proof);
}
}
(None, Some(state)) => {
if state.contains(&proof_info.state) {
proof = Some(proof_info.proof);
}
}
(None, None) => proof = Some(proof_info.proof),
}
}
proof
})
.collect();
transaction.done().await.map_err(Error::from)?;
let proofs: Option<Proofs> = serde_wasm_bindgen::from_value(proofs).map_err(Error::from)?;
if proofs.is_empty() {
return Ok(None);
}
Ok(proofs)
Ok(Some(proofs))
}
async fn remove_proofs(
&self,
mint_url: UncheckedUrl,
proofs: &Proofs,
) -> Result<(), Self::Err> {
async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
@@ -490,24 +513,10 @@ impl WalletDatabase for RexieWalletDatabase {
let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
for proof in proofs {
let y = serde_wasm_bindgen::to_value(&proof.y()?).map_err(Error::from)?;
let current_proofs: Option<Proofs> =
serde_wasm_bindgen::from_value(current_proofs).map_err(Error::from)?;
if let Some(current_proofs) = current_proofs {
let proofs: Proofs = current_proofs
.into_iter()
.filter(|p| !proofs.contains(p))
.collect();
let proofs = serde_wasm_bindgen::to_value(&proofs).map_err(Error::from)?;
proofs_store
.put(&proofs, Some(&mint_url))
.await
.map_err(Error::from)?;
proofs_store.delete(&y).await.map_err(Error::from)?;
}
transaction.done().await.map_err(Error::from)?;
@@ -515,35 +524,26 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}
async fn add_pending_proofs(
&self,
mint_url: UncheckedUrl,
proofs: Proofs,
) -> Result<(), Self::Err> {
async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
.transaction(&[PENDING_PROOFS], TransactionMode::ReadWrite)
.transaction(&[PROOFS], TransactionMode::ReadWrite)
.map_err(Error::from)?;
let proofs_store = transaction.store(PENDING_PROOFS).map_err(Error::from)?;
let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?;
let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
let proof = proofs_store.get(&y).await.map_err(Error::from)?;
let mut proof: ProofInfo = serde_wasm_bindgen::from_value(proof).map_err(Error::from)?;
let current_proofs: Proofs =
serde_wasm_bindgen::from_value(current_proofs).unwrap_or_default();
proof.state = state;
let all_proofs: Proofs = current_proofs
.into_iter()
.chain(proofs.into_iter())
.collect();
let all_proofs = serde_wasm_bindgen::to_value(&all_proofs).map_err(Error::from)?;
let proof = serde_wasm_bindgen::to_value(&proof).map_err(Error::from)?;
proofs_store
.put(&all_proofs, Some(&mint_url))
.put(&proof, Some(&y))
.await
.map_err(Error::from)?;
@@ -552,66 +552,6 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}
async fn get_pending_proofs(
&self,
mint_url: UncheckedUrl,
) -> Result<Option<Proofs>, Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
.transaction(&[PENDING_PROOFS], TransactionMode::ReadOnly)
.map_err(Error::from)?;
let proofs_store = transaction.store(PENDING_PROOFS).map_err(Error::from)?;
let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
let proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
transaction.done().await.map_err(Error::from)?;
let proofs: Option<Proofs> = serde_wasm_bindgen::from_value(proofs).unwrap_or(None);
Ok(proofs)
}
async fn remove_pending_proofs(
&self,
mint_url: UncheckedUrl,
proofs: &Proofs,
) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
.transaction(&[PENDING_PROOFS], TransactionMode::ReadWrite)
.map_err(Error::from)?;
let proofs_store = transaction.store(PENDING_PROOFS).map_err(Error::from)?;
let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
let current_proofs: Option<Proofs> =
serde_wasm_bindgen::from_value(current_proofs).map_err(Error::from)?;
if let Some(current_proofs) = current_proofs {
let proofs: Proofs = current_proofs
.into_iter()
.filter(|p| !proofs.contains(p))
.collect();
let proofs = serde_wasm_bindgen::to_value(&proofs).map_err(Error::from)?;
proofs_store
.add(&proofs, Some(&mint_url))
.await
.map_err(Error::from)?;
}
transaction.done().await.map_err(Error::from)?;
Ok(())
}
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;

View File

@@ -9,16 +9,18 @@ use thiserror::Error;
#[cfg(feature = "mint")]
use crate::mint::MintKeySetInfo;
#[cfg(any(feature = "nostr", feature = "mint"))]
use crate::nuts::PublicKey;
#[cfg(feature = "wallet")]
use crate::nuts::State;
#[cfg(feature = "mint")]
use crate::nuts::{BlindSignature, CurrencyUnit, Proof};
#[cfg(any(feature = "wallet", feature = "mint"))]
use crate::nuts::{Id, MintInfo};
use crate::nuts::{Id, MintInfo, PublicKey};
#[cfg(feature = "wallet")]
use crate::nuts::{KeySetInfo, Keys, Proofs};
#[cfg(feature = "mint")]
use crate::secret::Secret;
#[cfg(feature = "wallet")]
use crate::types::ProofInfo;
#[cfg(any(feature = "wallet", feature = "mint"))]
use crate::types::{MeltQuote, MintQuote};
#[cfg(feature = "wallet")]
@@ -35,6 +37,8 @@ pub enum Error {
Database(Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Cdk(#[from] crate::error::Error),
#[error(transparent)]
NUT01(#[from] crate::nuts::nut00::Error),
}
#[cfg(feature = "wallet")]
@@ -73,23 +77,15 @@ pub trait WalletDatabase {
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err>;
async fn add_proofs(&self, mint_url: UncheckedUrl, proof: Proofs) -> Result<(), Self::Err>;
async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err>;
async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs)
-> Result<(), Self::Err>;
async fn add_proofs(&self, proof_info: Vec<ProofInfo>) -> Result<(), Self::Err>;
async fn get_proofs(
&self,
mint_url: Option<UncheckedUrl>,
state: Option<Vec<State>>,
) -> Result<Option<Proofs>, Self::Err>;
async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err>;
async fn add_pending_proofs(
&self,
mint_url: UncheckedUrl,
proof: Proofs,
) -> Result<(), Self::Err>;
async fn get_pending_proofs(&self, mint_url: UncheckedUrl)
-> Result<Option<Proofs>, Self::Err>;
async fn remove_pending_proofs(
&self,
mint_url: UncheckedUrl,
proofs: &Proofs,
) -> Result<(), Self::Err>;
async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err>;
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;

View File

@@ -8,10 +8,8 @@ use tokio::sync::RwLock;
use super::WalletDatabase;
use crate::cdk_database::Error;
#[cfg(feature = "nostr")]
use crate::nuts::PublicKey;
use crate::nuts::{Id, KeySetInfo, Keys, MintInfo, Proof, Proofs};
use crate::types::{MeltQuote, MintQuote};
use crate::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, State};
use crate::types::{MeltQuote, MintQuote, ProofInfo};
use crate::url::UncheckedUrl;
// TODO: Change these all to RwLocks
@@ -22,8 +20,7 @@ pub struct WalletMemoryDatabase {
mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
melt_quotes: Arc<RwLock<HashMap<String, MeltQuote>>>,
mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
proofs: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Proof>>>>,
pending_proofs: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Proof>>>>,
proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
#[cfg(feature = "nostr")]
nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>,
@@ -50,7 +47,6 @@ impl WalletMemoryDatabase {
mint_keys.into_iter().map(|k| (Id::from(&k), k)).collect(),
)),
proofs: Arc::new(RwLock::new(HashMap::new())),
pending_proofs: Arc::new(RwLock::new(HashMap::new())),
keyset_counter: Arc::new(RwLock::new(keyset_counter)),
#[cfg(feature = "nostr")]
nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)),
@@ -160,69 +156,81 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(())
}
async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Error> {
async fn add_proofs(&self, proofs_info: Vec<ProofInfo>) -> Result<(), Error> {
let mut all_proofs = self.proofs.write().await;
let mint_proofs = all_proofs.entry(mint_url).or_insert(HashSet::new());
mint_proofs.extend(proofs);
Ok(())
}
async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
Ok(self
.proofs
.read()
.await
.get(&mint_url)
.map(|p| p.iter().cloned().collect()))
}
async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs) -> Result<(), Error> {
let mut mint_proofs = self.proofs.write().await;
if let Some(mint_proofs) = mint_proofs.get_mut(&mint_url) {
for proof in proofs {
mint_proofs.remove(proof);
}
for proof_info in proofs_info.into_iter() {
all_proofs.insert(proof_info.y, proof_info);
}
Ok(())
}
async fn add_pending_proofs(
async fn get_proofs(
&self,
mint_url: UncheckedUrl,
proofs: Proofs,
) -> Result<(), Error> {
let mut all_proofs = self.pending_proofs.write().await;
mint_url: Option<UncheckedUrl>,
state: Option<Vec<State>>,
) -> Result<Option<Proofs>, Error> {
let proofs = self.proofs.read().await;
let mint_proofs = all_proofs.entry(mint_url).or_insert(HashSet::new());
mint_proofs.extend(proofs);
let proofs: Proofs = proofs
.clone()
.into_values()
.filter_map(|proof_info| match (mint_url.clone(), state.clone()) {
(Some(mint_url), Some(state)) => {
if state.contains(&proof_info.state) && mint_url.eq(&proof_info.mint_url) {
Some(proof_info.proof)
} else {
None
}
}
(Some(mint_url), None) => {
if proof_info.mint_url.eq(&mint_url) {
Some(proof_info.proof)
} else {
None
}
}
(None, Some(state)) => {
if state.contains(&proof_info.state) {
Some(proof_info.proof)
} else {
None
}
}
(None, None) => Some(proof_info.proof),
})
.collect();
if proofs.is_empty() {
return Ok(None);
}
Ok(Some(proofs))
}
async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Error> {
let mut mint_proofs = self.proofs.write().await;
for proof in proofs {
mint_proofs.remove(&proof.y().map_err(Error::from)?);
}
Ok(())
}
async fn get_pending_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
Ok(self
.pending_proofs
.read()
.await
.get(&mint_url)
.map(|p| p.iter().cloned().collect()))
}
async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
let mint_proofs = self.proofs.read().await;
async fn remove_pending_proofs(
&self,
mint_url: UncheckedUrl,
proofs: &Proofs,
) -> Result<(), Error> {
let mut mint_proofs = self.pending_proofs.write().await;
let mint_proof = mint_proofs.get(&y);
if let Some(mint_proofs) = mint_proofs.get_mut(&mint_url) {
for proof in proofs {
mint_proofs.remove(proof);
}
let mut mint_proofs = self.proofs.write().await;
if let Some(proof_info) = mint_proof {
let mut proof_info = proof_info.clone();
proof_info.state = state;
mint_proofs.insert(y, proof_info);
}
Ok(())

View File

@@ -2,16 +2,56 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/07.md>
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::nut01::PublicKey;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
/// NUT07 Error
#[derive(Debug, Error, PartialEq, Eq)]
pub enum Error {
/// Unknown State error
#[error("Unknown State")]
UnknownState,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum State {
Spent,
Unspent,
Pending,
Reserved,
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
State::Spent => "SPENT",
State::Unspent => "UNSPENT",
State::Pending => "PENDING",
State::Reserved => "RESERVED",
};
write!(f, "{}", s)
}
}
impl FromStr for State {
type Err = Error;
fn from_str(state: &str) -> Result<Self, Self::Err> {
match state {
"SPENT" => Ok(Self::Spent),
"UNSPENT" => Ok(Self::Unspent),
"PENDING" => Ok(Self::Pending),
"RESERVED" => Ok(Self::Reserved),
_ => Err(Error::UnknownState),
}
}
}
/// Check spendabale request [NUT-07]

View File

@@ -3,12 +3,13 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::nuts::{CurrencyUnit, Proofs};
use crate::error::Error;
use crate::nuts::{CurrencyUnit, Proof, Proofs, PublicKey, State};
use crate::url::UncheckedUrl;
use crate::Amount;
/// Melt response with proofs
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Melted {
pub paid: bool,
pub preimage: Option<String>,
@@ -16,7 +17,7 @@ pub struct Melted {
}
/// Possible states of an invoice
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum InvoiceStatus {
Unpaid,
Paid,
@@ -25,7 +26,7 @@ pub enum InvoiceStatus {
}
/// Mint Quote Info
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintQuote {
pub id: String,
pub mint_url: UncheckedUrl,
@@ -59,7 +60,7 @@ impl MintQuote {
}
/// Melt Quote Info
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeltQuote {
pub id: String,
pub unit: CurrencyUnit,
@@ -91,3 +92,26 @@ impl MeltQuote {
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofInfo {
pub proof: Proof,
pub y: PublicKey,
pub mint_url: UncheckedUrl,
pub state: State,
}
impl ProofInfo {
pub fn new(proof: Proof, mint_url: UncheckedUrl, state: State) -> Result<Self, Error> {
let y = proof
.y()
.map_err(|_| Error::CustomError("Could not find y".to_string()))?;
Ok(Self {
proof,
y,
mint_url,
state,
})
}
}

View File

@@ -26,7 +26,7 @@ use crate::nuts::{
ProofState, Proofs, PublicKey, RestoreRequest, SecretKey, SigFlag, SpendingConditions, State,
SwapRequest, Token,
};
use crate::types::{MeltQuote, Melted, MintQuote};
use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo};
use crate::url::UncheckedUrl;
use crate::util::{hex, unix_time};
use crate::{Amount, Bolt11Invoice};
@@ -139,15 +139,16 @@ impl Wallet {
/// Total Balance of wallet
#[instrument(skip(self))]
pub async fn total_balance(&self) -> Result<Amount, Error> {
let mints = self.localstore.get_mints().await?;
let mut balance = Amount::ZERO;
for (mint, _) in mints {
if let Some(proofs) = self.localstore.get_proofs(mint.clone()).await? {
let amount = proofs.iter().map(|p| p.amount).sum();
if let Some(proofs) = self
.localstore
.get_proofs(None, Some(vec![State::Unspent]))
.await?
{
let amount = proofs.iter().map(|p| p.amount).sum();
balance += amount;
}
balance += amount;
}
Ok(balance)
@@ -156,15 +157,16 @@ impl Wallet {
/// Total Balance of wallet
#[instrument(skip(self))]
pub async fn total_pending_balance(&self) -> Result<Amount, Error> {
let mints = self.localstore.get_mints().await?;
let mut balance = Amount::ZERO;
for (mint, _) in mints {
if let Some(proofs) = self.localstore.get_pending_proofs(mint.clone()).await? {
let amount = proofs.iter().map(|p| p.amount).sum();
if let Some(proofs) = self
.localstore
.get_proofs(None, Some(vec![State::Pending]))
.await?
{
let amount = proofs.iter().map(|p| p.amount).sum();
balance += amount;
}
balance += amount;
}
Ok(balance)
@@ -177,7 +179,7 @@ impl Wallet {
let mut balances = HashMap::new();
for (mint, _) in mints {
if let Some(proofs) = self.localstore.get_proofs(mint.clone()).await? {
if let Some(proofs) = self.localstore.get_proofs(Some(mint.clone()), None).await? {
let amount = proofs.iter().map(|p| p.amount).sum();
balances.insert(mint, amount);
@@ -191,7 +193,10 @@ impl Wallet {
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
Ok(self.localstore.get_proofs(mint_url).await?)
Ok(self
.localstore
.get_proofs(Some(mint_url), Some(vec![State::Unspent]))
.await?)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
@@ -347,7 +352,14 @@ impl Wallet {
let mut balance = Amount::ZERO;
for (mint, _) in mints {
if let Some(proofs) = self.localstore.get_pending_proofs(mint.clone()).await? {
if let Some(proofs) = self
.localstore
.get_proofs(
Some(mint.clone()),
Some(vec![State::Unspent, State::Pending]),
)
.await?
{
let states = self
.check_proofs_spent(mint.clone(), proofs.clone())
.await?;
@@ -367,9 +379,7 @@ impl Wallet {
let amount = pending_proofs.iter().map(|p| p.amount).sum();
self.localstore
.remove_pending_proofs(mint, &non_pending_proofs)
.await?;
self.localstore.remove_proofs(&non_pending_proofs).await?;
balance += amount;
}
@@ -594,8 +604,13 @@ impl Wallet {
.increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
.await?;
let proofs = proofs
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
.collect();
// Add new proofs to store
self.localstore.add_proofs(mint_url, proofs).await?;
self.localstore.add_proofs(proofs).await?;
Ok(minted_amount)
}
@@ -680,9 +695,13 @@ impl Wallet {
);
}
self.localstore
.add_pending_proofs(mint_url.clone(), send_proofs.clone())
.await?;
let send_proofs_info = send_proofs
.clone()
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Reserved))
.collect();
self.localstore.add_proofs(send_proofs_info).await?;
proofs_to_send = Some(send_proofs);
}
@@ -692,17 +711,20 @@ impl Wallet {
}
}
self.localstore
.remove_proofs(mint_url.clone(), &input_proofs)
.await?;
self.localstore.remove_proofs(&input_proofs).await?;
self.localstore
.add_pending_proofs(mint_url.clone(), input_proofs)
.await?;
for proof in input_proofs {
self.localstore
.set_proof_state(proof.y()?, State::Reserved)
.await?;
}
self.localstore
.add_proofs(mint_url.clone(), keep_proofs)
.await?;
let keep_proofs = keep_proofs
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
.collect();
self.localstore.add_proofs(keep_proofs).await?;
Ok(proofs_to_send)
}
@@ -913,7 +935,7 @@ impl Wallet {
) -> Result<Proofs, Error> {
let mint_proofs = self
.localstore
.get_proofs(mint_url.clone())
.get_proofs(Some(mint_url.clone()), Some(vec![State::Unspent]))
.await?
.ok_or(Error::InsufficientFunds)?;
@@ -1048,16 +1070,17 @@ impl Wallet {
.increment_keyset_counter(&active_keyset_id, change_proofs.len() as u32)
.await?;
self.localstore
.add_proofs(mint_url.clone(), change_proofs)
.await?;
let change_proofs_info = change_proofs
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
.collect();
self.localstore.add_proofs(change_proofs_info).await?;
}
self.localstore.remove_melt_quote(&quote_info.id).await?;
self.localstore
.remove_proofs(mint_url.clone(), &proofs)
.await?;
self.localstore.remove_proofs(&proofs).await?;
Ok(melted)
}
@@ -1209,7 +1232,11 @@ impl Wallet {
let mut total_amount = Amount::ZERO;
for (mint, proofs) in received_proofs {
total_amount += proofs.iter().map(|p| p.amount).sum();
self.localstore.add_proofs(mint, proofs).await?;
let proofs = proofs
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint.clone(), State::Unspent))
.collect();
self.localstore.add_proofs(proofs).await?;
}
Ok(total_amount)
@@ -1402,9 +1429,12 @@ impl Wallet {
restored_value += unspent_proofs.iter().map(|p| p.amount).sum();
self.localstore
.add_proofs(mint_url.clone(), unspent_proofs)
.await?;
let unspent_proofs = unspent_proofs
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
.collect();
self.localstore.add_proofs(unspent_proofs).await?;
empty_batch = 0;
start_counter += 100;