|
@@ -1,5 +1,5 @@
|
|
|
//! Cashu Wallet
|
|
|
-use std::collections::HashMap;
|
|
|
+use std::collections::{HashMap, HashSet};
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
use bip39::Mnemonic;
|
|
@@ -7,8 +7,8 @@ use cashu::dhke::{construct_proofs, unblind_message};
|
|
|
#[cfg(feature = "nut07")]
|
|
|
use cashu::nuts::nut00::mint;
|
|
|
use cashu::nuts::{
|
|
|
- BlindedSignature, CurrencyUnit, Id, Keys, PreMintSecrets, PreSwap, Proof, Proofs, SwapRequest,
|
|
|
- Token,
|
|
|
+ BlindedSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, PreSwap, Proof,
|
|
|
+ Proofs, SwapRequest, Token,
|
|
|
};
|
|
|
#[cfg(feature = "nut07")]
|
|
|
use cashu::types::ProofsStatus;
|
|
@@ -52,26 +52,29 @@ pub struct BackupInfo {
|
|
|
pub struct Wallet<C: Client> {
|
|
|
backup_info: Option<BackupInfo>,
|
|
|
pub client: C,
|
|
|
- pub mint_url: UncheckedUrl,
|
|
|
+ pub mints: HashMap<UncheckedUrl, MintInfo>,
|
|
|
+ pub mint_keysets: HashMap<UncheckedUrl, HashSet<KeySetInfo>>,
|
|
|
pub mint_quotes: HashMap<String, MintQuote>,
|
|
|
pub melt_quotes: HashMap<String, MeltQuote>,
|
|
|
- pub mint_keys: Keys,
|
|
|
+ pub mint_keys: HashMap<Id, Keys>,
|
|
|
pub balance: Amount,
|
|
|
}
|
|
|
|
|
|
impl<C: Client> Wallet<C> {
|
|
|
pub fn new(
|
|
|
client: C,
|
|
|
- mint_url: UncheckedUrl,
|
|
|
+ mints: HashMap<UncheckedUrl, MintInfo>,
|
|
|
+ mint_keysets: HashMap<UncheckedUrl, HashSet<KeySetInfo>>,
|
|
|
mint_quotes: Vec<MintQuote>,
|
|
|
melt_quotes: Vec<MeltQuote>,
|
|
|
backup_info: Option<BackupInfo>,
|
|
|
- mint_keys: Keys,
|
|
|
+ mint_keys: HashMap<Id, Keys>,
|
|
|
) -> Self {
|
|
|
Self {
|
|
|
backup_info,
|
|
|
client,
|
|
|
- mint_url,
|
|
|
+ mints,
|
|
|
+ mint_keysets,
|
|
|
mint_keys,
|
|
|
mint_quotes: mint_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(),
|
|
|
melt_quotes: melt_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(),
|
|
@@ -81,11 +84,15 @@ impl<C: Client> Wallet<C> {
|
|
|
|
|
|
/// Check if a proof is spent
|
|
|
#[cfg(feature = "nut07")]
|
|
|
- pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<ProofsStatus, Error> {
|
|
|
+ pub async fn check_proofs_spent(
|
|
|
+ &self,
|
|
|
+ mint_url: UncheckedUrl,
|
|
|
+ proofs: Proofs,
|
|
|
+ ) -> Result<ProofsStatus, Error> {
|
|
|
let spendable = self
|
|
|
.client
|
|
|
.post_check_spendable(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
+ mint_url.try_into()?,
|
|
|
proofs
|
|
|
.clone()
|
|
|
.into_iter()
|
|
@@ -110,33 +117,36 @@ impl<C: Client> Wallet<C> {
|
|
|
// Mint a token
|
|
|
pub async fn mint_token(
|
|
|
&mut self,
|
|
|
+ mint_url: UncheckedUrl,
|
|
|
amount: Amount,
|
|
|
memo: Option<String>,
|
|
|
unit: Option<CurrencyUnit>,
|
|
|
) -> Result<Token, Error> {
|
|
|
let quote = self
|
|
|
.mint_quote(
|
|
|
+ mint_url.clone(),
|
|
|
amount,
|
|
|
unit.clone()
|
|
|
.ok_or(Error::Custom("Unit required".to_string()))?,
|
|
|
)
|
|
|
.await?;
|
|
|
|
|
|
- let proofs = self.mint("e.id).await?;
|
|
|
+ let proofs = self.mint(mint_url.clone(), "e.id).await?;
|
|
|
|
|
|
- let token = Token::new(self.mint_url.clone(), proofs, memo, unit);
|
|
|
+ let token = Token::new(mint_url.clone(), proofs, memo, unit);
|
|
|
Ok(token?)
|
|
|
}
|
|
|
|
|
|
/// Mint Quote
|
|
|
pub async fn mint_quote(
|
|
|
&mut self,
|
|
|
+ mint_url: UncheckedUrl,
|
|
|
amount: Amount,
|
|
|
unit: CurrencyUnit,
|
|
|
) -> Result<MintQuote, Error> {
|
|
|
let quote_res = self
|
|
|
.client
|
|
|
- .post_mint_quote(self.mint_url.clone().try_into()?, amount, unit.clone())
|
|
|
+ .post_mint_quote(mint_url.try_into()?, amount, unit.clone())
|
|
|
.await?;
|
|
|
|
|
|
let quote = MintQuote {
|
|
@@ -154,8 +164,27 @@ impl<C: Client> Wallet<C> {
|
|
|
Ok(quote)
|
|
|
}
|
|
|
|
|
|
+ fn active_mint_keyset(&self, mint_url: &UncheckedUrl, unit: &CurrencyUnit) -> Option<Id> {
|
|
|
+ if let Some(keysets) = self.mint_keysets.get(mint_url) {
|
|
|
+ for keyset in keysets {
|
|
|
+ if keyset.unit.eq(unit) && keyset.active {
|
|
|
+ return Some(keyset.id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+
|
|
|
+ fn active_keys(&self, mint_url: &UncheckedUrl, unit: &CurrencyUnit) -> Option<Keys> {
|
|
|
+ self.active_mint_keyset(mint_url, unit)
|
|
|
+ .map(|id| self.mint_keys.get(&id))
|
|
|
+ .flatten()
|
|
|
+ .cloned()
|
|
|
+ }
|
|
|
+
|
|
|
/// Mint
|
|
|
- pub async fn mint(&mut self, quote_id: &str) -> Result<Proofs, Error> {
|
|
|
+ pub async fn mint(&mut self, mint_url: UncheckedUrl, quote_id: &str) -> Result<Proofs, Error> {
|
|
|
let quote_info = self.mint_quotes.get(quote_id);
|
|
|
|
|
|
let quote_info = if let Some(quote) = quote_info {
|
|
@@ -168,33 +197,36 @@ impl<C: Client> Wallet<C> {
|
|
|
return Err(Error::QuoteUnknown);
|
|
|
};
|
|
|
|
|
|
+ let active_keyset_id = self
|
|
|
+ .active_mint_keyset(&mint_url, "e_info.unit)
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
let premint_secrets = match &self.backup_info {
|
|
|
Some(backup_info) => PreMintSecrets::from_seed(
|
|
|
- Id::from(&self.mint_keys),
|
|
|
- *backup_info
|
|
|
- .counter
|
|
|
- .get(&Id::from(&self.mint_keys))
|
|
|
- .unwrap_or(&0),
|
|
|
+ active_keyset_id,
|
|
|
+ *backup_info.counter.get(&active_keyset_id).unwrap_or(&0),
|
|
|
&backup_info.mnemonic,
|
|
|
quote_info.amount,
|
|
|
)?,
|
|
|
- None => PreMintSecrets::random((&self.mint_keys).into(), quote_info.amount)?,
|
|
|
+ None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?,
|
|
|
};
|
|
|
|
|
|
let mint_res = self
|
|
|
.client
|
|
|
.post_mint(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
+ mint_url.clone().try_into()?,
|
|
|
quote_id,
|
|
|
premint_secrets.clone(),
|
|
|
)
|
|
|
.await?;
|
|
|
|
|
|
+ let keys = self.mint_keys.get(&active_keyset_id).unwrap();
|
|
|
+
|
|
|
let proofs = construct_proofs(
|
|
|
mint_res.signatures,
|
|
|
premint_secrets.rs(),
|
|
|
premint_secrets.secrets(),
|
|
|
- &self.mint_keys,
|
|
|
+ keys,
|
|
|
)?;
|
|
|
|
|
|
self.mint_quotes.remove("e_info.id);
|
|
@@ -206,26 +238,35 @@ impl<C: Client> Wallet<C> {
|
|
|
pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> {
|
|
|
let token_data = Token::from_str(encoded_token)?;
|
|
|
|
|
|
+ let unit = token_data.unit.unwrap_or_default();
|
|
|
+
|
|
|
let mut proofs: Vec<Proofs> = vec![vec![]];
|
|
|
for token in token_data.token {
|
|
|
if token.proofs.is_empty() {
|
|
|
continue;
|
|
|
}
|
|
|
+ /*
|
|
|
+ let keys = if token.mint.to_string().eq(&self.mint_url.to_string()) {
|
|
|
+ self.mint_keys.clone()
|
|
|
+ } else {
|
|
|
+ self.client.get_mint_keys(token.mint.try_into()?).await?
|
|
|
+ };
|
|
|
+ */
|
|
|
|
|
|
- let keys = if token.mint.to_string().eq(&self.mint_url.to_string()) {
|
|
|
- self.mint_keys.clone()
|
|
|
- } else {
|
|
|
- self.client.get_mint_keys(token.mint.try_into()?).await?
|
|
|
- };
|
|
|
+ let active_keyset_id = self.active_mint_keyset(&token.mint, &unit);
|
|
|
+
|
|
|
+ // TODO: if none fetch keyset for mint
|
|
|
+
|
|
|
+ let keys = self.mint_keys.get(&active_keyset_id.unwrap());
|
|
|
|
|
|
// Sum amount of all proofs
|
|
|
- let _amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
|
|
|
+ let amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
|
|
|
|
|
|
- let pre_swap = self.create_split(None, token.proofs)?;
|
|
|
+ let pre_swap = self.create_split(&token.mint, &unit, Some(amount), token.proofs)?;
|
|
|
|
|
|
let swap_response = self
|
|
|
.client
|
|
|
- .post_split(self.mint_url.clone().try_into()?, pre_swap.split_request)
|
|
|
+ .post_split(token.mint.clone().try_into()?, pre_swap.split_request)
|
|
|
.await?;
|
|
|
|
|
|
// Proof to keep
|
|
@@ -233,7 +274,7 @@ impl<C: Client> Wallet<C> {
|
|
|
swap_response.signatures,
|
|
|
pre_swap.pre_mint_secrets.rs(),
|
|
|
pre_swap.pre_mint_secrets.secrets(),
|
|
|
- &keys,
|
|
|
+ &keys.unwrap(),
|
|
|
)?;
|
|
|
proofs.push(p);
|
|
|
}
|
|
@@ -241,16 +282,24 @@ impl<C: Client> Wallet<C> {
|
|
|
}
|
|
|
|
|
|
/// Create Split Payload
|
|
|
- fn create_split(&self, amount: Option<Amount>, proofs: Proofs) -> Result<PreSwap, Error> {
|
|
|
+ fn create_split(
|
|
|
+ &self,
|
|
|
+ mint_url: &UncheckedUrl,
|
|
|
+ unit: &CurrencyUnit,
|
|
|
+ amount: Option<Amount>,
|
|
|
+ proofs: Proofs,
|
|
|
+ ) -> Result<PreSwap, Error> {
|
|
|
// Since split is used to get the needed combination of tokens for a specific
|
|
|
// amount first blinded messages are created for the amount
|
|
|
|
|
|
+ let active_keyset_id = self.active_mint_keyset(mint_url, unit).unwrap();
|
|
|
+
|
|
|
let pre_mint_secrets = if let Some(amount) = amount {
|
|
|
- let mut desired_messages = PreMintSecrets::random((&self.mint_keys).into(), amount)?;
|
|
|
+ let mut desired_messages = PreMintSecrets::random(active_keyset_id, amount)?;
|
|
|
|
|
|
let change_amount = proofs.iter().map(|p| p.amount).sum::<Amount>() - amount;
|
|
|
|
|
|
- let change_messages = PreMintSecrets::random((&self.mint_keys).into(), change_amount)?;
|
|
|
+ let change_messages = PreMintSecrets::random(active_keyset_id, change_amount)?;
|
|
|
// Combine the BlindedMessages totoalling the desired amount with change
|
|
|
desired_messages.combine(change_messages);
|
|
|
// Sort the premint secrets to avoid finger printing
|
|
@@ -259,7 +308,7 @@ impl<C: Client> Wallet<C> {
|
|
|
} else {
|
|
|
let value = proofs.iter().map(|p| p.amount).sum();
|
|
|
|
|
|
- PreMintSecrets::random((&self.mint_keys).into(), value)?
|
|
|
+ PreMintSecrets::random(active_keyset_id, value)?
|
|
|
};
|
|
|
|
|
|
let split_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages());
|
|
@@ -280,6 +329,8 @@ impl<C: Client> Wallet<C> {
|
|
|
for (promise, premint) in promises.iter().zip(blinded_messages) {
|
|
|
let a = self
|
|
|
.mint_keys
|
|
|
+ .get(&promise.keyset_id)
|
|
|
+ .unwrap()
|
|
|
.amount_key(promise.amount)
|
|
|
.unwrap()
|
|
|
.to_owned();
|
|
@@ -301,7 +352,13 @@ impl<C: Client> Wallet<C> {
|
|
|
}
|
|
|
|
|
|
/// Send
|
|
|
- pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result<SendProofs, Error> {
|
|
|
+ pub async fn send(
|
|
|
+ &self,
|
|
|
+ mint_url: &UncheckedUrl,
|
|
|
+ unit: &CurrencyUnit,
|
|
|
+ amount: Amount,
|
|
|
+ proofs: Proofs,
|
|
|
+ ) -> Result<SendProofs, Error> {
|
|
|
let amount_available: Amount = proofs.iter().map(|p| p.amount).sum();
|
|
|
|
|
|
if amount_available.lt(&amount) {
|
|
@@ -309,11 +366,11 @@ impl<C: Client> Wallet<C> {
|
|
|
return Err(Error::InsufficientFunds);
|
|
|
}
|
|
|
|
|
|
- let pre_swap = self.create_split(Some(amount), proofs)?;
|
|
|
+ let pre_swap = self.create_split(mint_url, unit, Some(amount), proofs)?;
|
|
|
|
|
|
let swap_response = self
|
|
|
.client
|
|
|
- .post_split(self.mint_url.clone().try_into()?, pre_swap.split_request)
|
|
|
+ .post_split(mint_url.clone().try_into()?, pre_swap.split_request)
|
|
|
.await?;
|
|
|
|
|
|
let mut keep_proofs = Proofs::new();
|
|
@@ -323,7 +380,7 @@ impl<C: Client> Wallet<C> {
|
|
|
swap_response.signatures,
|
|
|
pre_swap.pre_mint_secrets.rs(),
|
|
|
pre_swap.pre_mint_secrets.secrets(),
|
|
|
- &self.mint_keys,
|
|
|
+ &self.active_keys(mint_url, unit).unwrap(),
|
|
|
)?;
|
|
|
|
|
|
proofs.reverse();
|
|
@@ -357,16 +414,13 @@ impl<C: Client> Wallet<C> {
|
|
|
/// Melt Quote
|
|
|
pub async fn melt_quote(
|
|
|
&mut self,
|
|
|
+ mint_url: UncheckedUrl,
|
|
|
unit: CurrencyUnit,
|
|
|
request: Bolt11Invoice,
|
|
|
) -> Result<MeltQuote, Error> {
|
|
|
let quote_res = self
|
|
|
.client
|
|
|
- .post_melt_quote(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
- unit.clone(),
|
|
|
- request.clone(),
|
|
|
- )
|
|
|
+ .post_melt_quote(mint_url.clone().try_into()?, unit.clone(), request.clone())
|
|
|
.await?;
|
|
|
|
|
|
let quote = MeltQuote {
|
|
@@ -385,7 +439,12 @@ impl<C: Client> Wallet<C> {
|
|
|
}
|
|
|
|
|
|
/// Melt
|
|
|
- pub async fn melt(&self, quote_id: &str, proofs: Proofs) -> Result<Melted, Error> {
|
|
|
+ pub async fn melt(
|
|
|
+ &self,
|
|
|
+ mint_url: &UncheckedUrl,
|
|
|
+ quote_id: &str,
|
|
|
+ proofs: Proofs,
|
|
|
+ ) -> Result<Melted, Error> {
|
|
|
let quote_info = self.melt_quotes.get(quote_id);
|
|
|
|
|
|
let quote_info = if let Some(quote) = quote_info {
|
|
@@ -398,12 +457,15 @@ impl<C: Client> Wallet<C> {
|
|
|
return Err(Error::QuoteUnknown);
|
|
|
};
|
|
|
|
|
|
- let blinded = PreMintSecrets::blank((&self.mint_keys).into(), quote_info.fee_reserve)?;
|
|
|
+ let blinded = PreMintSecrets::blank(
|
|
|
+ self.active_mint_keyset(mint_url, "e_info.unit).unwrap(),
|
|
|
+ quote_info.fee_reserve,
|
|
|
+ )?;
|
|
|
|
|
|
let melt_response = self
|
|
|
.client
|
|
|
.post_melt(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
+ mint_url.clone().try_into()?,
|
|
|
quote_id.to_string(),
|
|
|
proofs,
|
|
|
Some(blinded.blinded_messages()),
|
|
@@ -415,7 +477,7 @@ impl<C: Client> Wallet<C> {
|
|
|
change,
|
|
|
blinded.rs(),
|
|
|
blinded.secrets(),
|
|
|
- &self.mint_keys,
|
|
|
+ &self.active_keys(mint_url, "e_info.unit).unwrap(),
|
|
|
)?),
|
|
|
None => None,
|
|
|
};
|
|
@@ -431,11 +493,12 @@ impl<C: Client> Wallet<C> {
|
|
|
|
|
|
pub fn proofs_to_token(
|
|
|
&self,
|
|
|
+ mint_url: UncheckedUrl,
|
|
|
proofs: Proofs,
|
|
|
memo: Option<String>,
|
|
|
unit: Option<CurrencyUnit>,
|
|
|
) -> Result<String, Error> {
|
|
|
- Ok(Token::new(self.mint_url.clone(), proofs, memo, unit)?.to_string())
|
|
|
+ Ok(Token::new(mint_url, proofs, memo, unit)?.to_string())
|
|
|
}
|
|
|
}
|
|
|
|