|
@@ -1,35 +1,37 @@
|
|
|
#![doc = include_str!("./README.md")]
|
|
|
|
|
|
-use std::collections::{HashMap, HashSet};
|
|
|
+use std::collections::HashMap;
|
|
|
use std::str::FromStr;
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
use bitcoin::bip32::Xpriv;
|
|
|
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
|
|
|
-use bitcoin::hashes::Hash;
|
|
|
-use bitcoin::key::XOnlyPublicKey;
|
|
|
use bitcoin::Network;
|
|
|
use tracing::instrument;
|
|
|
|
|
|
use crate::amount::SplitTarget;
|
|
|
use crate::cdk_database::{self, WalletDatabase};
|
|
|
-use crate::dhke::{construct_proofs, hash_to_curve};
|
|
|
+use crate::dhke::construct_proofs;
|
|
|
use crate::error::Error;
|
|
|
use crate::fees::calculate_fee;
|
|
|
use crate::mint_url::MintUrl;
|
|
|
use crate::nuts::nut00::token::Token;
|
|
|
use crate::nuts::{
|
|
|
- nut10, nut12, Conditions, CurrencyUnit, Id, KeySetInfo, Keys, Kind, MeltQuoteBolt11Response,
|
|
|
- MeltQuoteState, MintInfo, MintQuoteBolt11Response, MintQuoteState, PaymentMethod,
|
|
|
- PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey,
|
|
|
- SigFlag, SpendingConditions, State, SwapRequest,
|
|
|
+ nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, Proofs,
|
|
|
+ RestoreRequest, SpendingConditions, State,
|
|
|
};
|
|
|
-use crate::types::{Melted, ProofInfo};
|
|
|
-use crate::util::{hex, unix_time};
|
|
|
-use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
|
|
|
+use crate::types::ProofInfo;
|
|
|
+use crate::{Amount, HttpClient};
|
|
|
|
|
|
+mod balance;
|
|
|
pub mod client;
|
|
|
+mod keysets;
|
|
|
+mod melt;
|
|
|
+mod mint;
|
|
|
pub mod multi_mint_wallet;
|
|
|
+mod proofs;
|
|
|
+mod receive;
|
|
|
+mod send;
|
|
|
+mod swap;
|
|
|
pub mod types;
|
|
|
pub mod util;
|
|
|
|
|
@@ -141,65 +143,6 @@ impl Wallet {
|
|
|
Ok(Amount::from(fee))
|
|
|
}
|
|
|
|
|
|
- /// Total unspent balance of wallet
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn total_balance(&self) -> Result<Amount, Error> {
|
|
|
- let proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Unspent]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?;
|
|
|
- let balance = Amount::try_sum(proofs.iter().map(|p| p.proof.amount))?;
|
|
|
-
|
|
|
- Ok(balance)
|
|
|
- }
|
|
|
-
|
|
|
- /// Total pending balance
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn total_pending_balance(&self) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
|
|
|
- let proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Pending]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let balances = proofs.iter().fold(HashMap::new(), |mut acc, proof| {
|
|
|
- *acc.entry(proof.unit).or_insert(Amount::ZERO) += proof.proof.amount;
|
|
|
- acc
|
|
|
- });
|
|
|
-
|
|
|
- Ok(balances)
|
|
|
- }
|
|
|
-
|
|
|
- /// Total reserved balance
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn total_reserved_balance(&self) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
|
|
|
- let proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Reserved]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let balances = proofs.iter().fold(HashMap::new(), |mut acc, proof| {
|
|
|
- *acc.entry(proof.unit).or_insert(Amount::ZERO) += proof.proof.amount;
|
|
|
- acc
|
|
|
- });
|
|
|
-
|
|
|
- Ok(balances)
|
|
|
- }
|
|
|
-
|
|
|
/// Update Mint information and related entries in the event a mint changes
|
|
|
/// its URL
|
|
|
#[instrument(skip(self))]
|
|
@@ -214,63 +157,6 @@ impl Wallet {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Get unspent proofs for mint
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn get_proofs(&self) -> Result<Proofs, Error> {
|
|
|
- Ok(self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Unspent]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?
|
|
|
- .into_iter()
|
|
|
- .map(|p| p.proof)
|
|
|
- .collect())
|
|
|
- }
|
|
|
-
|
|
|
- /// Get pending [`Proofs`]
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn get_pending_proofs(&self) -> Result<Proofs, Error> {
|
|
|
- Ok(self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Pending]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?
|
|
|
- .into_iter()
|
|
|
- .map(|p| p.proof)
|
|
|
- .collect())
|
|
|
- }
|
|
|
-
|
|
|
- /// Get reserved [`Proofs`]
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn get_reserved_proofs(&self) -> Result<Proofs, Error> {
|
|
|
- Ok(self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Reserved]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?
|
|
|
- .into_iter()
|
|
|
- .map(|p| p.proof)
|
|
|
- .collect())
|
|
|
- }
|
|
|
-
|
|
|
- /// Return proofs to unspent allowing them to be selected and spent
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn unreserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
|
|
|
- Ok(self.localstore.set_unspent_proofs(ys).await?)
|
|
|
- }
|
|
|
-
|
|
|
/// Qeury mint for current mint information
|
|
|
#[instrument(skip(self))]
|
|
|
pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> {
|
|
@@ -295,450 +181,6 @@ impl Wallet {
|
|
|
Ok(mint_info)
|
|
|
}
|
|
|
|
|
|
- /// Get keys for mint keyset
|
|
|
- ///
|
|
|
- /// Selected keys from localstore if they are already known
|
|
|
- /// If they are not known queries mint for keyset id and stores the [`Keys`]
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn get_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
|
|
|
- let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
|
|
|
- keys
|
|
|
- } else {
|
|
|
- let keys = self
|
|
|
- .client
|
|
|
- .get_mint_keyset(self.mint_url.clone().try_into()?, keyset_id)
|
|
|
- .await?;
|
|
|
-
|
|
|
- self.localstore.add_keys(keys.keys.clone()).await?;
|
|
|
-
|
|
|
- keys.keys
|
|
|
- };
|
|
|
-
|
|
|
- Ok(keys)
|
|
|
- }
|
|
|
-
|
|
|
- /// Get keysets for mint
|
|
|
- ///
|
|
|
- /// Queries mint for all keysets
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
|
|
|
- let keysets = self
|
|
|
- .client
|
|
|
- .get_mint_keysets(self.mint_url.clone().try_into()?)
|
|
|
- .await?;
|
|
|
-
|
|
|
- self.localstore
|
|
|
- .add_mint_keysets(self.mint_url.clone(), keysets.keysets.clone())
|
|
|
- .await?;
|
|
|
-
|
|
|
- Ok(keysets.keysets)
|
|
|
- }
|
|
|
-
|
|
|
- /// Get active keyset for mint
|
|
|
- ///
|
|
|
- /// Queries mint for current keysets then gets [`Keys`] for any unknown
|
|
|
- /// keysets
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn get_active_mint_keyset(&self) -> Result<KeySetInfo, Error> {
|
|
|
- let keysets = self
|
|
|
- .client
|
|
|
- .get_mint_keysets(self.mint_url.clone().try_into()?)
|
|
|
- .await?;
|
|
|
- let keysets = keysets.keysets;
|
|
|
-
|
|
|
- self.localstore
|
|
|
- .add_mint_keysets(self.mint_url.clone(), keysets.clone())
|
|
|
- .await?;
|
|
|
-
|
|
|
- let active_keysets = keysets
|
|
|
- .clone()
|
|
|
- .into_iter()
|
|
|
- .filter(|k| k.active && k.unit == self.unit)
|
|
|
- .collect::<Vec<KeySetInfo>>();
|
|
|
-
|
|
|
- match self
|
|
|
- .localstore
|
|
|
- .get_mint_keysets(self.mint_url.clone())
|
|
|
- .await?
|
|
|
- {
|
|
|
- Some(known_keysets) => {
|
|
|
- let unknown_keysets: Vec<&KeySetInfo> = keysets
|
|
|
- .iter()
|
|
|
- .filter(|k| known_keysets.contains(k))
|
|
|
- .collect();
|
|
|
-
|
|
|
- for keyset in unknown_keysets {
|
|
|
- self.get_keyset_keys(keyset.id).await?;
|
|
|
- }
|
|
|
- }
|
|
|
- None => {
|
|
|
- for keyset in keysets {
|
|
|
- self.get_keyset_keys(keyset.id).await?;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- active_keysets.first().ok_or(Error::NoActiveKeyset).cloned()
|
|
|
- }
|
|
|
-
|
|
|
- /// Reclaim unspent proofs
|
|
|
- ///
|
|
|
- /// Checks the stats of [`Proofs`] swapping for a new [`Proof`] if unspent
|
|
|
- #[instrument(skip(self, proofs))]
|
|
|
- pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> {
|
|
|
- let proof_ys = proofs
|
|
|
- .iter()
|
|
|
- // Find Y for the secret
|
|
|
- .map(|p| hash_to_curve(p.secret.as_bytes()))
|
|
|
- .collect::<Result<Vec<PublicKey>, _>>()?;
|
|
|
-
|
|
|
- let spendable = self
|
|
|
- .client
|
|
|
- .post_check_state(self.mint_url.clone().try_into()?, proof_ys)
|
|
|
- .await?
|
|
|
- .states;
|
|
|
-
|
|
|
- let unspent: Proofs = proofs
|
|
|
- .into_iter()
|
|
|
- .zip(spendable)
|
|
|
- .filter_map(|(p, s)| (s.state == State::Unspent).then_some(p))
|
|
|
- .collect();
|
|
|
-
|
|
|
- self.swap(None, SplitTarget::default(), unspent, None, false)
|
|
|
- .await?;
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// NUT-07 Check the state of a [`Proof`] with the mint
|
|
|
- #[instrument(skip(self, proofs))]
|
|
|
- pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Error> {
|
|
|
- let spendable = self
|
|
|
- .client
|
|
|
- .post_check_state(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
- proofs
|
|
|
- .iter()
|
|
|
- // Find Y for the secret
|
|
|
- .map(|p| hash_to_curve(p.secret.as_bytes()))
|
|
|
- .collect::<Result<Vec<PublicKey>, _>>()?,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- Ok(spendable.states)
|
|
|
- }
|
|
|
-
|
|
|
- /// Checks pending proofs for spent status
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn check_all_pending_proofs(&self) -> Result<Amount, Error> {
|
|
|
- let mut balance = Amount::ZERO;
|
|
|
-
|
|
|
- let proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Pending, State::Reserved]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- if proofs.is_empty() {
|
|
|
- return Ok(Amount::ZERO);
|
|
|
- }
|
|
|
-
|
|
|
- let states = self
|
|
|
- .check_proofs_spent(proofs.clone().into_iter().map(|p| p.proof).collect())
|
|
|
- .await?;
|
|
|
-
|
|
|
- // Both `State::Pending` and `State::Unspent` should be included in the pending
|
|
|
- // table. This is because a proof that has been crated to send will be
|
|
|
- // stored in the pending table in order to avoid accidentally double
|
|
|
- // spending but to allow it to be explicitly reclaimed
|
|
|
- let pending_states: HashSet<PublicKey> = states
|
|
|
- .into_iter()
|
|
|
- .filter(|s| s.state.ne(&State::Spent))
|
|
|
- .map(|s| s.y)
|
|
|
- .collect();
|
|
|
-
|
|
|
- let (pending_proofs, non_pending_proofs): (Vec<ProofInfo>, Vec<ProofInfo>) = proofs
|
|
|
- .into_iter()
|
|
|
- .partition(|p| pending_states.contains(&p.y));
|
|
|
-
|
|
|
- let amount = Amount::try_sum(pending_proofs.iter().map(|p| p.proof.amount))?;
|
|
|
-
|
|
|
- self.localstore
|
|
|
- .update_proofs(
|
|
|
- vec![],
|
|
|
- non_pending_proofs.into_iter().map(|p| p.y).collect(),
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- balance += amount;
|
|
|
-
|
|
|
- Ok(balance)
|
|
|
- }
|
|
|
-
|
|
|
- /// Mint Quote
|
|
|
- /// # Synopsis
|
|
|
- /// ```rust
|
|
|
- /// use std::sync::Arc;
|
|
|
- ///
|
|
|
- /// use cdk::amount::Amount;
|
|
|
- /// use cdk::cdk_database::WalletMemoryDatabase;
|
|
|
- /// use cdk::nuts::CurrencyUnit;
|
|
|
- /// use cdk::wallet::Wallet;
|
|
|
- /// use rand::Rng;
|
|
|
- ///
|
|
|
- /// #[tokio::main]
|
|
|
- /// async fn main() -> anyhow::Result<()> {
|
|
|
- /// let seed = rand::thread_rng().gen::<[u8; 32]>();
|
|
|
- /// let mint_url = "https://testnut.cashu.space";
|
|
|
- /// let unit = CurrencyUnit::Sat;
|
|
|
- ///
|
|
|
- /// let localstore = WalletMemoryDatabase::default();
|
|
|
- /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?;
|
|
|
- /// let amount = Amount::from(100);
|
|
|
- ///
|
|
|
- /// let quote = wallet.mint_quote(amount, None).await?;
|
|
|
- /// Ok(())
|
|
|
- /// }
|
|
|
- /// ```
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn mint_quote(
|
|
|
- &self,
|
|
|
- amount: Amount,
|
|
|
- description: Option<String>,
|
|
|
- ) -> Result<MintQuote, Error> {
|
|
|
- let mint_url = self.mint_url.clone();
|
|
|
- let unit = self.unit;
|
|
|
-
|
|
|
- // If we have a description, we check that the mint supports it.
|
|
|
- // If we have a description, we check that the mint supports it.
|
|
|
- if description.is_some() {
|
|
|
- let mint_method_settings = self
|
|
|
- .localstore
|
|
|
- .get_mint(mint_url.clone())
|
|
|
- .await?
|
|
|
- .ok_or(Error::IncorrectMint)?
|
|
|
- .nuts
|
|
|
- .nut04
|
|
|
- .get_settings(&unit, &PaymentMethod::Bolt11)
|
|
|
- .ok_or(Error::UnsupportedUnit)?;
|
|
|
-
|
|
|
- if !mint_method_settings.description {
|
|
|
- return Err(Error::InvoiceDescriptionUnsupported);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let quote_res = self
|
|
|
- .client
|
|
|
- .post_mint_quote(mint_url.clone().try_into()?, amount, unit, description)
|
|
|
- .await?;
|
|
|
-
|
|
|
- let quote = MintQuote {
|
|
|
- mint_url,
|
|
|
- id: quote_res.quote.clone(),
|
|
|
- amount,
|
|
|
- unit,
|
|
|
- request: quote_res.request,
|
|
|
- state: quote_res.state,
|
|
|
- expiry: quote_res.expiry.unwrap_or(0),
|
|
|
- };
|
|
|
-
|
|
|
- self.localstore.add_mint_quote(quote.clone()).await?;
|
|
|
-
|
|
|
- Ok(quote)
|
|
|
- }
|
|
|
-
|
|
|
- /// Check mint quote status
|
|
|
- #[instrument(skip(self, quote_id))]
|
|
|
- pub async fn mint_quote_state(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> {
|
|
|
- let response = self
|
|
|
- .client
|
|
|
- .get_mint_quote_status(self.mint_url.clone().try_into()?, quote_id)
|
|
|
- .await?;
|
|
|
-
|
|
|
- match self.localstore.get_mint_quote(quote_id).await? {
|
|
|
- Some(quote) => {
|
|
|
- let mut quote = quote;
|
|
|
-
|
|
|
- quote.state = response.state;
|
|
|
- self.localstore.add_mint_quote(quote).await?;
|
|
|
- }
|
|
|
- None => {
|
|
|
- tracing::info!("Quote mint {} unknown", quote_id);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Ok(response)
|
|
|
- }
|
|
|
-
|
|
|
- /// Check status of pending mint quotes
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn check_all_mint_quotes(&self) -> Result<Amount, Error> {
|
|
|
- let mint_quotes = self.localstore.get_mint_quotes().await?;
|
|
|
- let mut total_amount = Amount::ZERO;
|
|
|
-
|
|
|
- for mint_quote in mint_quotes {
|
|
|
- let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
|
|
|
-
|
|
|
- if mint_quote_response.state == MintQuoteState::Paid {
|
|
|
- let amount = self
|
|
|
- .mint(&mint_quote.id, SplitTarget::default(), None)
|
|
|
- .await?;
|
|
|
- total_amount += amount;
|
|
|
- } else if mint_quote.expiry.le(&unix_time()) {
|
|
|
- self.localstore.remove_mint_quote(&mint_quote.id).await?;
|
|
|
- }
|
|
|
- }
|
|
|
- Ok(total_amount)
|
|
|
- }
|
|
|
-
|
|
|
- /// Mint
|
|
|
- /// # Synopsis
|
|
|
- /// ```rust
|
|
|
- /// use std::sync::Arc;
|
|
|
- ///
|
|
|
- /// use anyhow::Result;
|
|
|
- /// use cdk::amount::{Amount, SplitTarget};
|
|
|
- /// use cdk::cdk_database::WalletMemoryDatabase;
|
|
|
- /// use cdk::nuts::CurrencyUnit;
|
|
|
- /// use cdk::wallet::Wallet;
|
|
|
- /// use rand::Rng;
|
|
|
- ///
|
|
|
- /// #[tokio::main]
|
|
|
- /// async fn main() -> Result<()> {
|
|
|
- /// let seed = rand::thread_rng().gen::<[u8; 32]>();
|
|
|
- /// let mint_url = "https://testnut.cashu.space";
|
|
|
- /// let unit = CurrencyUnit::Sat;
|
|
|
- ///
|
|
|
- /// let localstore = WalletMemoryDatabase::default();
|
|
|
- /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
|
|
- /// let amount = Amount::from(100);
|
|
|
- ///
|
|
|
- /// let quote = wallet.mint_quote(amount, None).await?;
|
|
|
- /// let quote_id = quote.id;
|
|
|
- /// // To be called after quote request is paid
|
|
|
- /// let amount_minted = wallet.mint("e_id, SplitTarget::default(), None).await?;
|
|
|
- ///
|
|
|
- /// Ok(())
|
|
|
- /// }
|
|
|
- /// ```
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn mint(
|
|
|
- &self,
|
|
|
- quote_id: &str,
|
|
|
- amount_split_target: SplitTarget,
|
|
|
- spending_conditions: Option<SpendingConditions>,
|
|
|
- ) -> Result<Amount, Error> {
|
|
|
- // Check that mint is in store of mints
|
|
|
- if self
|
|
|
- .localstore
|
|
|
- .get_mint(self.mint_url.clone())
|
|
|
- .await?
|
|
|
- .is_none()
|
|
|
- {
|
|
|
- self.get_mint_info().await?;
|
|
|
- }
|
|
|
-
|
|
|
- let quote_info = self.localstore.get_mint_quote(quote_id).await?;
|
|
|
-
|
|
|
- let quote_info = if let Some(quote) = quote_info {
|
|
|
- if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
|
|
|
- return Err(Error::ExpiredQuote(quote.expiry, unix_time()));
|
|
|
- }
|
|
|
-
|
|
|
- quote.clone()
|
|
|
- } else {
|
|
|
- return Err(Error::UnknownQuote);
|
|
|
- };
|
|
|
-
|
|
|
- let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
|
|
-
|
|
|
- let count = self
|
|
|
- .localstore
|
|
|
- .get_keyset_counter(&active_keyset_id)
|
|
|
- .await?;
|
|
|
-
|
|
|
- let count = count.map_or(0, |c| c + 1);
|
|
|
-
|
|
|
- let premint_secrets = match &spending_conditions {
|
|
|
- Some(spending_conditions) => PreMintSecrets::with_conditions(
|
|
|
- active_keyset_id,
|
|
|
- quote_info.amount,
|
|
|
- &amount_split_target,
|
|
|
- spending_conditions,
|
|
|
- )?,
|
|
|
- None => PreMintSecrets::from_xpriv(
|
|
|
- active_keyset_id,
|
|
|
- count,
|
|
|
- self.xpriv,
|
|
|
- quote_info.amount,
|
|
|
- &amount_split_target,
|
|
|
- )?,
|
|
|
- };
|
|
|
-
|
|
|
- let mint_res = self
|
|
|
- .client
|
|
|
- .post_mint(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
- quote_id,
|
|
|
- premint_secrets.clone(),
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let keys = self.get_keyset_keys(active_keyset_id).await?;
|
|
|
-
|
|
|
- // Verify the signature DLEQ is valid
|
|
|
- {
|
|
|
- for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
|
|
|
- let keys = self.get_keyset_keys(sig.keyset_id).await?;
|
|
|
- let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
|
|
|
- match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
|
|
|
- Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
|
|
|
- Err(_) => return Err(Error::CouldNotVerifyDleq),
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let proofs = construct_proofs(
|
|
|
- mint_res.signatures,
|
|
|
- premint_secrets.rs(),
|
|
|
- premint_secrets.secrets(),
|
|
|
- &keys,
|
|
|
- )?;
|
|
|
-
|
|
|
- let minted_amount = Amount::try_sum(proofs.iter().map(|p| p.amount))?;
|
|
|
-
|
|
|
- // Remove filled quote from store
|
|
|
- self.localstore.remove_mint_quote("e_info.id).await?;
|
|
|
-
|
|
|
- if spending_conditions.is_none() {
|
|
|
- // Update counter for keyset
|
|
|
- self.localstore
|
|
|
- .increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
|
|
|
- .await?;
|
|
|
- }
|
|
|
-
|
|
|
- let proofs = proofs
|
|
|
- .into_iter()
|
|
|
- .map(|proof| {
|
|
|
- ProofInfo::new(
|
|
|
- proof,
|
|
|
- self.mint_url.clone(),
|
|
|
- State::Unspent,
|
|
|
- quote_info.unit,
|
|
|
- )
|
|
|
- })
|
|
|
- .collect::<Result<Vec<ProofInfo>, _>>()?;
|
|
|
-
|
|
|
- // Add new proofs to store
|
|
|
- self.localstore.update_proofs(proofs, vec![]).await?;
|
|
|
-
|
|
|
- Ok(minted_amount)
|
|
|
- }
|
|
|
-
|
|
|
/// Get amounts needed to refill proof state
|
|
|
#[instrument(skip(self))]
|
|
|
pub async fn amounts_needed_for_state_target(&self) -> Result<Vec<Amount>, Error> {
|
|
@@ -794,1101 +236,6 @@ impl Wallet {
|
|
|
Ok(SplitTarget::Values(values))
|
|
|
}
|
|
|
|
|
|
- /// Create Swap Payload
|
|
|
- #[instrument(skip(self, proofs))]
|
|
|
- pub async fn create_swap(
|
|
|
- &self,
|
|
|
- amount: Option<Amount>,
|
|
|
- amount_split_target: SplitTarget,
|
|
|
- proofs: Proofs,
|
|
|
- spending_conditions: Option<SpendingConditions>,
|
|
|
- include_fees: bool,
|
|
|
- ) -> Result<PreSwap, Error> {
|
|
|
- let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
|
|
-
|
|
|
- // Desired amount is either amount passed or value of all proof
|
|
|
- let proofs_total = Amount::try_sum(proofs.iter().map(|p| p.amount))?;
|
|
|
-
|
|
|
- let ys: Vec<PublicKey> = proofs.iter().map(|p| p.y()).collect::<Result<_, _>>()?;
|
|
|
- self.localstore.set_pending_proofs(ys).await?;
|
|
|
-
|
|
|
- let fee = self.get_proofs_fee(&proofs).await?;
|
|
|
-
|
|
|
- let change_amount: Amount = proofs_total - amount.unwrap_or(Amount::ZERO) - fee;
|
|
|
-
|
|
|
- let (send_amount, change_amount) = match include_fees {
|
|
|
- true => {
|
|
|
- let split_count = amount
|
|
|
- .unwrap_or(Amount::ZERO)
|
|
|
- .split_targeted(&SplitTarget::default())
|
|
|
- .unwrap()
|
|
|
- .len();
|
|
|
-
|
|
|
- let fee_to_redeem = self
|
|
|
- .get_keyset_count_fee(&active_keyset_id, split_count as u64)
|
|
|
- .await?;
|
|
|
-
|
|
|
- (
|
|
|
- amount.map(|a| a + fee_to_redeem),
|
|
|
- change_amount - fee_to_redeem,
|
|
|
- )
|
|
|
- }
|
|
|
- false => (amount, change_amount),
|
|
|
- };
|
|
|
-
|
|
|
- // If a non None split target is passed use that
|
|
|
- // else use state refill
|
|
|
- let change_split_target = match amount_split_target {
|
|
|
- SplitTarget::None => self.determine_split_target_values(change_amount).await?,
|
|
|
- s => s,
|
|
|
- };
|
|
|
-
|
|
|
- let derived_secret_count;
|
|
|
-
|
|
|
- let count = self
|
|
|
- .localstore
|
|
|
- .get_keyset_counter(&active_keyset_id)
|
|
|
- .await?;
|
|
|
-
|
|
|
- let mut count = count.map_or(0, |c| c + 1);
|
|
|
-
|
|
|
- let (mut desired_messages, change_messages) = match spending_conditions {
|
|
|
- Some(conditions) => {
|
|
|
- let change_premint_secrets = PreMintSecrets::from_xpriv(
|
|
|
- active_keyset_id,
|
|
|
- count,
|
|
|
- self.xpriv,
|
|
|
- change_amount,
|
|
|
- &change_split_target,
|
|
|
- )?;
|
|
|
-
|
|
|
- derived_secret_count = change_premint_secrets.len();
|
|
|
-
|
|
|
- (
|
|
|
- PreMintSecrets::with_conditions(
|
|
|
- active_keyset_id,
|
|
|
- send_amount.unwrap_or(Amount::ZERO),
|
|
|
- &SplitTarget::default(),
|
|
|
- &conditions,
|
|
|
- )?,
|
|
|
- change_premint_secrets,
|
|
|
- )
|
|
|
- }
|
|
|
- None => {
|
|
|
- let premint_secrets = PreMintSecrets::from_xpriv(
|
|
|
- active_keyset_id,
|
|
|
- count,
|
|
|
- self.xpriv,
|
|
|
- send_amount.unwrap_or(Amount::ZERO),
|
|
|
- &SplitTarget::default(),
|
|
|
- )?;
|
|
|
-
|
|
|
- count += premint_secrets.len() as u32;
|
|
|
-
|
|
|
- let change_premint_secrets = PreMintSecrets::from_xpriv(
|
|
|
- active_keyset_id,
|
|
|
- count,
|
|
|
- self.xpriv,
|
|
|
- change_amount,
|
|
|
- &change_split_target,
|
|
|
- )?;
|
|
|
-
|
|
|
- derived_secret_count = change_premint_secrets.len() + premint_secrets.len();
|
|
|
-
|
|
|
- (premint_secrets, change_premint_secrets)
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // Combine the BlindedMessages totaling the desired amount with change
|
|
|
- desired_messages.combine(change_messages);
|
|
|
- // Sort the premint secrets to avoid finger printing
|
|
|
- desired_messages.sort_secrets();
|
|
|
-
|
|
|
- let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
|
|
|
-
|
|
|
- Ok(PreSwap {
|
|
|
- pre_mint_secrets: desired_messages,
|
|
|
- swap_request,
|
|
|
- derived_secret_count: derived_secret_count as u32,
|
|
|
- fee,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- /// Swap
|
|
|
- #[instrument(skip(self, input_proofs))]
|
|
|
- pub async fn swap(
|
|
|
- &self,
|
|
|
- amount: Option<Amount>,
|
|
|
- amount_split_target: SplitTarget,
|
|
|
- input_proofs: Proofs,
|
|
|
- spending_conditions: Option<SpendingConditions>,
|
|
|
- include_fees: bool,
|
|
|
- ) -> Result<Option<Proofs>, Error> {
|
|
|
- let mint_url = &self.mint_url;
|
|
|
- let unit = &self.unit;
|
|
|
-
|
|
|
- let pre_swap = self
|
|
|
- .create_swap(
|
|
|
- amount,
|
|
|
- amount_split_target,
|
|
|
- input_proofs.clone(),
|
|
|
- spending_conditions.clone(),
|
|
|
- include_fees,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let swap_response = self
|
|
|
- .client
|
|
|
- .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request)
|
|
|
- .await?;
|
|
|
-
|
|
|
- let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;
|
|
|
-
|
|
|
- let active_keys = self
|
|
|
- .localstore
|
|
|
- .get_keys(&active_keyset_id)
|
|
|
- .await?
|
|
|
- .ok_or(Error::NoActiveKeyset)?;
|
|
|
-
|
|
|
- let post_swap_proofs = construct_proofs(
|
|
|
- swap_response.signatures,
|
|
|
- pre_swap.pre_mint_secrets.rs(),
|
|
|
- pre_swap.pre_mint_secrets.secrets(),
|
|
|
- &active_keys,
|
|
|
- )?;
|
|
|
-
|
|
|
- self.localstore
|
|
|
- .increment_keyset_counter(&active_keyset_id, pre_swap.derived_secret_count)
|
|
|
- .await?;
|
|
|
-
|
|
|
- let mut added_proofs = Vec::new();
|
|
|
- let change_proofs;
|
|
|
- let send_proofs;
|
|
|
- match amount {
|
|
|
- Some(amount) => {
|
|
|
- let (proofs_with_condition, proofs_without_condition): (Proofs, Proofs) =
|
|
|
- post_swap_proofs.into_iter().partition(|p| {
|
|
|
- let nut10_secret: Result<nut10::Secret, _> = p.secret.clone().try_into();
|
|
|
-
|
|
|
- nut10_secret.is_ok()
|
|
|
- });
|
|
|
-
|
|
|
- let (proofs_to_send, proofs_to_keep) = match spending_conditions {
|
|
|
- Some(_) => (proofs_with_condition, proofs_without_condition),
|
|
|
- None => {
|
|
|
- let mut all_proofs = proofs_without_condition;
|
|
|
- all_proofs.reverse();
|
|
|
-
|
|
|
- let mut proofs_to_send: Proofs = Vec::new();
|
|
|
- let mut proofs_to_keep = Vec::new();
|
|
|
-
|
|
|
- for proof in all_proofs {
|
|
|
- let proofs_to_send_amount =
|
|
|
- Amount::try_sum(proofs_to_send.iter().map(|p| p.amount))?;
|
|
|
- if proof.amount + proofs_to_send_amount <= amount + pre_swap.fee {
|
|
|
- proofs_to_send.push(proof);
|
|
|
- } else {
|
|
|
- proofs_to_keep.push(proof);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- (proofs_to_send, proofs_to_keep)
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- let send_amount = Amount::try_sum(proofs_to_send.iter().map(|p| p.amount))?;
|
|
|
-
|
|
|
- if send_amount.ne(&(amount + pre_swap.fee)) {
|
|
|
- tracing::warn!(
|
|
|
- "Send amount proofs is {:?} expected {:?}",
|
|
|
- send_amount,
|
|
|
- amount
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- let send_proofs_info = proofs_to_send
|
|
|
- .clone()
|
|
|
- .into_iter()
|
|
|
- .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Reserved, *unit))
|
|
|
- .collect::<Result<Vec<ProofInfo>, _>>()?;
|
|
|
- added_proofs = send_proofs_info;
|
|
|
-
|
|
|
- change_proofs = proofs_to_keep;
|
|
|
- send_proofs = Some(proofs_to_send);
|
|
|
- }
|
|
|
- None => {
|
|
|
- change_proofs = post_swap_proofs;
|
|
|
- send_proofs = None;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let keep_proofs = change_proofs
|
|
|
- .into_iter()
|
|
|
- .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, *unit))
|
|
|
- .collect::<Result<Vec<ProofInfo>, _>>()?;
|
|
|
- added_proofs.extend(keep_proofs);
|
|
|
-
|
|
|
- // Remove spent proofs used as inputs
|
|
|
- let deleted_ys = input_proofs
|
|
|
- .into_iter()
|
|
|
- .map(|proof| proof.y())
|
|
|
- .collect::<Result<Vec<PublicKey>, _>>()?;
|
|
|
-
|
|
|
- self.localstore
|
|
|
- .update_proofs(added_proofs, deleted_ys)
|
|
|
- .await?;
|
|
|
- Ok(send_proofs)
|
|
|
- }
|
|
|
-
|
|
|
- #[instrument(skip(self))]
|
|
|
- async fn swap_from_unspent(
|
|
|
- &self,
|
|
|
- amount: Amount,
|
|
|
- conditions: Option<SpendingConditions>,
|
|
|
- include_fees: bool,
|
|
|
- ) -> Result<Proofs, Error> {
|
|
|
- let available_proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(self.mint_url.clone()),
|
|
|
- Some(self.unit),
|
|
|
- Some(vec![State::Unspent]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let (available_proofs, proofs_sum) = available_proofs.into_iter().map(|p| p.proof).fold(
|
|
|
- (Vec::new(), Amount::ZERO),
|
|
|
- |(mut acc1, mut acc2), p| {
|
|
|
- acc2 += p.amount;
|
|
|
- acc1.push(p);
|
|
|
- (acc1, acc2)
|
|
|
- },
|
|
|
- );
|
|
|
-
|
|
|
- if proofs_sum < amount {
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
-
|
|
|
- let proofs = self.select_proofs_to_swap(amount, available_proofs).await?;
|
|
|
-
|
|
|
- self.swap(
|
|
|
- Some(amount),
|
|
|
- SplitTarget::default(),
|
|
|
- proofs,
|
|
|
- conditions,
|
|
|
- include_fees,
|
|
|
- )
|
|
|
- .await?
|
|
|
- .ok_or(Error::InsufficientFunds)
|
|
|
- }
|
|
|
-
|
|
|
- /// Send specific proofs
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn send_proofs(&self, memo: Option<String>, proofs: Proofs) -> Result<Token, Error> {
|
|
|
- let ys = proofs
|
|
|
- .iter()
|
|
|
- .map(|p| p.y())
|
|
|
- .collect::<Result<Vec<PublicKey>, _>>()?;
|
|
|
- self.localstore.reserve_proofs(ys).await?;
|
|
|
-
|
|
|
- Ok(Token::new(
|
|
|
- self.mint_url.clone(),
|
|
|
- proofs,
|
|
|
- memo,
|
|
|
- Some(self.unit),
|
|
|
- ))
|
|
|
- }
|
|
|
-
|
|
|
- /// Send
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn send(
|
|
|
- &self,
|
|
|
- amount: Amount,
|
|
|
- memo: Option<String>,
|
|
|
- conditions: Option<SpendingConditions>,
|
|
|
- amount_split_target: &SplitTarget,
|
|
|
- send_kind: &SendKind,
|
|
|
- include_fees: bool,
|
|
|
- ) -> Result<Token, Error> {
|
|
|
- // If online send check mint for current keysets fees
|
|
|
- if matches!(
|
|
|
- send_kind,
|
|
|
- SendKind::OnlineExact | SendKind::OnlineTolerance(_)
|
|
|
- ) {
|
|
|
- if let Err(e) = self.get_active_mint_keyset().await {
|
|
|
- tracing::error!(
|
|
|
- "Error fetching active mint keyset: {:?}. Using stored keysets",
|
|
|
- e
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let mint_url = &self.mint_url;
|
|
|
- let unit = &self.unit;
|
|
|
- let available_proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(mint_url.clone()),
|
|
|
- Some(*unit),
|
|
|
- Some(vec![State::Unspent]),
|
|
|
- conditions.clone().map(|c| vec![c]),
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let (available_proofs, proofs_sum) = available_proofs.into_iter().map(|p| p.proof).fold(
|
|
|
- (Vec::new(), Amount::ZERO),
|
|
|
- |(mut acc1, mut acc2), p| {
|
|
|
- acc2 += p.amount;
|
|
|
- acc1.push(p);
|
|
|
- (acc1, acc2)
|
|
|
- },
|
|
|
- );
|
|
|
- let available_proofs = if proofs_sum < amount {
|
|
|
- match &conditions {
|
|
|
- Some(conditions) => {
|
|
|
- let available_proofs = self
|
|
|
- .localstore
|
|
|
- .get_proofs(
|
|
|
- Some(mint_url.clone()),
|
|
|
- Some(*unit),
|
|
|
- Some(vec![State::Unspent]),
|
|
|
- None,
|
|
|
- )
|
|
|
- .await?;
|
|
|
-
|
|
|
- let available_proofs = available_proofs.into_iter().map(|p| p.proof).collect();
|
|
|
-
|
|
|
- let proofs_to_swap =
|
|
|
- self.select_proofs_to_swap(amount, available_proofs).await?;
|
|
|
-
|
|
|
- let proofs_with_conditions = self
|
|
|
- .swap(
|
|
|
- Some(amount),
|
|
|
- SplitTarget::default(),
|
|
|
- proofs_to_swap,
|
|
|
- Some(conditions.clone()),
|
|
|
- include_fees,
|
|
|
- )
|
|
|
- .await?;
|
|
|
- proofs_with_conditions.ok_or(Error::InsufficientFunds)?
|
|
|
- }
|
|
|
- None => {
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- available_proofs
|
|
|
- };
|
|
|
-
|
|
|
- let selected = self
|
|
|
- .select_proofs_to_send(amount, available_proofs, include_fees)
|
|
|
- .await;
|
|
|
-
|
|
|
- let send_proofs: Proofs = match (send_kind, selected, conditions.clone()) {
|
|
|
- // Handle exact matches offline
|
|
|
- (SendKind::OfflineExact, Ok(selected_proofs), _) => {
|
|
|
- let selected_proofs_amount =
|
|
|
- Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
-
|
|
|
- let amount_to_send = match include_fees {
|
|
|
- true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
|
|
- false => amount,
|
|
|
- };
|
|
|
-
|
|
|
- if selected_proofs_amount == amount_to_send {
|
|
|
- selected_proofs
|
|
|
- } else {
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Handle exact matches
|
|
|
- (SendKind::OnlineExact, Ok(selected_proofs), _) => {
|
|
|
- let selected_proofs_amount =
|
|
|
- Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
-
|
|
|
- let amount_to_send = match include_fees {
|
|
|
- true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
|
|
- false => amount,
|
|
|
- };
|
|
|
-
|
|
|
- if selected_proofs_amount == amount_to_send {
|
|
|
- selected_proofs
|
|
|
- } else {
|
|
|
- tracing::info!("Could not select proofs exact while offline.");
|
|
|
- tracing::info!("Attempting to select proofs and swapping");
|
|
|
-
|
|
|
- self.swap_from_unspent(amount, conditions, include_fees)
|
|
|
- .await?
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Handle offline tolerance
|
|
|
- (SendKind::OfflineTolerance(tolerance), Ok(selected_proofs), _) => {
|
|
|
- let selected_proofs_amount =
|
|
|
- Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
-
|
|
|
- let amount_to_send = match include_fees {
|
|
|
- true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
|
|
- false => amount,
|
|
|
- };
|
|
|
- if selected_proofs_amount - amount_to_send <= *tolerance {
|
|
|
- selected_proofs
|
|
|
- } else {
|
|
|
- tracing::info!("Selected proofs greater than tolerance. Must swap online");
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Handle online tolerance when selection fails and conditions are present
|
|
|
- (SendKind::OnlineTolerance(_), Err(_), Some(_)) => {
|
|
|
- tracing::info!("Could not select proofs with conditions while offline.");
|
|
|
- tracing::info!("Attempting to select proofs without conditions and swapping");
|
|
|
-
|
|
|
- self.swap_from_unspent(amount, conditions, include_fees)
|
|
|
- .await?
|
|
|
- }
|
|
|
-
|
|
|
- // Handle online tolerance with successful selection
|
|
|
- (SendKind::OnlineTolerance(tolerance), Ok(selected_proofs), _) => {
|
|
|
- let selected_proofs_amount =
|
|
|
- Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
- let amount_to_send = match include_fees {
|
|
|
- true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
|
|
- false => amount,
|
|
|
- };
|
|
|
- if selected_proofs_amount - amount_to_send <= *tolerance {
|
|
|
- selected_proofs
|
|
|
- } else {
|
|
|
- tracing::info!("Could not select proofs while offline. Attempting swap");
|
|
|
- self.swap_from_unspent(amount, conditions, include_fees)
|
|
|
- .await?
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Handle all other cases where selection fails
|
|
|
- (
|
|
|
- SendKind::OfflineExact
|
|
|
- | SendKind::OnlineExact
|
|
|
- | SendKind::OfflineTolerance(_)
|
|
|
- | SendKind::OnlineTolerance(_),
|
|
|
- Err(_),
|
|
|
- _,
|
|
|
- ) => {
|
|
|
- tracing::debug!("Could not select proofs");
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- self.send_proofs(memo, send_proofs).await
|
|
|
- }
|
|
|
-
|
|
|
- /// Melt Quote
|
|
|
- /// # Synopsis
|
|
|
- /// ```rust
|
|
|
- /// use std::sync::Arc;
|
|
|
- ///
|
|
|
- /// use cdk::cdk_database::WalletMemoryDatabase;
|
|
|
- /// use cdk::nuts::CurrencyUnit;
|
|
|
- /// use cdk::wallet::Wallet;
|
|
|
- /// use rand::Rng;
|
|
|
- ///
|
|
|
- /// #[tokio::main]
|
|
|
- /// async fn main() -> anyhow::Result<()> {
|
|
|
- /// let seed = rand::thread_rng().gen::<[u8; 32]>();
|
|
|
- /// let mint_url = "https://testnut.cashu.space";
|
|
|
- /// let unit = CurrencyUnit::Sat;
|
|
|
- ///
|
|
|
- /// let localstore = WalletMemoryDatabase::default();
|
|
|
- /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
|
|
- /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string();
|
|
|
- /// let quote = wallet.melt_quote(bolt11, None).await?;
|
|
|
- ///
|
|
|
- /// Ok(())
|
|
|
- /// }
|
|
|
- /// ```
|
|
|
- #[instrument(skip(self, request))]
|
|
|
- pub async fn melt_quote(
|
|
|
- &self,
|
|
|
- request: String,
|
|
|
- mpp: Option<Amount>,
|
|
|
- ) -> Result<MeltQuote, Error> {
|
|
|
- let invoice = Bolt11Invoice::from_str(&request)?;
|
|
|
-
|
|
|
- let request_amount = invoice
|
|
|
- .amount_milli_satoshis()
|
|
|
- .ok_or(Error::InvoiceAmountUndefined)?;
|
|
|
-
|
|
|
- let amount = match self.unit {
|
|
|
- CurrencyUnit::Sat => Amount::from(request_amount / 1000),
|
|
|
- CurrencyUnit::Msat => Amount::from(request_amount),
|
|
|
- _ => return Err(Error::UnitUnsupported),
|
|
|
- };
|
|
|
-
|
|
|
- let quote_res = self
|
|
|
- .client
|
|
|
- .post_melt_quote(self.mint_url.clone().try_into()?, self.unit, invoice, mpp)
|
|
|
- .await?;
|
|
|
-
|
|
|
- if quote_res.amount != amount {
|
|
|
- return Err(Error::IncorrectQuoteAmount);
|
|
|
- }
|
|
|
-
|
|
|
- let quote = MeltQuote {
|
|
|
- id: quote_res.quote,
|
|
|
- amount,
|
|
|
- request,
|
|
|
- unit: self.unit,
|
|
|
- fee_reserve: quote_res.fee_reserve,
|
|
|
- state: quote_res.state,
|
|
|
- expiry: quote_res.expiry,
|
|
|
- payment_preimage: quote_res.payment_preimage,
|
|
|
- };
|
|
|
-
|
|
|
- self.localstore.add_melt_quote(quote.clone()).await?;
|
|
|
-
|
|
|
- Ok(quote)
|
|
|
- }
|
|
|
-
|
|
|
- /// Melt quote status
|
|
|
- #[instrument(skip(self, quote_id))]
|
|
|
- pub async fn melt_quote_status(
|
|
|
- &self,
|
|
|
- quote_id: &str,
|
|
|
- ) -> Result<MeltQuoteBolt11Response, Error> {
|
|
|
- let response = self
|
|
|
- .client
|
|
|
- .get_melt_quote_status(self.mint_url.clone().try_into()?, quote_id)
|
|
|
- .await?;
|
|
|
-
|
|
|
- match self.localstore.get_melt_quote(quote_id).await? {
|
|
|
- Some(quote) => {
|
|
|
- let mut quote = quote;
|
|
|
-
|
|
|
- quote.state = response.state;
|
|
|
- self.localstore.add_melt_quote(quote).await?;
|
|
|
- }
|
|
|
- None => {
|
|
|
- tracing::info!("Quote melt {} unknown", quote_id);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Ok(response)
|
|
|
- }
|
|
|
-
|
|
|
- /// Melt specific proofs
|
|
|
- #[instrument(skip(self, proofs))]
|
|
|
- pub async fn melt_proofs(&self, quote_id: &str, proofs: Proofs) -> Result<Melted, Error> {
|
|
|
- let quote_info = self.localstore.get_melt_quote(quote_id).await?;
|
|
|
- let quote_info = if let Some(quote) = quote_info {
|
|
|
- if quote.expiry.le(&unix_time()) {
|
|
|
- return Err(Error::ExpiredQuote(quote.expiry, unix_time()));
|
|
|
- }
|
|
|
-
|
|
|
- quote.clone()
|
|
|
- } else {
|
|
|
- return Err(Error::UnknownQuote);
|
|
|
- };
|
|
|
-
|
|
|
- let proofs_total = Amount::try_sum(proofs.iter().map(|p| p.amount))?;
|
|
|
- if proofs_total < quote_info.amount + quote_info.fee_reserve {
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
-
|
|
|
- let ys = proofs
|
|
|
- .iter()
|
|
|
- .map(|p| p.y())
|
|
|
- .collect::<Result<Vec<PublicKey>, _>>()?;
|
|
|
- self.localstore.set_pending_proofs(ys).await?;
|
|
|
-
|
|
|
- let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
|
|
-
|
|
|
- let count = self
|
|
|
- .localstore
|
|
|
- .get_keyset_counter(&active_keyset_id)
|
|
|
- .await?;
|
|
|
-
|
|
|
- let count = count.map_or(0, |c| c + 1);
|
|
|
-
|
|
|
- let premint_secrets = PreMintSecrets::from_xpriv_blank(
|
|
|
- active_keyset_id,
|
|
|
- count,
|
|
|
- self.xpriv,
|
|
|
- proofs_total - quote_info.amount,
|
|
|
- )?;
|
|
|
-
|
|
|
- let melt_response = self
|
|
|
- .client
|
|
|
- .post_melt(
|
|
|
- self.mint_url.clone().try_into()?,
|
|
|
- quote_id.to_string(),
|
|
|
- proofs.clone(),
|
|
|
- Some(premint_secrets.blinded_messages()),
|
|
|
- )
|
|
|
- .await;
|
|
|
-
|
|
|
- let melt_response = match melt_response {
|
|
|
- Ok(melt_response) => melt_response,
|
|
|
- Err(err) => {
|
|
|
- tracing::error!("Could not melt: {}", err);
|
|
|
- tracing::info!("Checking status of input proofs.");
|
|
|
-
|
|
|
- self.reclaim_unspent(proofs).await?;
|
|
|
-
|
|
|
- return Err(err);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- let active_keys = self
|
|
|
- .localstore
|
|
|
- .get_keys(&active_keyset_id)
|
|
|
- .await?
|
|
|
- .ok_or(Error::NoActiveKeyset)?;
|
|
|
-
|
|
|
- let change_proofs = match melt_response.change {
|
|
|
- Some(change) => {
|
|
|
- let num_change_proof = change.len();
|
|
|
-
|
|
|
- let num_change_proof = match (
|
|
|
- premint_secrets.len() < num_change_proof,
|
|
|
- premint_secrets.secrets().len() < num_change_proof,
|
|
|
- ) {
|
|
|
- (true, _) | (_, true) => {
|
|
|
- tracing::error!("Mismatch in change promises to change");
|
|
|
- premint_secrets.len()
|
|
|
- }
|
|
|
- _ => num_change_proof,
|
|
|
- };
|
|
|
-
|
|
|
- Some(construct_proofs(
|
|
|
- change,
|
|
|
- premint_secrets.rs()[..num_change_proof].to_vec(),
|
|
|
- premint_secrets.secrets()[..num_change_proof].to_vec(),
|
|
|
- &active_keys,
|
|
|
- )?)
|
|
|
- }
|
|
|
- None => None,
|
|
|
- };
|
|
|
-
|
|
|
- let state = match melt_response.paid {
|
|
|
- true => MeltQuoteState::Paid,
|
|
|
- false => MeltQuoteState::Unpaid,
|
|
|
- };
|
|
|
-
|
|
|
- let melted = Melted::from_proofs(
|
|
|
- state,
|
|
|
- melt_response.payment_preimage,
|
|
|
- quote_info.amount,
|
|
|
- proofs.clone(),
|
|
|
- change_proofs.clone(),
|
|
|
- )?;
|
|
|
-
|
|
|
- let change_proof_infos = match change_proofs {
|
|
|
- Some(change_proofs) => {
|
|
|
- tracing::debug!(
|
|
|
- "Change amount returned from melt: {}",
|
|
|
- Amount::try_sum(change_proofs.iter().map(|p| p.amount))?
|
|
|
- );
|
|
|
-
|
|
|
- // Update counter for keyset
|
|
|
- self.localstore
|
|
|
- .increment_keyset_counter(&active_keyset_id, change_proofs.len() as u32)
|
|
|
- .await?;
|
|
|
-
|
|
|
- change_proofs
|
|
|
- .into_iter()
|
|
|
- .map(|proof| {
|
|
|
- ProofInfo::new(
|
|
|
- proof,
|
|
|
- self.mint_url.clone(),
|
|
|
- State::Unspent,
|
|
|
- quote_info.unit,
|
|
|
- )
|
|
|
- })
|
|
|
- .collect::<Result<Vec<ProofInfo>, _>>()?
|
|
|
- }
|
|
|
- None => Vec::new(),
|
|
|
- };
|
|
|
-
|
|
|
- self.localstore.remove_melt_quote("e_info.id).await?;
|
|
|
-
|
|
|
- let deleted_ys = proofs
|
|
|
- .iter()
|
|
|
- .map(|p| p.y())
|
|
|
- .collect::<Result<Vec<PublicKey>, _>>()?;
|
|
|
- self.localstore
|
|
|
- .update_proofs(change_proof_infos, deleted_ys)
|
|
|
- .await?;
|
|
|
-
|
|
|
- Ok(melted)
|
|
|
- }
|
|
|
-
|
|
|
- /// Melt
|
|
|
- /// # Synopsis
|
|
|
- /// ```rust, no_run
|
|
|
- /// use std::sync::Arc;
|
|
|
- ///
|
|
|
- /// use cdk::cdk_database::WalletMemoryDatabase;
|
|
|
- /// use cdk::nuts::CurrencyUnit;
|
|
|
- /// use cdk::wallet::Wallet;
|
|
|
- /// use rand::Rng;
|
|
|
- ///
|
|
|
- /// #[tokio::main]
|
|
|
- /// async fn main() -> anyhow::Result<()> {
|
|
|
- /// let seed = rand::thread_rng().gen::<[u8; 32]>();
|
|
|
- /// let mint_url = "https://testnut.cashu.space";
|
|
|
- /// let unit = CurrencyUnit::Sat;
|
|
|
- ///
|
|
|
- /// let localstore = WalletMemoryDatabase::default();
|
|
|
- /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
|
|
- /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string();
|
|
|
- /// let quote = wallet.melt_quote(bolt11, None).await?;
|
|
|
- /// let quote_id = quote.id;
|
|
|
- ///
|
|
|
- /// let _ = wallet.melt("e_id).await?;
|
|
|
- ///
|
|
|
- /// Ok(())
|
|
|
- /// }
|
|
|
- #[instrument(skip(self))]
|
|
|
- pub async fn melt(&self, quote_id: &str) -> Result<Melted, Error> {
|
|
|
- let quote_info = self.localstore.get_melt_quote(quote_id).await?;
|
|
|
-
|
|
|
- let quote_info = if let Some(quote) = quote_info {
|
|
|
- if quote.expiry.le(&unix_time()) {
|
|
|
- return Err(Error::ExpiredQuote(quote.expiry, unix_time()));
|
|
|
- }
|
|
|
-
|
|
|
- quote.clone()
|
|
|
- } else {
|
|
|
- return Err(Error::UnknownQuote);
|
|
|
- };
|
|
|
-
|
|
|
- let inputs_needed_amount = quote_info.amount + quote_info.fee_reserve;
|
|
|
-
|
|
|
- let available_proofs = self.get_proofs().await?;
|
|
|
-
|
|
|
- let input_proofs = self
|
|
|
- .select_proofs_to_swap(inputs_needed_amount, available_proofs)
|
|
|
- .await?;
|
|
|
-
|
|
|
- self.melt_proofs(quote_id, input_proofs).await
|
|
|
- }
|
|
|
-
|
|
|
- /// Select proofs to send
|
|
|
- #[instrument(skip_all)]
|
|
|
- pub async fn select_proofs_to_send(
|
|
|
- &self,
|
|
|
- amount: Amount,
|
|
|
- proofs: Proofs,
|
|
|
- include_fees: bool,
|
|
|
- ) -> Result<Proofs, Error> {
|
|
|
- // TODO: Check all proofs are same unit
|
|
|
-
|
|
|
- if Amount::try_sum(proofs.iter().map(|p| p.amount))? < amount {
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
-
|
|
|
- let (mut proofs_larger, mut proofs_smaller): (Proofs, Proofs) =
|
|
|
- proofs.into_iter().partition(|p| p.amount > amount);
|
|
|
-
|
|
|
- let next_bigger_proof = proofs_larger.first().cloned();
|
|
|
-
|
|
|
- let mut selected_proofs: Vec<Proof> = Vec::new();
|
|
|
- let mut remaining_amount = amount;
|
|
|
-
|
|
|
- while remaining_amount > Amount::ZERO {
|
|
|
- proofs_larger.sort();
|
|
|
- // Sort smaller proofs in descending order
|
|
|
- proofs_smaller.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
|
|
|
-
|
|
|
- let selected_proof = if let Some(next_small) = proofs_smaller.clone().first() {
|
|
|
- next_small.clone()
|
|
|
- } else if let Some(next_bigger) = proofs_larger.first() {
|
|
|
- next_bigger.clone()
|
|
|
- } else {
|
|
|
- break;
|
|
|
- };
|
|
|
-
|
|
|
- let proof_amount = selected_proof.amount;
|
|
|
-
|
|
|
- selected_proofs.push(selected_proof);
|
|
|
-
|
|
|
- let fees = match include_fees {
|
|
|
- true => self.get_proofs_fee(&selected_proofs).await?,
|
|
|
- false => Amount::ZERO,
|
|
|
- };
|
|
|
-
|
|
|
- if proof_amount >= remaining_amount + fees {
|
|
|
- remaining_amount = Amount::ZERO;
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- remaining_amount = amount.checked_add(fees).ok_or(Error::AmountOverflow)?
|
|
|
- - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
- (proofs_larger, proofs_smaller) = proofs_smaller
|
|
|
- .into_iter()
|
|
|
- .skip(1)
|
|
|
- .partition(|p| p.amount > remaining_amount);
|
|
|
- }
|
|
|
-
|
|
|
- if remaining_amount > Amount::ZERO {
|
|
|
- if let Some(next_bigger) = next_bigger_proof {
|
|
|
- return Ok(vec![next_bigger.clone()]);
|
|
|
- }
|
|
|
-
|
|
|
- return Err(Error::InsufficientFunds);
|
|
|
- }
|
|
|
-
|
|
|
- Ok(selected_proofs)
|
|
|
- }
|
|
|
-
|
|
|
- /// Select proofs to send
|
|
|
- #[instrument(skip_all)]
|
|
|
- pub async fn select_proofs_to_swap(
|
|
|
- &self,
|
|
|
- amount: Amount,
|
|
|
- proofs: Proofs,
|
|
|
- ) -> Result<Proofs, Error> {
|
|
|
- let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
|
|
-
|
|
|
- let (mut active_proofs, mut inactive_proofs): (Proofs, Proofs) = proofs
|
|
|
- .into_iter()
|
|
|
- .partition(|p| p.keyset_id == active_keyset_id);
|
|
|
-
|
|
|
- let mut selected_proofs: Proofs = Vec::new();
|
|
|
- inactive_proofs.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
|
|
|
-
|
|
|
- for inactive_proof in inactive_proofs {
|
|
|
- selected_proofs.push(inactive_proof);
|
|
|
- let selected_total = Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
- let fees = self.get_proofs_fee(&selected_proofs).await?;
|
|
|
-
|
|
|
- if selected_total >= amount + fees {
|
|
|
- return Ok(selected_proofs);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- active_proofs.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
|
|
|
-
|
|
|
- for active_proof in active_proofs {
|
|
|
- selected_proofs.push(active_proof);
|
|
|
- let selected_total = Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?;
|
|
|
- let fees = self.get_proofs_fee(&selected_proofs).await?;
|
|
|
-
|
|
|
- if selected_total >= amount + fees {
|
|
|
- return Ok(selected_proofs);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Err(Error::InsufficientFunds)
|
|
|
- }
|
|
|
-
|
|
|
- /// Receive proofs
|
|
|
- #[instrument(skip_all)]
|
|
|
- pub async fn receive_proofs(
|
|
|
- &self,
|
|
|
- proofs: Proofs,
|
|
|
- amount_split_target: SplitTarget,
|
|
|
- p2pk_signing_keys: &[SecretKey],
|
|
|
- preimages: &[String],
|
|
|
- ) -> Result<Amount, Error> {
|
|
|
- let mut received_proofs: HashMap<MintUrl, Proofs> = HashMap::new();
|
|
|
- let mint_url = &self.mint_url;
|
|
|
- // Add mint if it does not exist in the store
|
|
|
- if self
|
|
|
- .localstore
|
|
|
- .get_mint(self.mint_url.clone())
|
|
|
- .await?
|
|
|
- .is_none()
|
|
|
- {
|
|
|
- tracing::debug!(
|
|
|
- "Mint not in localstore fetching info for: {}",
|
|
|
- self.mint_url
|
|
|
- );
|
|
|
- self.get_mint_info().await?;
|
|
|
- }
|
|
|
-
|
|
|
- let _ = self.get_active_mint_keyset().await?;
|
|
|
-
|
|
|
- let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
|
|
-
|
|
|
- let keys = self.get_keyset_keys(active_keyset_id).await?;
|
|
|
-
|
|
|
- let mut proofs = proofs;
|
|
|
-
|
|
|
- let mut sig_flag = SigFlag::SigInputs;
|
|
|
-
|
|
|
- // Map hash of preimage to preimage
|
|
|
- let hashed_to_preimage: HashMap<String, &String> = preimages
|
|
|
- .iter()
|
|
|
- .map(|p| {
|
|
|
- let hex_bytes = hex::decode(p)?;
|
|
|
- Ok::<(String, &String), Error>((Sha256Hash::hash(&hex_bytes).to_string(), p))
|
|
|
- })
|
|
|
- .collect::<Result<HashMap<String, &String>, _>>()?;
|
|
|
-
|
|
|
- let p2pk_signing_keys: HashMap<XOnlyPublicKey, &SecretKey> = p2pk_signing_keys
|
|
|
- .iter()
|
|
|
- .map(|s| (s.x_only_public_key(&SECP256K1).0, s))
|
|
|
- .collect();
|
|
|
-
|
|
|
- for proof in &mut proofs {
|
|
|
- // Verify that proof DLEQ is valid
|
|
|
- if proof.dleq.is_some() {
|
|
|
- let keys = self.get_keyset_keys(proof.keyset_id).await?;
|
|
|
- let key = keys.amount_key(proof.amount).ok_or(Error::AmountKey)?;
|
|
|
- proof.verify_dleq(key)?;
|
|
|
- }
|
|
|
-
|
|
|
- if let Ok(secret) =
|
|
|
- <crate::secret::Secret as TryInto<crate::nuts::nut10::Secret>>::try_into(
|
|
|
- proof.secret.clone(),
|
|
|
- )
|
|
|
- {
|
|
|
- let conditions: Result<Conditions, _> =
|
|
|
- secret.secret_data.tags.unwrap_or_default().try_into();
|
|
|
- if let Ok(conditions) = conditions {
|
|
|
- let mut pubkeys = conditions.pubkeys.unwrap_or_default();
|
|
|
-
|
|
|
- match secret.kind {
|
|
|
- Kind::P2PK => {
|
|
|
- let data_key = PublicKey::from_str(&secret.secret_data.data)?;
|
|
|
-
|
|
|
- pubkeys.push(data_key);
|
|
|
- }
|
|
|
- Kind::HTLC => {
|
|
|
- let hashed_preimage = &secret.secret_data.data;
|
|
|
- let preimage = hashed_to_preimage
|
|
|
- .get(hashed_preimage)
|
|
|
- .ok_or(Error::PreimageNotProvided)?;
|
|
|
- proof.add_preimage(preimage.to_string());
|
|
|
- }
|
|
|
- }
|
|
|
- for pubkey in pubkeys {
|
|
|
- if let Some(signing) = p2pk_signing_keys.get(&pubkey.x_only_public_key()) {
|
|
|
- proof.sign_p2pk(signing.to_owned().clone())?;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if conditions.sig_flag.eq(&SigFlag::SigAll) {
|
|
|
- sig_flag = SigFlag::SigAll;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Since the proofs are unknown they need to be added to the database
|
|
|
- let proofs_info = proofs
|
|
|
- .clone()
|
|
|
- .into_iter()
|
|
|
- .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit))
|
|
|
- .collect::<Result<Vec<ProofInfo>, _>>()?;
|
|
|
- self.localstore.update_proofs(proofs_info, vec![]).await?;
|
|
|
-
|
|
|
- let mut pre_swap = self
|
|
|
- .create_swap(None, amount_split_target, proofs, None, false)
|
|
|
- .await?;
|
|
|
-
|
|
|
- if sig_flag.eq(&SigFlag::SigAll) {
|
|
|
- for blinded_message in &mut pre_swap.swap_request.outputs {
|
|
|
- for signing_key in p2pk_signing_keys.values() {
|
|
|
- blinded_message.sign_p2pk(signing_key.to_owned().clone())?
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let swap_response = self
|
|
|
- .client
|
|
|
- .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request)
|
|
|
- .await?;
|
|
|
-
|
|
|
- // Proof to keep
|
|
|
- let p = construct_proofs(
|
|
|
- swap_response.signatures,
|
|
|
- pre_swap.pre_mint_secrets.rs(),
|
|
|
- pre_swap.pre_mint_secrets.secrets(),
|
|
|
- &keys,
|
|
|
- )?;
|
|
|
- let mint_proofs = received_proofs.entry(mint_url.clone()).or_default();
|
|
|
-
|
|
|
- self.localstore
|
|
|
- .increment_keyset_counter(&active_keyset_id, p.len() as u32)
|
|
|
- .await?;
|
|
|
-
|
|
|
- mint_proofs.extend(p);
|
|
|
-
|
|
|
- let mut total_amount = Amount::ZERO;
|
|
|
- for (mint, proofs) in received_proofs {
|
|
|
- total_amount += Amount::try_sum(proofs.iter().map(|p| p.amount))?;
|
|
|
- let proofs = proofs
|
|
|
- .into_iter()
|
|
|
- .map(|proof| ProofInfo::new(proof, mint.clone(), State::Unspent, self.unit))
|
|
|
- .collect::<Result<Vec<ProofInfo>, _>>()?;
|
|
|
- self.localstore.update_proofs(proofs, vec![]).await?;
|
|
|
- }
|
|
|
-
|
|
|
- Ok(total_amount)
|
|
|
- }
|
|
|
-
|
|
|
- /// Receive
|
|
|
- /// # Synopsis
|
|
|
- /// ```rust, no_run
|
|
|
- /// use std::sync::Arc;
|
|
|
- ///
|
|
|
- /// use cdk::amount::SplitTarget;
|
|
|
- /// use cdk::cdk_database::WalletMemoryDatabase;
|
|
|
- /// use cdk::nuts::CurrencyUnit;
|
|
|
- /// use cdk::wallet::Wallet;
|
|
|
- /// use rand::Rng;
|
|
|
- ///
|
|
|
- /// #[tokio::main]
|
|
|
- /// async fn main() -> anyhow::Result<()> {
|
|
|
- /// let seed = rand::thread_rng().gen::<[u8; 32]>();
|
|
|
- /// let mint_url = "https://testnut.cashu.space";
|
|
|
- /// let unit = CurrencyUnit::Sat;
|
|
|
- ///
|
|
|
- /// let localstore = WalletMemoryDatabase::default();
|
|
|
- /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
|
|
- /// let token = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjEsInNlY3JldCI6ImI0ZjVlNDAxMDJhMzhiYjg3NDNiOTkwMzU5MTU1MGYyZGEzZTQxNWEzMzU0OTUyN2M2MmM5ZDc5MGVmYjM3MDUiLCJDIjoiMDIzYmU1M2U4YzYwNTMwZWVhOWIzOTQzZmRhMWEyY2U3MWM3YjNmMGNmMGRjNmQ4NDZmYTc2NWFhZjc3OWZhODFkIiwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0=";
|
|
|
- /// let amount_receive = wallet.receive(token, SplitTarget::default(), &[], &[]).await?;
|
|
|
- /// Ok(())
|
|
|
- /// }
|
|
|
- /// ```
|
|
|
- #[instrument(skip_all)]
|
|
|
- pub async fn receive(
|
|
|
- &self,
|
|
|
- encoded_token: &str,
|
|
|
- amount_split_target: SplitTarget,
|
|
|
- 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();
|
|
|
-
|
|
|
- if unit != self.unit {
|
|
|
- return Err(Error::UnitUnsupported);
|
|
|
- }
|
|
|
-
|
|
|
- let proofs = token_data.proofs();
|
|
|
- if proofs.len() != 1 {
|
|
|
- return Err(Error::MultiMintTokenNotSupported);
|
|
|
- }
|
|
|
-
|
|
|
- let (mint_url, proofs) = proofs.into_iter().next().expect("Token has proofs");
|
|
|
-
|
|
|
- if self.mint_url != mint_url {
|
|
|
- return Err(Error::IncorrectMint);
|
|
|
- }
|
|
|
-
|
|
|
- let amount = self
|
|
|
- .receive_proofs(proofs, amount_split_target, p2pk_signing_keys, preimages)
|
|
|
- .await?;
|
|
|
-
|
|
|
- Ok(amount)
|
|
|
- }
|
|
|
-
|
|
|
/// Restore
|
|
|
#[instrument(skip(self))]
|
|
|
pub async fn restore(&self) -> Result<Amount, Error> {
|