refactor: wallet into multiple mods

This commit is contained in:
thesimplekid
2024-09-29 14:08:34 +02:00
parent 008c913583
commit b23b48c3fe
9 changed files with 1783 additions and 1667 deletions

View File

@@ -0,0 +1,69 @@
use std::collections::HashMap;
use tracing::instrument;
use crate::{
nuts::{CurrencyUnit, State},
Amount, Error, Wallet,
};
impl Wallet {
/// 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)
}
}

View File

@@ -0,0 +1,95 @@
use tracing::instrument;
use crate::nuts::Id;
use crate::nuts::KeySetInfo;
use crate::nuts::Keys;
use crate::Error;
use crate::Wallet;
impl Wallet {
/// 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()
}
}

View File

@@ -0,0 +1,310 @@
use std::str::FromStr;
use lightning_invoice::Bolt11Invoice;
use tracing::instrument;
use crate::{
dhke::construct_proofs,
nuts::{
CurrencyUnit, MeltQuoteBolt11Response, MeltQuoteState, PreMintSecrets, Proofs, PublicKey,
State,
},
types::{Melted, ProofInfo},
util::unix_time,
Amount, Error, Wallet,
};
use super::MeltQuote;
impl Wallet {
/// 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(&quote_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(&quote_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
}
}

View File

@@ -0,0 +1,274 @@
use tracing::instrument;
use super::MintQuote;
use crate::{
amount::SplitTarget,
dhke::construct_proofs,
nuts::{nut12, MintQuoteBolt11Response, PreMintSecrets, SpendingConditions, State},
types::ProofInfo,
util::unix_time,
wallet::MintQuoteState,
Amount, Error, Wallet,
};
impl Wallet {
/// 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, &crate::nuts::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(&quote_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(&quote_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)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,277 @@
use std::collections::HashSet;
use tracing::instrument;
use crate::{
amount::SplitTarget,
dhke::hash_to_curve,
nuts::{Proof, ProofState, Proofs, PublicKey, State},
types::ProofInfo,
Amount, Error, Wallet,
};
impl Wallet {
/// 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?)
}
/// 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)
}
/// 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: Proofs = 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)
}
}

View File

@@ -0,0 +1,224 @@
use std::{collections::HashMap, str::FromStr};
use bitcoin::hashes::Hash;
use bitcoin::{hashes::sha256::Hash as Sha256Hash, XOnlyPublicKey};
use tracing::instrument;
use crate::nuts::nut10::Kind;
use crate::nuts::{Conditions, Token};
use crate::{
amount::SplitTarget,
dhke::construct_proofs,
mint_url::MintUrl,
nuts::{Proofs, PublicKey, SecretKey, SigFlag, State},
types::ProofInfo,
util::hex,
Amount, Error, Wallet, SECP256K1,
};
impl Wallet {
/// 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)
}
}

View File

@@ -0,0 +1,212 @@
use tracing::instrument;
use crate::{
amount::SplitTarget,
nuts::{Proofs, PublicKey, SpendingConditions, State, Token},
Amount, Error, Wallet,
};
use super::SendKind;
impl Wallet {
/// 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
}
}

View File

@@ -0,0 +1,308 @@
use tracing::instrument;
use crate::amount::SplitTarget;
use crate::dhke::construct_proofs;
use crate::nuts::nut10;
use crate::nuts::PreMintSecrets;
use crate::nuts::PreSwap;
use crate::nuts::Proofs;
use crate::nuts::PublicKey;
use crate::nuts::SpendingConditions;
use crate::nuts::State;
use crate::nuts::SwapRequest;
use crate::types::ProofInfo;
use crate::Amount;
use crate::Error;
use crate::Wallet;
impl Wallet {
/// 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)
}
/// Swap from unspent proofs in db
#[instrument(skip(self))]
pub 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)
}
/// 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,
})
}
}