123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- //! MultiMint Wallet
- //!
- //! Wrapper around core [`Wallet`] that enables the use of multiple mint unit
- //! pairs
- use std::collections::{BTreeMap, HashMap};
- use std::fmt;
- use std::str::FromStr;
- use std::sync::Arc;
- use serde::{Deserialize, Serialize};
- use tokio::sync::Mutex;
- use tracing::instrument;
- use super::types::SendKind;
- use super::Error;
- use crate::amount::SplitTarget;
- use crate::mint_url::MintUrl;
- use crate::nuts::{CurrencyUnit, Proof, SecretKey, SpendingConditions, Token};
- use crate::types::Melted;
- use crate::wallet::types::MintQuote;
- use crate::{Amount, Wallet};
- /// Multi Mint Wallet
- #[derive(Debug, Clone)]
- pub struct MultiMintWallet {
- /// Wallets
- pub wallets: Arc<Mutex<BTreeMap<WalletKey, Wallet>>>,
- }
- /// Wallet Key
- #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
- pub struct WalletKey {
- mint_url: MintUrl,
- unit: CurrencyUnit,
- }
- impl fmt::Display for WalletKey {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
- }
- }
- impl WalletKey {
- /// Create new [`WalletKey`]
- pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
- Self { mint_url, unit }
- }
- }
- impl MultiMintWallet {
- /// New Multimint wallet
- pub fn new(wallets: Vec<Wallet>) -> Self {
- Self {
- wallets: Arc::new(Mutex::new(
- wallets
- .into_iter()
- .map(|w| (WalletKey::new(w.mint_url.clone(), w.unit.clone()), w))
- .collect(),
- )),
- }
- }
- /// Add wallet to MultiMintWallet
- #[instrument(skip(self, wallet))]
- pub async fn add_wallet(&self, wallet: Wallet) {
- let wallet_key = WalletKey::new(wallet.mint_url.clone(), wallet.unit.clone());
- let mut wallets = self.wallets.lock().await;
- wallets.insert(wallet_key, wallet);
- }
- /// Remove Wallet from MultiMintWallet
- #[instrument(skip(self))]
- pub async fn remove_wallet(&self, wallet_key: &WalletKey) {
- let mut wallets = self.wallets.lock().await;
- wallets.remove(wallet_key);
- }
- /// Get Wallets from MultiMintWallet
- #[instrument(skip(self))]
- pub async fn get_wallets(&self) -> Vec<Wallet> {
- self.wallets.lock().await.values().cloned().collect()
- }
- /// Get Wallet from MultiMintWallet
- #[instrument(skip(self))]
- pub async fn get_wallet(&self, wallet_key: &WalletKey) -> Option<Wallet> {
- let wallets = self.wallets.lock().await;
- wallets.get(wallet_key).cloned()
- }
- /// Check if mint unit pair is in wallet
- #[instrument(skip(self))]
- pub async fn has(&self, wallet_key: &WalletKey) -> bool {
- self.wallets.lock().await.contains_key(wallet_key)
- }
- /// Get wallet balances
- #[instrument(skip(self))]
- pub async fn get_balances(
- &self,
- unit: &CurrencyUnit,
- ) -> Result<BTreeMap<MintUrl, Amount>, Error> {
- let mut balances = BTreeMap::new();
- for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() {
- if unit == u {
- let wallet_balance = wallet.total_balance().await?;
- balances.insert(mint_url.clone(), wallet_balance);
- }
- }
- Ok(balances)
- }
- /// List proofs.
- #[instrument(skip(self))]
- pub async fn list_proofs(
- &self,
- ) -> Result<BTreeMap<MintUrl, (Vec<Proof>, CurrencyUnit)>, Error> {
- let mut mint_proofs = BTreeMap::new();
- for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() {
- let wallet_proofs = wallet.get_unspent_proofs().await?;
- mint_proofs.insert(mint_url.clone(), (wallet_proofs, u.clone()));
- }
- Ok(mint_proofs)
- }
- /// Create cashu token
- #[instrument(skip(self))]
- pub async fn send(
- &self,
- wallet_key: &WalletKey,
- amount: Amount,
- memo: Option<String>,
- conditions: Option<SpendingConditions>,
- send_kind: SendKind,
- include_fees: bool,
- ) -> Result<Token, Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- wallet
- .send(
- amount,
- memo,
- conditions,
- &SplitTarget::default(),
- &send_kind,
- include_fees,
- )
- .await
- }
- /// Mint quote for wallet
- #[instrument(skip(self))]
- pub async fn mint_quote(
- &self,
- wallet_key: &WalletKey,
- amount: Amount,
- description: Option<String>,
- ) -> Result<MintQuote, Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- wallet.mint_quote(amount, description).await
- }
- /// Check all mint quotes
- /// If quote is paid, wallet will mint
- #[instrument(skip(self))]
- pub async fn check_all_mint_quotes(
- &self,
- wallet_key: Option<WalletKey>,
- ) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
- let mut amount_minted = HashMap::new();
- match wallet_key {
- Some(wallet_key) => {
- let wallet = self
- .get_wallet(&wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- let amount = wallet.check_all_mint_quotes().await?;
- amount_minted.insert(wallet.unit, amount);
- }
- None => {
- for (_, wallet) in self.wallets.lock().await.iter() {
- let amount = wallet.check_all_mint_quotes().await?;
- amount_minted
- .entry(wallet.unit.clone())
- .and_modify(|b| *b += amount)
- .or_insert(amount);
- }
- }
- }
- Ok(amount_minted)
- }
- /// Mint a specific quote
- #[instrument(skip(self))]
- pub async fn mint(
- &self,
- wallet_key: &WalletKey,
- quote_id: &str,
- conditions: Option<SpendingConditions>,
- ) -> Result<Amount, Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- wallet
- .mint(quote_id, SplitTarget::default(), conditions)
- .await
- }
- /// Receive token
- /// Wallet must be already added to multimintwallet
- #[instrument(skip_all)]
- pub async fn receive(
- &self,
- encoded_token: &str,
- p2pk_signing_keys: &[SecretKey],
- preimages: &[String],
- ) -> Result<Amount, Error> {
- let token_data = Token::from_str(encoded_token)?;
- let unit = token_data.unit().unwrap_or_default();
- let proofs = token_data.proofs();
- let mut amount_received = Amount::ZERO;
- let mut mint_errors = None;
- let mint_url = token_data.mint_url()?;
- // Check that all mints in tokes have wallets
- let wallet_key = WalletKey::new(mint_url.clone(), unit.clone());
- if !self.has(&wallet_key).await {
- return Err(Error::UnknownWallet(wallet_key.clone()));
- }
- let wallet_key = WalletKey::new(mint_url.clone(), unit);
- let wallet = self
- .get_wallet(&wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- match wallet
- .receive_proofs(proofs, SplitTarget::default(), p2pk_signing_keys, preimages)
- .await
- {
- Ok(amount) => {
- amount_received += amount;
- }
- Err(err) => {
- tracing::error!("Could no receive proofs for mint: {}", err);
- mint_errors = Some(err);
- }
- }
- match mint_errors {
- None => Ok(amount_received),
- Some(err) => Err(err),
- }
- }
- /// Pay an bolt11 invoice from specific wallet
- #[instrument(skip(self, bolt11))]
- pub async fn pay_invoice_for_wallet(
- &self,
- bolt11: &str,
- wallet_key: &WalletKey,
- max_fee: Option<Amount>,
- ) -> Result<Melted, Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
- if let Some(max_fee) = max_fee {
- if quote.fee_reserve > max_fee {
- return Err(Error::MaxFeeExceeded);
- }
- }
- wallet.melt("e.id).await
- }
- /// Restore
- #[instrument(skip(self))]
- pub async fn restore(&self, wallet_key: &WalletKey) -> Result<Amount, Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- wallet.restore().await
- }
- /// Verify token matches p2pk conditions
- #[instrument(skip(self, token))]
- pub async fn verify_token_p2pk(
- &self,
- wallet_key: &WalletKey,
- token: &Token,
- conditions: SpendingConditions,
- ) -> Result<(), Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- wallet.verify_token_p2pk(token, conditions)
- }
- /// Verifys all proofs in toke have valid dleq proof
- #[instrument(skip(self, token))]
- pub async fn verify_token_dleq(
- &self,
- wallet_key: &WalletKey,
- token: &Token,
- ) -> Result<(), Error> {
- let wallet = self
- .get_wallet(wallet_key)
- .await
- .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
- wallet.verify_token_dleq(token).await
- }
- }
|