mirror of
https://github.com/aljazceru/cdk.git
synced 2026-01-10 16:35:37 +01:00
refactor: mint memory localstore
This commit is contained in:
91
crates/cashu-sdk/src/mint/localstore/memory.rs
Normal file
91
crates/cashu-sdk/src/mint/localstore/memory.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cashu::nuts::nut02::mint::KeySet;
|
||||
use cashu::nuts::{Id, Proof};
|
||||
use cashu::secret::Secret;
|
||||
use cashu::types::{MeltQuote, MintQuote};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::{Error, LocalStore};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct MemoryLocalStore {
|
||||
keysets: Arc<Mutex<HashMap<Id, KeySet>>>,
|
||||
mint_quotes: Arc<Mutex<HashMap<String, MintQuote>>>,
|
||||
melt_quotes: Arc<Mutex<HashMap<String, MeltQuote>>>,
|
||||
pending_proofs: Arc<Mutex<HashMap<Secret, Proof>>>,
|
||||
spent_proofs: Arc<Mutex<HashMap<Secret, Proof>>>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LocalStore for MemoryLocalStore {
|
||||
async fn add_keyset(&self, keyset: KeySet) -> Result<(), Error> {
|
||||
self.keysets.lock().await.insert(keyset.id, keyset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_keyset(&self, keyset_id: &Id) -> Result<Option<KeySet>, Error> {
|
||||
Ok(self.keysets.lock().await.get(keyset_id).cloned())
|
||||
}
|
||||
|
||||
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error> {
|
||||
self.mint_quotes
|
||||
.lock()
|
||||
.await
|
||||
.insert(quote.id.clone(), quote);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Error> {
|
||||
Ok(self.mint_quotes.lock().await.get(quote_id).cloned())
|
||||
}
|
||||
|
||||
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
|
||||
self.mint_quotes.lock().await.remove(quote_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
|
||||
self.melt_quotes
|
||||
.lock()
|
||||
.await
|
||||
.insert(quote.id.clone(), quote);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Error> {
|
||||
Ok(self.melt_quotes.lock().await.get(quote_id).cloned())
|
||||
}
|
||||
|
||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> {
|
||||
self.melt_quotes.lock().await.remove(quote_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_spent_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error> {
|
||||
self.spent_proofs.lock().await.insert(secret, proof);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_spent_proof(&self, secret: &Secret) -> Result<Option<Proof>, Error> {
|
||||
Ok(self.spent_proofs.lock().await.get(secret).cloned())
|
||||
}
|
||||
|
||||
async fn add_pending_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error> {
|
||||
self.pending_proofs.lock().await.insert(secret, proof);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_pending_proof(&self, secret: &Secret) -> Result<Option<Proof>, Error> {
|
||||
Ok(self.pending_proofs.lock().await.get(secret).cloned())
|
||||
}
|
||||
|
||||
async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Error> {
|
||||
self.pending_proofs.lock().await.remove(secret);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
54
crates/cashu-sdk/src/mint/localstore/mod.rs
Normal file
54
crates/cashu-sdk/src/mint/localstore/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
mod memory;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cashu::nuts::nut02::mint::KeySet;
|
||||
use cashu::nuts::{Id, Proof};
|
||||
use cashu::secret::Secret;
|
||||
use cashu::types::{MeltQuote, MintQuote};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Redb(#[from] redb::Error),
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Database(#[from] redb::DatabaseError),
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Transaction(#[from] redb::TransactionError),
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Commit(#[from] redb::CommitError),
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Table(#[from] redb::TableError),
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Storage(#[from] redb::StorageError),
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))]
|
||||
#[error("`{0}`")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait LocalStore {
|
||||
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error>;
|
||||
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Error>;
|
||||
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error>;
|
||||
|
||||
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error>;
|
||||
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Error>;
|
||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error>;
|
||||
|
||||
async fn add_keyset(&self, keyset: KeySet) -> Result<(), Error>;
|
||||
async fn get_keyset(&self, id: &Id) -> Result<Option<KeySet>, Error>;
|
||||
|
||||
async fn add_spent_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error>;
|
||||
async fn get_spent_proof(&self, secret: &Secret) -> Result<Option<Proof>, Error>;
|
||||
|
||||
async fn add_pending_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error>;
|
||||
async fn get_pending_proof(&self, secret: &Secret) -> Result<Option<Proof>, Error>;
|
||||
async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Error>;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use cashu::dhke::{sign_message, verify_message};
|
||||
pub use cashu::error::mint::Error;
|
||||
use cashu::nuts::{
|
||||
BlindedMessage, BlindedSignature, MeltBolt11Request, MeltBolt11Response, Proof, SwapRequest,
|
||||
SwapResponse, *,
|
||||
@@ -11,40 +10,61 @@ use cashu::nuts::{CheckSpendableRequest, CheckSpendableResponse};
|
||||
use cashu::secret::Secret;
|
||||
use cashu::Amount;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::types::MeltQuote;
|
||||
use crate::utils::unix_time;
|
||||
use crate::Mnemonic;
|
||||
|
||||
pub struct Mint {
|
||||
mod localstore;
|
||||
|
||||
use localstore::LocalStore;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Unknown Keyset
|
||||
#[error("Unknown Keyset")]
|
||||
UnknownKeySet,
|
||||
/// Inactive Keyset
|
||||
#[error("Inactive Keyset")]
|
||||
InactiveKeyset,
|
||||
#[error("No key for amount")]
|
||||
AmountKey,
|
||||
#[error("Amount")]
|
||||
Amount,
|
||||
#[error("Duplicate proofs")]
|
||||
DuplicateProofs,
|
||||
#[error("Token Spent")]
|
||||
TokenSpent,
|
||||
#[error("`{0}`")]
|
||||
Custom(String),
|
||||
#[error("`{0}`")]
|
||||
Cashu(#[from] cashu::error::mint::Error),
|
||||
#[error("`{0}`")]
|
||||
Localstore(#[from] localstore::Error),
|
||||
}
|
||||
|
||||
pub struct Mint<L: LocalStore> {
|
||||
// pub pubkey: PublicKey
|
||||
pub keysets: HashMap<Id, nut02::mint::KeySet>,
|
||||
pub keysets_info: HashMap<Id, MintKeySetInfo>,
|
||||
// pub pubkey: PublicKey,
|
||||
mnemonic: Mnemonic,
|
||||
pub spent_secrets: HashSet<Secret>,
|
||||
pub pending_secrets: HashSet<Secret>,
|
||||
pub fee_reserve: FeeReserve,
|
||||
pub melt_quotes: HashMap<String, MeltQuote>,
|
||||
localstore: L,
|
||||
}
|
||||
|
||||
impl Mint {
|
||||
pub fn new(
|
||||
impl<L: LocalStore> Mint<L> {
|
||||
pub async fn new(
|
||||
localstore: L,
|
||||
mnemonic: Mnemonic,
|
||||
keysets_info: HashSet<MintKeySetInfo>,
|
||||
spent_secrets: HashSet<Secret>,
|
||||
melt_quotes: Vec<MeltQuote>,
|
||||
min_fee_reserve: Amount,
|
||||
percent_fee_reserve: f32,
|
||||
) -> Self {
|
||||
let mut keysets = HashMap::default();
|
||||
) -> Result<Self, Error> {
|
||||
let mut info = HashMap::default();
|
||||
|
||||
let mut active_units: HashSet<CurrencyUnit> = HashSet::default();
|
||||
|
||||
let melt_quotes = melt_quotes.into_iter().map(|q| (q.id.clone(), q)).collect();
|
||||
|
||||
// Check that there is only one active keyset per unit
|
||||
for keyset_info in keysets_info {
|
||||
if keyset_info.active && !active_units.insert(keyset_info.unit.clone()) {
|
||||
@@ -59,38 +79,35 @@ impl Mint {
|
||||
keyset_info.max_order,
|
||||
);
|
||||
|
||||
keysets.insert(keyset.id, keyset);
|
||||
|
||||
info.insert(keyset_info.id, keyset_info);
|
||||
|
||||
localstore.add_keyset(keyset).await?;
|
||||
}
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
localstore,
|
||||
mnemonic,
|
||||
keysets,
|
||||
melt_quotes,
|
||||
keysets_info: info,
|
||||
spent_secrets,
|
||||
pending_secrets: HashSet::new(),
|
||||
fee_reserve: FeeReserve {
|
||||
min_fee_reserve,
|
||||
percent_fee_reserve,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve the public keys of the active keyset for distribution to
|
||||
/// wallet clients
|
||||
pub fn keyset_pubkeys(&self, keyset_id: &Id) -> Option<KeysResponse> {
|
||||
let keyset = match self.keysets.get(keyset_id) {
|
||||
pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<Option<KeysResponse>, Error> {
|
||||
let keyset = match self.localstore.get_keyset(keyset_id).await? {
|
||||
Some(keyset) => keyset.clone(),
|
||||
None => {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
Some(KeysResponse {
|
||||
Ok(Some(KeysResponse {
|
||||
keysets: vec![keyset.into()],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return a list of all supported keysets
|
||||
@@ -104,13 +121,22 @@ impl Mint {
|
||||
KeysetResponse { keysets }
|
||||
}
|
||||
|
||||
pub fn keyset(&self, id: &Id) -> Option<KeySet> {
|
||||
self.keysets.get(id).map(|ks| ks.clone().into())
|
||||
pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> {
|
||||
Ok(self
|
||||
.localstore
|
||||
.get_keyset(id)
|
||||
.await?
|
||||
.map(|ks| ks.clone().into()))
|
||||
}
|
||||
|
||||
/// Add current keyset to inactive keysets
|
||||
/// Generate new keyset
|
||||
pub fn rotate_keyset(&mut self, unit: CurrencyUnit, derivation_path: &str, max_order: u8) {
|
||||
pub async fn rotate_keyset(
|
||||
&mut self,
|
||||
unit: CurrencyUnit,
|
||||
derivation_path: &str,
|
||||
max_order: u8,
|
||||
) -> Result<(), Error> {
|
||||
let new_keyset = MintKeySet::generate(
|
||||
&self.mnemonic.to_seed_normalized(""),
|
||||
unit.clone(),
|
||||
@@ -118,7 +144,7 @@ impl Mint {
|
||||
max_order,
|
||||
);
|
||||
|
||||
self.keysets.insert(new_keyset.id, new_keyset.clone());
|
||||
self.localstore.add_keyset(new_keyset.clone()).await?;
|
||||
|
||||
for mint_keyset_info in self.keysets_info.values_mut() {
|
||||
if mint_keyset_info.active && mint_keyset_info.unit.eq(&unit) {
|
||||
@@ -137,16 +163,17 @@ impl Mint {
|
||||
};
|
||||
|
||||
self.keysets_info.insert(new_keyset.id, mint_keyset_info);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_mint_request(
|
||||
pub async fn process_mint_request(
|
||||
&mut self,
|
||||
mint_request: nut04::MintBolt11Request,
|
||||
) -> Result<nut04::MintBolt11Response, Error> {
|
||||
let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
|
||||
|
||||
for blinded_message in mint_request.outputs {
|
||||
blind_signatures.push(self.blind_sign(&blinded_message)?);
|
||||
blind_signatures.push(self.blind_sign(&blinded_message).await?);
|
||||
}
|
||||
|
||||
Ok(nut04::MintBolt11Response {
|
||||
@@ -154,14 +181,21 @@ impl Mint {
|
||||
})
|
||||
}
|
||||
|
||||
fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result<BlindedSignature, Error> {
|
||||
async fn blind_sign(
|
||||
&self,
|
||||
blinded_message: &BlindedMessage,
|
||||
) -> Result<BlindedSignature, Error> {
|
||||
let BlindedMessage {
|
||||
amount,
|
||||
b,
|
||||
keyset_id,
|
||||
} = blinded_message;
|
||||
|
||||
let keyset = self.keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?;
|
||||
let keyset = self
|
||||
.localstore
|
||||
.get_keyset(keyset_id)
|
||||
.await?
|
||||
.ok_or(Error::UnknownKeySet)?;
|
||||
|
||||
// Check that the keyset is active and should be used to sign
|
||||
if !self
|
||||
@@ -187,7 +221,7 @@ impl Mint {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_swap_request(
|
||||
pub async fn process_swap_request(
|
||||
&mut self,
|
||||
swap_request: SwapRequest,
|
||||
) -> Result<SwapResponse, Error> {
|
||||
@@ -213,30 +247,41 @@ impl Mint {
|
||||
}
|
||||
|
||||
for proof in &swap_request.inputs {
|
||||
self.verify_proof(proof)?
|
||||
self.verify_proof(proof).await?
|
||||
}
|
||||
|
||||
for secret in secrets {
|
||||
self.spent_secrets.insert(secret);
|
||||
for (secret, proof) in secrets.iter().zip(swap_request.inputs) {
|
||||
self.localstore
|
||||
.add_spent_proof(secret.clone(), proof)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let promises: Vec<BlindedSignature> = swap_request
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|b| self.blind_sign(b).unwrap())
|
||||
.collect();
|
||||
let mut promises = Vec::with_capacity(swap_request.outputs.len());
|
||||
|
||||
for output in swap_request.outputs {
|
||||
let promise = self.blind_sign(&output).await?;
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
Ok(SwapResponse::new(promises))
|
||||
}
|
||||
|
||||
fn verify_proof(&self, proof: &Proof) -> Result<(), Error> {
|
||||
if self.spent_secrets.contains(&proof.secret) {
|
||||
async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> {
|
||||
if self
|
||||
.localstore
|
||||
.get_spent_proof(&proof.secret)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::TokenSpent);
|
||||
}
|
||||
|
||||
let keyset = self
|
||||
.keysets
|
||||
.get(&proof.keyset_id)
|
||||
.localstore
|
||||
.get_keyset(&proof.keyset_id)
|
||||
.await?
|
||||
.ok_or(Error::UnknownKeySet)?;
|
||||
|
||||
let Some(keypair) = keyset.keys.0.get(&proof.amount) else {
|
||||
@@ -253,7 +298,7 @@ impl Mint {
|
||||
}
|
||||
|
||||
#[cfg(feature = "nut07")]
|
||||
pub fn check_spendable(
|
||||
pub async fn check_spendable(
|
||||
&self,
|
||||
check_spendable: &CheckSpendableRequest,
|
||||
) -> Result<CheckSpendableResponse, Error> {
|
||||
@@ -261,15 +306,40 @@ impl Mint {
|
||||
let mut pending = Vec::with_capacity(check_spendable.proofs.len());
|
||||
|
||||
for proof in &check_spendable.proofs {
|
||||
spendable.push(!self.spent_secrets.contains(&proof.secret));
|
||||
pending.push(self.pending_secrets.contains(&proof.secret));
|
||||
spendable.push(
|
||||
self.localstore
|
||||
.get_spent_proof(&proof.secret)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
);
|
||||
pending.push(
|
||||
self.localstore
|
||||
.get_pending_proof(&proof.secret)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(CheckSpendableResponse { spendable, pending })
|
||||
}
|
||||
|
||||
pub fn verify_melt_request(&mut self, melt_request: &MeltBolt11Request) -> Result<(), Error> {
|
||||
let quote = self.melt_quotes.get(&melt_request.quote).unwrap();
|
||||
pub async fn verify_melt_request(
|
||||
&mut self,
|
||||
melt_request: &MeltBolt11Request,
|
||||
) -> Result<(), Error> {
|
||||
let quote = self
|
||||
.localstore
|
||||
.get_melt_quote(&melt_request.quote)
|
||||
.await
|
||||
.unwrap();
|
||||
let quote = if let Some(quote) = quote {
|
||||
quote
|
||||
} else {
|
||||
return Err(Error::Custom("Unknown Quote".to_string()));
|
||||
};
|
||||
|
||||
let proofs_total = melt_request.proofs_amount();
|
||||
|
||||
let required_total = quote.amount + quote.fee_reserve;
|
||||
@@ -290,23 +360,25 @@ impl Mint {
|
||||
}
|
||||
|
||||
for proof in &melt_request.inputs {
|
||||
self.verify_proof(proof)?
|
||||
self.verify_proof(proof).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_melt_request(
|
||||
pub async fn process_melt_request(
|
||||
&mut self,
|
||||
melt_request: &MeltBolt11Request,
|
||||
preimage: &str,
|
||||
total_spent: Amount,
|
||||
) -> Result<MeltBolt11Response, Error> {
|
||||
self.verify_melt_request(melt_request)?;
|
||||
self.verify_melt_request(melt_request).await?;
|
||||
|
||||
let secrets = Vec::with_capacity(melt_request.inputs.len());
|
||||
for secret in secrets {
|
||||
self.spent_secrets.insert(secret);
|
||||
for input in &melt_request.inputs {
|
||||
self.localstore
|
||||
.add_spent_proof(input.secret.clone(), input.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut change = None;
|
||||
@@ -333,7 +405,7 @@ impl Mint {
|
||||
let mut blinded_message = blinded_message;
|
||||
blinded_message.amount = *amount;
|
||||
|
||||
let signature = self.blind_sign(&blinded_message)?;
|
||||
let signature = self.blind_sign(&blinded_message).await?;
|
||||
change_sigs.push(signature)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user