123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893 |
- //! Cashu Wallet
- //!
- //! Each wallet is single mint and single unit
- use std::collections::{HashMap, HashSet};
- use std::str::FromStr;
- use std::sync::Arc;
- use bitcoin::bip32::ExtendedPrivKey;
- use bitcoin::hashes::sha256::Hash as Sha256Hash;
- use bitcoin::hashes::Hash;
- use bitcoin::key::XOnlyPublicKey;
- use bitcoin::Network;
- use error::Error;
- use tracing::instrument;
- use crate::amount::SplitTarget;
- use crate::cdk_database::{self, WalletDatabase};
- use crate::dhke::{construct_proofs, hash_to_curve};
- use crate::nuts::nut00::token::Token;
- use crate::nuts::{
- nut10, nut12, Conditions, CurrencyUnit, Id, KeySetInfo, Keys, Kind, MeltQuoteBolt11Response,
- MeltQuoteState, MintInfo, MintQuoteBolt11Response, MintQuoteState, PreMintSecrets, PreSwap,
- Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey, SigFlag, SpendingConditions,
- State, SwapRequest,
- };
- use crate::types::{Melted, ProofInfo};
- use crate::url::UncheckedUrl;
- use crate::util::{hex, unix_time};
- use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
- pub mod client;
- pub mod error;
- pub mod multi_mint_wallet;
- pub mod types;
- pub mod util;
- pub use types::{MeltQuote, MintQuote, SendKind};
- /// CDK Wallet
- #[derive(Debug, Clone)]
- pub struct Wallet {
- /// Mint Url
- pub mint_url: UncheckedUrl,
- /// Unit
- pub unit: CurrencyUnit,
- /// Storage backend
- pub localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync>,
- /// The targeted amount of proofs to have at each size
- pub target_proof_count: usize,
- xpriv: ExtendedPrivKey,
- client: HttpClient,
- }
- impl Wallet {
- /// Create new [`Wallet`]
- pub fn new(
- mint_url: &str,
- unit: CurrencyUnit,
- localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync>,
- seed: &[u8],
- target_proof_count: Option<usize>,
- ) -> Self {
- let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, seed)
- .expect("Could not create master key");
- Self {
- mint_url: UncheckedUrl::from(mint_url),
- unit,
- client: HttpClient::new(),
- localstore,
- xpriv,
- target_proof_count: target_proof_count.unwrap_or(3),
- }
- }
- /// Fee required for proof set
- #[instrument(skip_all)]
- pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
- let mut sum_fee = 0;
- for proof in proofs {
- let input_fee_ppk = self
- .localstore
- .get_keyset_by_id(&proof.keyset_id)
- .await?
- .ok_or(Error::UnknownKey)?;
- sum_fee += input_fee_ppk.input_fee_ppk;
- }
- let fee = (sum_fee + 999) / 1000;
- Ok(Amount::from(fee))
- }
- /// Get fee for count of proofs in a keyset
- #[instrument(skip_all)]
- pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
- let input_fee_ppk = self
- .localstore
- .get_keyset_by_id(keyset_id)
- .await?
- .ok_or(Error::UnknownKey)?
- .input_fee_ppk;
- let fee = (input_fee_ppk * count + 999) / 1000;
- Ok(Amount::from(fee))
- }
- /// Total unspent balance of wallet
- #[instrument(skip(self))]
- pub async fn total_balance(&self) -> Result<Amount, Error> {
- if let Some(proofs) = self
- .localstore
- .get_proofs(
- Some(self.mint_url.clone()),
- Some(self.unit),
- Some(vec![State::Unspent]),
- None,
- )
- .await?
- {
- let balance = proofs.iter().map(|p| p.proof.amount).sum::<Amount>();
- return Ok(balance);
- }
- Ok(Amount::ZERO)
- }
- /// Total pending balance
- #[instrument(skip(self))]
- pub async fn total_pending_balance(&self) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
- let mut balances = HashMap::new();
- if let Some(proofs) = self
- .localstore
- .get_proofs(
- Some(self.mint_url.clone()),
- Some(self.unit),
- Some(vec![State::Pending]),
- None,
- )
- .await?
- {
- for proof in proofs {
- balances
- .entry(proof.unit)
- .and_modify(|ps| *ps += proof.proof.amount)
- .or_insert(proof.proof.amount);
- }
- }
- Ok(balances)
- }
- /// Total reserved balance
- #[instrument(skip(self))]
- pub async fn total_reserved_balance(&self) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
- let mut balances = HashMap::new();
- if let Some(proofs) = self
- .localstore
- .get_proofs(
- Some(self.mint_url.clone()),
- Some(self.unit),
- Some(vec![State::Reserved]),
- None,
- )
- .await?
- {
- for proof in proofs {
- balances
- .entry(proof.unit)
- .and_modify(|ps| *ps += proof.proof.amount)
- .or_insert(proof.proof.amount);
- }
- }
- Ok(balances)
- }
- /// Update Mint information and related entries in the event a mint changes its URL
- #[instrument(skip(self))]
- pub async fn update_mint_url(&mut self, new_mint_url: UncheckedUrl) -> Result<(), Error> {
- self.mint_url = new_mint_url.clone();
- // Where the mint_url is in the database it must be updated
- self.localstore
- .update_mint_url(self.mint_url.clone(), new_mint_url)
- .await?;
- self.localstore.remove_mint(self.mint_url.clone()).await?;
- 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?
- .map(|p| p.into_iter().map(|p| p.proof).collect())
- .unwrap_or_default())
- }
- /// 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?
- .map(|p| p.into_iter().map(|p| p.proof).collect())
- .unwrap_or_default())
- }
- /// 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?
- .map(|p| p.into_iter().map(|p| p.proof).collect())
- .unwrap_or_default())
- }
- /// 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> {
- for y in ys {
- self.localstore.set_proof_state(y, State::Unspent).await?;
- }
- Ok(())
- }
- /// Add mint to wallet
- #[instrument(skip(self))]
- pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> {
- let mint_info = match self
- .client
- .get_mint_info(self.mint_url.clone().try_into()?)
- .await
- {
- Ok(mint_info) => Some(mint_info),
- Err(err) => {
- tracing::warn!("Could not get mint info {}", err);
- None
- }
- };
- self.localstore
- .add_mint(self.mint_url.clone(), mint_info.clone())
- .await?;
- Ok(mint_info)
- }
- /// Get keys for mint keyset
- #[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
- #[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
- /// Quieries 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
- #[instrument(skip(self, proofs))]
- pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> {
- let proof_ys = proofs
- .iter()
- // Find Y for the secret
- .flat_map(|p| hash_to_curve(p.secret.as_bytes()))
- .collect::<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(())
- }
- /// Check if a proof is spent
- #[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
- .flat_map(|p| hash_to_curve(p.secret.as_bytes()))
- .collect::<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;
- if let Some(proofs) = self
- .localstore
- .get_proofs(
- Some(self.mint_url.clone()),
- Some(self.unit),
- Some(vec![State::Pending, State::Reserved]),
- None,
- )
- .await?
- {
- 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 = pending_proofs.iter().map(|p| p.proof.amount).sum();
- self.localstore
- .remove_proofs(&non_pending_proofs.into_iter().map(|p| p.proof).collect())
- .await?;
- balance += amount;
- }
- Ok(balance)
- }
- /// Mint Quote
- #[instrument(skip(self))]
- pub async fn mint_quote(&self, amount: Amount) -> Result<MintQuote, Error> {
- let mint_url = self.mint_url.clone();
- let unit = self.unit;
- let quote_res = self
- .client
- .post_mint_quote(mint_url.clone().try_into()?, amount, unit)
- .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)
- }
- /// 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
- #[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::QuoteExpired);
- }
- quote.clone()
- } else {
- return Err(Error::QuoteUnknown);
- };
- 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::UnknownKey)?;
- 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 = proofs.iter().map(|p| p.amount).sum();
- // 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()
- .flat_map(|proof| {
- ProofInfo::new(
- proof,
- self.mint_url.clone(),
- State::Unspent,
- quote_info.unit,
- )
- })
- .collect();
- // Add new proofs to store
- self.localstore.add_proofs(proofs).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> {
- let unspent_proofs = self.get_proofs().await?;
- let amounts_count: HashMap<usize, usize> =
- unspent_proofs
- .iter()
- .fold(HashMap::new(), |mut acc, proof| {
- let amount = proof.amount;
- let counter = acc.entry(u64::from(amount) as usize).or_insert(0);
- *counter += 1;
- acc
- });
- let all_possible_amounts: Vec<usize> = (0..32).map(|i| 2usize.pow(i as u32)).collect();
- let needed_amounts = all_possible_amounts
- .iter()
- .fold(Vec::new(), |mut acc, amount| {
- let count_needed: usize = self
- .target_proof_count
- .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
- for _i in 0..count_needed {
- acc.push(Amount::from(*amount as u64));
- }
- acc
- });
- Ok(needed_amounts)
- }
- /// Determine [`SplitTarget`] for amount based on state
- #[instrument(skip(self))]
- async fn determine_split_target_values(
- &self,
- change_amount: Amount,
- ) -> Result<SplitTarget, Error> {
- let mut amounts_needed_refill = self.amounts_needed_for_state_target().await?;
- amounts_needed_refill.sort();
- let mut values = Vec::new();
- for amount in amounts_needed_refill {
- let values_sum: Amount = values.clone().into_iter().sum();
- if values_sum + amount <= change_amount {
- values.push(amount);
- }
- }
- 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 = proofs.iter().map(|p| p.amount).sum();
- for proof in proofs.iter() {
- self.localstore
- .set_proof_state(proof.y()?, State::Pending)
- .await
- .ok();
- }
- 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_redeam = self
- .get_keyset_count_fee(&active_keyset_id, split_count as u64)
- .await?;
- (
- amount.map(|a| a + fee_to_redeam),
- change_amount - fee_to_redeam,
- )
- }
- 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 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 =
- proofs_to_send.iter().map(|p| p.amount).sum::<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 = proofs_to_send.iter().map(|p| p.amount).sum();
- 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()
- .flat_map(|proof| {
- ProofInfo::new(proof, mint_url.clone(), State::Reserved, *unit)
- })
- .collect();
- self.localstore.add_proofs(send_proofs_info).await?;
- 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()
- .flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, *unit))
- .collect();
- self.localstore.add_proofs(keep_proofs).await?;
- // Remove spent proofs used as inputs
- self.localstore.remove_proofs(&input_proofs).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?
- .ok_or(Error::InsufficientFunds)?;
- let available_proofs = available_proofs.into_iter().map(|p| p.proof).collect();
- 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<String, Error> {
- for proof in proofs.iter() {
- self.localstore
- .set_proof_state(proof.y()?, State::Reserved)
- .await?;
- }
- Ok(Token::new(self.mint_url.clone(), proofs, memo, Some(self.unit)).to_string())
- }
- /// 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<String, 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?
- .unwrap_or_default();
- let available_proofs = available_proofs.into_iter().map(|p| p.proof).collect();
- 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 =
- selected_proofs.iter().map(|p| p.amount).sum::<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 =
- selected_proofs.iter().map(|p| p.amount).sum::<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 =
- selected_proofs.iter().map(|p| p.amount).sum::<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 =
- selected_proofs.iter().map(|p| p.amount).sum::<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
- #[instrument(skip(self))]
- 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::UnitNotSupported),
- };
- 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))]
- 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::QuoteExpired);
- }
- quote.clone()
- } else {
- return Err(Error::QuoteUnknown);
- };
- for proof in proofs.iter() {
- self.localstore
- .set_proof_state(proof.y()?, State::Pending)
- .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,
- quote_info.fee_reserve,
- )?;
- 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) => Some(construct_proofs(
- change,
- premint_secrets.rs(),
- premint_secrets.secrets(),
- &active_keys,
- )?),
- None => None,
- };
- let state = match melt_response.paid {
- true => MeltQuoteState::Paid,
- false => MeltQuoteState::Unpaid,
- };
- let melted = Melted {
- state,
- preimage: melt_response.payment_preimage,
- change: change_proofs.clone(),
- };
- if let Some(change_proofs) = change_proofs {
- tracing::debug!(
- "Change amount returned from melt: {}",
- change_proofs.iter().map(|p| p.amount).sum::<Amount>()
- );
- // Update counter for keyset
- self.localstore
- .increment_keyset_counter(&active_keyset_id, change_proofs.len() as u32)
- .await?;
- let change_proofs_info = change_proofs
- .into_iter()
- .flat_map(|proof| {
- ProofInfo::new(
- proof,
- self.mint_url.clone(),
- State::Unspent,
- quote_info.unit,
- )
- })
- .collect();
- self.localstore.add_proofs(change_proofs_info).await?;
- }
- self.localstore.remove_melt_quote("e_info.id).await?;
- self.localstore.remove_proofs(&proofs).await?;
- Ok(melted)
- }
- /// Melt
- #[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::QuoteExpired);
- }
- quote.clone()
- } else {
- return Err(Error::QuoteUnknown);
- };
- 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 proofs.iter().map(|p| p.amount).sum::<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 + fees - selected_proofs.iter().map(|p| p.amount).sum::<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 = selected_proofs.iter().map(|p| p.amount).sum::<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 = selected_proofs.iter().map(|p| p.amount).sum::<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 _ = self.get_active_mint_keyset().await?;
- let mut received_proofs: HashMap<UncheckedUrl, 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()
- {
- self.get_mint_info().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()
- .flat_map(|p| match hex::decode(p) {
- Ok(hex_bytes) => Some((Sha256Hash::hash(&hex_bytes).to_string(), p)),
- Err(_) => None,
- })
- .collect();
- 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::UnknownKey)?;
- 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;
- }
- }
- }
- }
- 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 += proofs.iter().map(|p| p.amount).sum();
- let proofs = proofs
- .into_iter()
- .flat_map(|proof| ProofInfo::new(proof, mint.clone(), State::Unspent, self.unit))
- .collect();
- self.localstore.add_proofs(proofs).await?;
- }
- Ok(total_amount)
- }
- /// Receive
- #[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::UnitNotSupported);
- }
- 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> {
- // 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 keysets = self.get_mint_keysets().await?;
- let mut restored_value = Amount::ZERO;
- for keyset in keysets {
- let keys = self.get_keyset_keys(keyset.id).await?;
- let mut empty_batch = 0;
- let mut start_counter = 0;
- while empty_batch.lt(&3) {
- let premint_secrets = PreMintSecrets::restore_batch(
- keyset.id,
- self.xpriv,
- start_counter,
- start_counter + 100,
- )?;
- tracing::debug!(
- "Attempting to restore counter {}-{} for mint {} keyset {}",
- start_counter,
- start_counter + 100,
- self.mint_url,
- keyset.id
- );
- let restore_request = RestoreRequest {
- outputs: premint_secrets.blinded_messages(),
- };
- let response = self
- .client
- .post_restore(self.mint_url.clone().try_into()?, restore_request)
- .await?;
- if response.signatures.is_empty() {
- empty_batch += 1;
- start_counter += 100;
- continue;
- }
- let premint_secrets: Vec<_> = premint_secrets
- .secrets
- .iter()
- .filter(|p| response.outputs.contains(&p.blinded_message))
- .collect();
- let premint_secrets: Vec<_> = premint_secrets
- .iter()
- .filter(|p| response.outputs.contains(&p.blinded_message))
- .collect();
- // the response outputs and premint secrets should be the same after filtering
- // blinded messages the mint did not have signatures for
- assert_eq!(response.outputs.len(), premint_secrets.len());
- let proofs = construct_proofs(
- response.signatures,
- premint_secrets.iter().map(|p| p.r.clone()).collect(),
- premint_secrets.iter().map(|p| p.secret.clone()).collect(),
- &keys,
- )?;
- tracing::debug!("Restored {} proofs", proofs.len());
- self.localstore
- .increment_keyset_counter(&keyset.id, proofs.len() as u32)
- .await?;
- let states = self.check_proofs_spent(proofs.clone()).await?;
- let unspent_proofs: Vec<Proof> = proofs
- .iter()
- .zip(states)
- .filter(|(_, state)| !state.state.eq(&State::Spent))
- .map(|(p, _)| p)
- .cloned()
- .collect();
- restored_value += unspent_proofs.iter().map(|p| p.amount).sum();
- let unspent_proofs = unspent_proofs
- .into_iter()
- .flat_map(|proof| {
- ProofInfo::new(proof, self.mint_url.clone(), State::Unspent, keyset.unit)
- })
- .collect();
- self.localstore.add_proofs(unspent_proofs).await?;
- empty_batch = 0;
- start_counter += 100;
- }
- }
- Ok(restored_value)
- }
- /// Verify all proofs in token have meet the required spend
- /// Can be used to allow a wallet to accept payments offline while reducing
- /// the risk of claiming back to the limits let by the spending_conditions
- #[instrument(skip(self, token))]
- pub fn verify_token_p2pk(
- &self,
- token: &Token,
- spending_conditions: SpendingConditions,
- ) -> Result<(), Error> {
- let (refund_keys, pubkeys, locktime, num_sigs) = match spending_conditions {
- SpendingConditions::P2PKConditions { data, conditions } => {
- let mut pubkeys = vec![data];
- match conditions {
- Some(conditions) => {
- pubkeys.extend(conditions.pubkeys.unwrap_or_default());
- (
- conditions.refund_keys,
- Some(pubkeys),
- conditions.locktime,
- conditions.num_sigs,
- )
- }
- None => (None, Some(pubkeys), None, None),
- }
- }
- SpendingConditions::HTLCConditions {
- conditions,
- data: _,
- } => match conditions {
- Some(conditions) => (
- conditions.refund_keys,
- conditions.pubkeys,
- conditions.locktime,
- conditions.num_sigs,
- ),
- None => (None, None, None, None),
- },
- };
- if refund_keys.is_some() && locktime.is_none() {
- tracing::warn!(
- "Invalid spending conditions set: Locktime must be set if refund keys are allowed"
- );
- return Err(Error::InvalidSpendConditions(
- "Must set locktime".to_string(),
- ));
- }
- for (mint_url, proofs) in &token.proofs() {
- if mint_url != &self.mint_url {
- return Err(Error::IncorrectWallet(format!(
- "Should be {} not {}",
- self.mint_url, mint_url
- )));
- }
- for proof in proofs {
- let secret: nut10::Secret = (&proof.secret).try_into()?;
- let proof_conditions: SpendingConditions = secret.try_into()?;
- if num_sigs.ne(&proof_conditions.num_sigs()) {
- tracing::debug!(
- "Spending condition requires: {:?} sigs proof secret specifies: {:?}",
- num_sigs,
- proof_conditions.num_sigs()
- );
- return Err(Error::P2PKConditionsNotMet(
- "Num sigs did not match spending condition".to_string(),
- ));
- }
- let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default();
- let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default();
- // Check the Proof has the required pubkeys
- if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
- || !proof_pubkeys
- .iter()
- .all(|pubkey| spending_condition_pubkeys.contains(pubkey))
- {
- tracing::debug!("Proof did not included Publickeys meeting condition");
- tracing::debug!("{:?}", proof_pubkeys);
- tracing::debug!("{:?}", spending_condition_pubkeys);
- return Err(Error::P2PKConditionsNotMet(
- "Pubkeys in proof not allowed by spending condition".to_string(),
- ));
- }
- // If spending condition refund keys is allowed (Some(Empty Vec))
- // If spending conition refund keys is allowed to restricted set of keys check
- // it is one of them Check that proof locktime is > condition
- // locktime
- if let Some(proof_refund_keys) = proof_conditions.refund_keys() {
- let proof_locktime = proof_conditions
- .locktime()
- .ok_or(Error::LocktimeNotProvided)?;
- if let (Some(condition_refund_keys), Some(condition_locktime)) =
- (&refund_keys, locktime)
- {
- // Proof locktime must be greater then condition locktime to ensure it
- // cannot be claimed back
- if proof_locktime.lt(&condition_locktime) {
- return Err(Error::P2PKConditionsNotMet(
- "Proof locktime less then required".to_string(),
- ));
- }
- // A non empty condition refund key list is used as a restricted set of keys
- // returns are allowed to An empty list means the
- // proof can be refunded to anykey set in the secret
- if !condition_refund_keys.is_empty()
- && !proof_refund_keys
- .iter()
- .all(|refund_key| condition_refund_keys.contains(refund_key))
- {
- return Err(Error::P2PKConditionsNotMet(
- "Refund Key not allowed".to_string(),
- ));
- }
- } else {
- // Spending conditions does not allow refund keys
- return Err(Error::P2PKConditionsNotMet(
- "Spending condition does not allow refund keys".to_string(),
- ));
- }
- }
- }
- }
- Ok(())
- }
- /// Verify all proofs in token have a valid DLEQ proof
- #[instrument(skip(self, token))]
- pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
- let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
- for (mint_url, proofs) in &token.proofs() {
- if mint_url != &self.mint_url {
- return Err(Error::IncorrectWallet(format!(
- "Should be {} not {}",
- self.mint_url, mint_url
- )));
- }
- for proof in proofs {
- let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
- Some(keys) => keys.amount_key(proof.amount),
- None => {
- let keys = self.get_keyset_keys(proof.keyset_id).await?;
- let key = keys.amount_key(proof.amount);
- keys_cache.insert(proof.keyset_id, keys);
- key
- }
- }
- .ok_or(Error::UnknownKey)?;
- proof
- .verify_dleq(mint_pubkey)
- .map_err(|_| Error::CouldNotVerifyDleq)?;
- }
- }
- Ok(())
- }
- }
|