refactor: wallet clean up

This commit is contained in:
thesimplekid
2024-06-13 00:00:19 +01:00
parent 6298da79f5
commit 865b159a96
8 changed files with 359 additions and 314 deletions

View File

@@ -149,7 +149,7 @@ impl JsWallet {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?; let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let quote = self let quote = self
.inner .inner
.mint_quote(mint_url, amount.into(), unit.into()) .mint_quote(mint_url, unit.into(), amount.into())
.await .await
.map_err(into_err)?; .map_err(into_err)?;
@@ -311,10 +311,10 @@ impl JsWallet {
.send( .send(
&mint_url, &mint_url,
unit.into(), unit.into(),
memo,
Amount::from(amount), Amount::from(amount),
&target, memo,
conditions, conditions,
&target,
) )
.await .await
.map_err(into_err) .map_err(into_err)

View File

@@ -28,8 +28,8 @@ pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<(
let quote = wallet let quote = wallet
.mint_quote( .mint_quote(
mint_url.clone(), mint_url.clone(),
Amount::from(sub_command_args.amount),
CurrencyUnit::from(&sub_command_args.unit), CurrencyUnit::from(&sub_command_args.unit),
Amount::from(sub_command_args.amount),
) )
.await?; .await?;

View File

@@ -146,10 +146,10 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
.send( .send(
&mint_url, &mint_url,
CurrencyUnit::Sat, CurrencyUnit::Sat,
sub_command_args.memo.clone(),
token_amount, token_amount,
&SplitTarget::default(), sub_command_args.memo.clone(),
conditions, conditions,
&SplitTarget::default(),
) )
.await?; .await?;

View File

@@ -23,7 +23,7 @@ async fn main() -> Result<(), Error> {
let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]); let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]);
let quote = wallet let quote = wallet
.mint_quote(mint_url.clone(), amount, unit.clone()) .mint_quote(mint_url.clone(), unit.clone(), amount)
.await .await
.unwrap(); .unwrap();
@@ -52,7 +52,7 @@ async fn main() -> Result<(), Error> {
println!("Received {receive_amount} from mint {mint_url}"); println!("Received {receive_amount} from mint {mint_url}");
let token = wallet let token = wallet
.send(&mint_url, unit, None, amount, &SplitTarget::None, None) .send(&mint_url, unit, amount, None, None, &SplitTarget::default())
.await .await
.unwrap(); .unwrap();

View File

@@ -23,7 +23,7 @@ async fn main() -> Result<(), Error> {
let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]); let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]);
let quote = wallet let quote = wallet
.mint_quote(mint_url.clone(), amount, unit.clone()) .mint_quote(mint_url.clone(), unit.clone(), amount)
.await .await
.unwrap(); .unwrap();
@@ -58,10 +58,10 @@ async fn main() -> Result<(), Error> {
.send( .send(
&mint_url, &mint_url,
unit, unit,
None,
amount, amount,
&SplitTarget::None, None,
Some(spending_conditions), Some(spending_conditions),
&SplitTarget::None,
) )
.await .await
.unwrap(); .unwrap();

View File

@@ -11,10 +11,6 @@ use bitcoin::hashes::Hash;
use bitcoin::secp256k1::XOnlyPublicKey; use bitcoin::secp256k1::XOnlyPublicKey;
use bitcoin::Network; use bitcoin::Network;
use error::Error; use error::Error;
#[cfg(feature = "nostr")]
use nostr_sdk::nips::nip04;
#[cfg(feature = "nostr")]
use nostr_sdk::{Filter, Timestamp};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::instrument; use tracing::instrument;
@@ -34,6 +30,9 @@ use crate::{Amount, Bolt11Invoice, HttpClient};
pub mod client; pub mod client;
pub mod error; pub mod error;
#[cfg(feature = "nostr")]
pub mod nostr;
pub mod util;
#[derive(Clone)] #[derive(Clone)]
pub struct Wallet { pub struct Wallet {
@@ -90,14 +89,6 @@ impl Wallet {
self.p2pk_signing_keys.read().await.deref().clone() self.p2pk_signing_keys.read().await.deref().clone()
} }
/// Add nostr relays to client
#[cfg(feature = "nostr")]
#[instrument(skip(self))]
pub async fn add_nostr_relays(&self, relays: Vec<String>) -> Result<(), Error> {
self.nostr_client.add_relays(relays).await?;
Ok(())
}
/// Total Balance of wallet for given unit /// Total Balance of wallet for given unit
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn unit_balance(&self, unit: CurrencyUnit) -> Result<Amount, Error> { pub async fn unit_balance(&self, unit: CurrencyUnit) -> Result<Amount, Error> {
@@ -180,6 +171,7 @@ impl Wallet {
Ok(balances) Ok(balances)
} }
/// Total balance by mint
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn mint_balances( pub async fn mint_balances(
&self, &self,
@@ -212,6 +204,7 @@ impl Wallet {
Ok(mint_balances) Ok(mint_balances)
} }
/// Get unspent proofs for mint
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> { pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
Ok(self Ok(self
@@ -221,6 +214,7 @@ impl Wallet {
.map(|p| p.into_iter().map(|p| p.proof).collect())) .map(|p| p.into_iter().map(|p| p.proof).collect()))
} }
/// Add mint to wallet
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn add_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Error> { pub async fn add_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Error> {
let mint_info = match self let mint_info = match self
@@ -242,6 +236,7 @@ impl Wallet {
Ok(mint_info) Ok(mint_info)
} }
/// Get keys for mint keyset
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_keyset_keys( pub async fn get_keyset_keys(
&self, &self,
@@ -264,6 +259,7 @@ impl Wallet {
Ok(keys) Ok(keys)
} }
/// Get keysets for mint
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_keysets( pub async fn get_mint_keysets(
&self, &self,
@@ -338,6 +334,62 @@ impl Wallet {
Ok(()) Ok(())
} }
#[instrument(skip(self), fields(mint_url = %mint_url))]
async fn active_mint_keyset(
&self,
mint_url: &UncheckedUrl,
unit: &CurrencyUnit,
) -> Result<Id, Error> {
if let Some(keysets) = self.localstore.get_mint_keysets(mint_url.clone()).await? {
for keyset in keysets {
if keyset.unit.eq(unit) && keyset.active {
return Ok(keyset.id);
}
}
}
let keysets = self.client.get_mint_keysets(mint_url.try_into()?).await?;
self.localstore
.add_mint_keysets(
mint_url.clone(),
keysets.keysets.clone().into_iter().collect(),
)
.await?;
for keyset in &keysets.keysets {
if keyset.unit.eq(unit) && keyset.active {
return Ok(keyset.id);
}
}
Err(Error::NoActiveKeyset)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
async fn active_keys(
&self,
mint_url: &UncheckedUrl,
unit: &CurrencyUnit,
) -> Result<Option<Keys>, Error> {
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
let keys;
if let Some(k) = self.localstore.get_keys(&active_keyset_id).await? {
keys = Some(k.clone())
} else {
let keyset = self
.client
.get_mint_keyset(mint_url.try_into()?, active_keyset_id)
.await?;
self.localstore.add_keys(keyset.keys.clone()).await?;
keys = Some(keyset.keys);
}
Ok(keys)
}
/// Check if a proof is spent /// Check if a proof is spent
#[instrument(skip(self, proofs), fields(mint_url = %mint_url))] #[instrument(skip(self, proofs), fields(mint_url = %mint_url))]
pub async fn check_proofs_spent( pub async fn check_proofs_spent(
@@ -423,8 +475,8 @@ impl Wallet {
pub async fn mint_quote( pub async fn mint_quote(
&self, &self,
mint_url: UncheckedUrl, mint_url: UncheckedUrl,
amount: Amount,
unit: CurrencyUnit, unit: CurrencyUnit,
amount: Amount,
) -> Result<MintQuote, Error> { ) -> Result<MintQuote, Error> {
let quote_res = self let quote_res = self
.client .client
@@ -500,62 +552,6 @@ impl Wallet {
Ok(total_amount) Ok(total_amount)
} }
#[instrument(skip(self), fields(mint_url = %mint_url))]
async fn active_mint_keyset(
&self,
mint_url: &UncheckedUrl,
unit: &CurrencyUnit,
) -> Result<Id, Error> {
if let Some(keysets) = self.localstore.get_mint_keysets(mint_url.clone()).await? {
for keyset in keysets {
if keyset.unit.eq(unit) && keyset.active {
return Ok(keyset.id);
}
}
}
let keysets = self.client.get_mint_keysets(mint_url.try_into()?).await?;
self.localstore
.add_mint_keysets(
mint_url.clone(),
keysets.keysets.clone().into_iter().collect(),
)
.await?;
for keyset in &keysets.keysets {
if keyset.unit.eq(unit) && keyset.active {
return Ok(keyset.id);
}
}
Err(Error::NoActiveKeyset)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
async fn active_keys(
&self,
mint_url: &UncheckedUrl,
unit: &CurrencyUnit,
) -> Result<Option<Keys>, Error> {
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
let keys;
if let Some(k) = self.localstore.get_keys(&active_keyset_id).await? {
keys = Some(k.clone())
} else {
let keyset = self
.client
.get_mint_keyset(mint_url.try_into()?, active_keyset_id)
.await?;
self.localstore.add_keys(keyset.keys.clone()).await?;
keys = Some(keyset.keys);
}
Ok(keys)
}
/// Mint /// Mint
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))] #[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn mint( pub async fn mint(
@@ -880,10 +876,10 @@ impl Wallet {
&self, &self,
mint_url: &UncheckedUrl, mint_url: &UncheckedUrl,
unit: CurrencyUnit, unit: CurrencyUnit,
memo: Option<String>,
amount: Amount, amount: Amount,
amount_split_target: &SplitTarget, memo: Option<String>,
conditions: Option<SpendingConditions>, conditions: Option<SpendingConditions>,
amount_split_target: &SplitTarget,
) -> Result<String, Error> { ) -> Result<String, Error> {
let (condition_input_proofs, input_proofs) = self let (condition_input_proofs, input_proofs) = self
.select_proofs( .select_proofs(
@@ -956,9 +952,10 @@ impl Wallet {
.await?; .await?;
} }
Ok(self Ok(
.proof_to_token(mint_url.clone(), send_proofs, memo, Some(unit.clone()))? util::proof_to_token(mint_url.clone(), send_proofs, memo, Some(unit.clone()))?
.to_string()) .to_string(),
)
} }
/// Melt Quote /// Melt Quote
@@ -1020,6 +1017,118 @@ impl Wallet {
Ok(response) Ok(response)
} }
/// Melt
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn melt(
&self,
mint_url: &UncheckedUrl,
quote_id: &str,
amount_split_target: SplitTarget,
) -> 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 proofs = self
.select_proofs(
mint_url.clone(),
quote_info.unit.clone(),
quote_info.amount,
None,
)
.await?
.1;
let proofs_amount = proofs.iter().map(|p| p.amount).sum();
let active_keyset_id = self.active_mint_keyset(mint_url, &quote_info.unit).await?;
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(
active_keyset_id,
count,
self.xpriv,
proofs_amount,
true,
&amount_split_target,
)?;
let melt_response = self
.client
.post_melt(
mint_url.clone().try_into()?,
quote_id.to_string(),
proofs.clone(),
Some(premint_secrets.blinded_messages()),
)
.await?;
let change_proofs = match melt_response.change {
Some(change) => Some(construct_proofs(
change,
premint_secrets.rs(),
premint_secrets.secrets(),
&self
.active_keys(mint_url, &quote_info.unit)
.await?
.ok_or(Error::UnknownKey)?,
)?),
None => None,
};
let melted = Melted {
paid: true,
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,
mint_url.clone(),
State::Unspent,
quote_info.unit.clone(),
)
})
.collect();
self.localstore.add_proofs(change_proofs_info).await?;
}
self.localstore.remove_melt_quote(&quote_info.id).await?;
self.localstore.remove_proofs(&proofs).await?;
Ok(melted)
}
// Select proofs // Select proofs
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn select_proofs( pub async fn select_proofs(
@@ -1142,118 +1251,6 @@ impl Wallet {
Ok((condition_selected_proofs, selected_proofs)) Ok((condition_selected_proofs, selected_proofs))
} }
/// Melt
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn melt(
&self,
mint_url: &UncheckedUrl,
quote_id: &str,
amount_split_target: SplitTarget,
) -> 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 proofs = self
.select_proofs(
mint_url.clone(),
quote_info.unit.clone(),
quote_info.amount,
None,
)
.await?
.1;
let proofs_amount = proofs.iter().map(|p| p.amount).sum();
let active_keyset_id = self.active_mint_keyset(mint_url, &quote_info.unit).await?;
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(
active_keyset_id,
count,
self.xpriv,
proofs_amount,
true,
&amount_split_target,
)?;
let melt_response = self
.client
.post_melt(
mint_url.clone().try_into()?,
quote_id.to_string(),
proofs.clone(),
Some(premint_secrets.blinded_messages()),
)
.await?;
let change_proofs = match melt_response.change {
Some(change) => Some(construct_proofs(
change,
premint_secrets.rs(),
premint_secrets.secrets(),
&self
.active_keys(mint_url, &quote_info.unit)
.await?
.ok_or(Error::UnknownKey)?,
)?),
None => None,
};
let melted = Melted {
paid: true,
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,
mint_url.clone(),
State::Unspent,
quote_info.unit.clone(),
)
})
.collect();
self.localstore.add_proofs(change_proofs_info).await?;
}
self.localstore.remove_melt_quote(&quote_info.id).await?;
self.localstore.remove_proofs(&proofs).await?;
Ok(melted)
}
/// Receive /// Receive
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn receive( pub async fn receive(
@@ -1406,105 +1403,6 @@ impl Wallet {
Ok(total_amount) Ok(total_amount)
} }
#[cfg(feature = "nostr")]
#[instrument(skip_all)]
pub async fn nostr_receive(
&self,
nostr_signing_key: SecretKey,
since: Option<u64>,
amount_split_target: SplitTarget,
) -> Result<Amount, Error> {
use nostr_sdk::{Keys, Kind};
let verifying_key = nostr_signing_key.public_key();
let x_only_pubkey = verifying_key.x_only_public_key();
let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?;
let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;
self.add_p2pk_signing_key(nostr_signing_key).await;
let since = match since {
Some(since) => Some(Timestamp::from(since)),
None => self
.localstore
.get_nostr_last_checked(&verifying_key)
.await?
.map(|s| Timestamp::from(s as u64)),
};
let filter = match since {
Some(since) => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage)
.since(since),
None => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage),
};
self.nostr_client.connect().await;
let events = self.nostr_client.get_events_of(vec![filter], None).await?;
let mut tokens: HashSet<String> = HashSet::new();
for event in events {
if event.kind() == Kind::EncryptedDirectMessage {
if let Ok(msg) =
nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
{
if let Some(token) = Self::token_from_text(&msg) {
tokens.insert(token.to_string());
}
} else {
tracing::error!("Impossible to decrypt direct message");
}
}
}
let mut total_received = Amount::ZERO;
for token in tokens.iter() {
match self.receive(token, &amount_split_target, None).await {
Ok(amount) => total_received += amount,
Err(err) => {
tracing::error!("Could not receive token: {}", err);
}
}
}
self.localstore
.add_nostr_last_checked(verifying_key, unix_time() as u32)
.await?;
Ok(total_received)
}
#[cfg(feature = "nostr")]
fn token_from_text(text: &str) -> Option<&str> {
let text = text.trim();
if let Some(start) = text.find("cashu") {
match text[start..].find(' ') {
Some(end) => return Some(&text[start..(end + start)]),
None => return Some(&text[start..]),
}
}
None
}
#[instrument(skip(self, proofs), fields(mint_url = %mint_url))]
pub fn proof_to_token(
&self,
mint_url: UncheckedUrl,
proofs: Proofs,
memo: Option<String>,
unit: Option<CurrencyUnit>,
) -> Result<String, Error> {
Ok(Token::new(mint_url, proofs, memo, unit)?.to_string())
}
#[instrument(skip(self), fields(mint_url = %mint_url))] #[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn restore(&self, mint_url: UncheckedUrl) -> Result<Amount, Error> { pub async fn restore(&self, mint_url: UncheckedUrl) -> Result<Amount, Error> {
// Check that mint is in store of mints // Check that mint is in store of mints
@@ -1761,21 +1659,3 @@ impl Wallet {
Ok(()) Ok(())
} }
} }
#[cfg(feature = "nostr")]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_from_text() {
let text = " Here is some ecash: cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjIsInNlY3JldCI6ImI2Zjk1ODIxYmZlNjUyYjYwZGQ2ZjYwMDU4N2UyZjNhOTk4MzVhMGMyNWI4MTQzODNlYWIwY2QzOWFiNDFjNzUiLCJDIjoiMDI1YWU4ZGEyOTY2Y2E5OGVmYjA5ZDcwOGMxM2FiZmEwZDkxNGUwYTk3OTE4MmFjMzQ4MDllMjYxODY5YTBhNDJlIiwicmVzZXJ2ZWQiOmZhbHNlLCJpZCI6IjAwOWExZjI5MzI1M2U0MWUifSx7ImFtb3VudCI6Miwic2VjcmV0IjoiZjU0Y2JjNmNhZWZmYTY5MTUyOTgyM2M1MjU1MDkwYjRhMDZjNGQ3ZDRjNzNhNDFlZTFkNDBlM2ExY2EzZGZhNyIsIkMiOiIwMjMyMTIzN2JlYjcyMWU3NGI1NzcwNWE5MjJjNjUxMGQwOTYyYzAzNzlhZDM0OTJhMDYwMDliZTAyNjA5ZjA3NTAiLCJyZXNlcnZlZCI6ZmFsc2UsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSJ9LHsiYW1vdW50IjoxLCJzZWNyZXQiOiJhNzdhM2NjODY4YWM4ZGU3YmNiOWMxMzJmZWI3YzEzMDY4Nzg3ODk5Yzk3YTk2NWE2ZThkZTFiMzliMmQ2NmQ3IiwiQyI6IjAzMTY0YTMxNWVhNjM0NGE5NWI2NzM1NzBkYzg0YmZlMTQ2NDhmMTQwM2EwMDJiZmJlMDhlNWFhMWE0NDQ0YWE0MCIsInJlc2VydmVkIjpmYWxzZSwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0= fdfdfg
sdfs";
let token = Wallet::token_from_text(text).unwrap();
let token_str = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjIsInNlY3JldCI6ImI2Zjk1ODIxYmZlNjUyYjYwZGQ2ZjYwMDU4N2UyZjNhOTk4MzVhMGMyNWI4MTQzODNlYWIwY2QzOWFiNDFjNzUiLCJDIjoiMDI1YWU4ZGEyOTY2Y2E5OGVmYjA5ZDcwOGMxM2FiZmEwZDkxNGUwYTk3OTE4MmFjMzQ4MDllMjYxODY5YTBhNDJlIiwicmVzZXJ2ZWQiOmZhbHNlLCJpZCI6IjAwOWExZjI5MzI1M2U0MWUifSx7ImFtb3VudCI6Miwic2VjcmV0IjoiZjU0Y2JjNmNhZWZmYTY5MTUyOTgyM2M1MjU1MDkwYjRhMDZjNGQ3ZDRjNzNhNDFlZTFkNDBlM2ExY2EzZGZhNyIsIkMiOiIwMjMyMTIzN2JlYjcyMWU3NGI1NzcwNWE5MjJjNjUxMGQwOTYyYzAzNzlhZDM0OTJhMDYwMDliZTAyNjA5ZjA3NTAiLCJyZXNlcnZlZCI6ZmFsc2UsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSJ9LHsiYW1vdW50IjoxLCJzZWNyZXQiOiJhNzdhM2NjODY4YWM4ZGU3YmNiOWMxMzJmZWI3YzEzMDY4Nzg3ODk5Yzk3YTk2NWE2ZThkZTFiMzliMmQ2NmQ3IiwiQyI6IjAzMTY0YTMxNWVhNjM0NGE5NWI2NzM1NzBkYzg0YmZlMTQ2NDhmMTQwM2EwMDJiZmJlMDhlNWFhMWE0NDQ0YWE0MCIsInJlc2VydmVkIjpmYWxzZSwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0=";
assert_eq!(token, token_str)
}
}

View File

@@ -0,0 +1,118 @@
//! Wallet Nostr functions
use std::collections::HashSet;
use std::str::FromStr;
use nostr_sdk::nips::nip04;
use nostr_sdk::{Filter, Timestamp};
use tracing::instrument;
use super::error::Error;
use super::{util, Wallet};
use crate::amount::{Amount, SplitTarget};
use crate::nuts::SecretKey;
impl Wallet {
/// Add nostr relays to client
#[instrument(skip(self))]
pub async fn add_nostr_relays(&self, relays: Vec<String>) -> Result<(), Error> {
self.nostr_client.add_relays(relays).await?;
Ok(())
}
/// Remove nostr relays to client
#[instrument(skip(self))]
pub async fn remove_nostr_relays(&self, relay: String) -> Result<(), Error> {
self.nostr_client.remove_relay(relay).await?;
Ok(())
}
/// Nostr relays
#[instrument(skip(self))]
pub async fn nostr_relays(&self) -> Vec<String> {
self.nostr_client
.relays()
.await
.keys()
.map(|url| url.to_string())
.collect()
}
/// Receive tokens sent to nostr pubkey via dm
#[instrument(skip_all)]
pub async fn nostr_receive(
&self,
nostr_signing_key: SecretKey,
since: Option<u64>,
amount_split_target: SplitTarget,
) -> Result<Amount, Error> {
use nostr_sdk::{Keys, Kind};
use crate::util::unix_time;
use crate::Amount;
let verifying_key = nostr_signing_key.public_key();
let x_only_pubkey = verifying_key.x_only_public_key();
let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?;
let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;
self.add_p2pk_signing_key(nostr_signing_key).await;
let since = match since {
Some(since) => Some(Timestamp::from(since)),
None => self
.localstore
.get_nostr_last_checked(&verifying_key)
.await?
.map(|s| Timestamp::from(s as u64)),
};
let filter = match since {
Some(since) => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage)
.since(since),
None => Filter::new()
.pubkey(nostr_pubkey)
.kind(Kind::EncryptedDirectMessage),
};
self.nostr_client.connect().await;
let events = self.nostr_client.get_events_of(vec![filter], None).await?;
let mut tokens: HashSet<String> = HashSet::new();
for event in events {
if event.kind() == Kind::EncryptedDirectMessage {
if let Ok(msg) =
nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
{
if let Some(token) = util::token_from_text(&msg) {
tokens.insert(token.to_string());
}
} else {
tracing::error!("Impossible to decrypt direct message");
}
}
}
let mut total_received = Amount::ZERO;
for token in tokens.iter() {
match self.receive(token, &amount_split_target, None).await {
Ok(amount) => total_received += amount,
Err(err) => {
tracing::error!("Could not receive token: {}", err);
}
}
}
self.localstore
.add_nostr_last_checked(verifying_key, unix_time() as u32)
.await?;
Ok(total_received)
}
}

View File

@@ -0,0 +1,47 @@
//! Wallet Utility Functions
use super::Error;
use crate::nuts::{CurrencyUnit, Proofs, Token};
use crate::UncheckedUrl;
/// Extract token from text
#[cfg(feature = "nostr")]
pub(crate) fn token_from_text(text: &str) -> Option<&str> {
let text = text.trim();
if let Some(start) = text.find("cashu") {
match text[start..].find(' ') {
Some(end) => return Some(&text[start..(end + start)]),
None => return Some(&text[start..]),
}
}
None
}
/// Convert proofs to token
pub fn proof_to_token(
mint_url: UncheckedUrl,
proofs: Proofs,
memo: Option<String>,
unit: Option<CurrencyUnit>,
) -> Result<Token, Error> {
Ok(Token::new(mint_url, proofs, memo, unit)?)
}
#[cfg(feature = "nostr")]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_from_text() {
let text = " Here is some ecash: cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjIsInNlY3JldCI6ImI2Zjk1ODIxYmZlNjUyYjYwZGQ2ZjYwMDU4N2UyZjNhOTk4MzVhMGMyNWI4MTQzODNlYWIwY2QzOWFiNDFjNzUiLCJDIjoiMDI1YWU4ZGEyOTY2Y2E5OGVmYjA5ZDcwOGMxM2FiZmEwZDkxNGUwYTk3OTE4MmFjMzQ4MDllMjYxODY5YTBhNDJlIiwicmVzZXJ2ZWQiOmZhbHNlLCJpZCI6IjAwOWExZjI5MzI1M2U0MWUifSx7ImFtb3VudCI6Miwic2VjcmV0IjoiZjU0Y2JjNmNhZWZmYTY5MTUyOTgyM2M1MjU1MDkwYjRhMDZjNGQ3ZDRjNzNhNDFlZTFkNDBlM2ExY2EzZGZhNyIsIkMiOiIwMjMyMTIzN2JlYjcyMWU3NGI1NzcwNWE5MjJjNjUxMGQwOTYyYzAzNzlhZDM0OTJhMDYwMDliZTAyNjA5ZjA3NTAiLCJyZXNlcnZlZCI6ZmFsc2UsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSJ9LHsiYW1vdW50IjoxLCJzZWNyZXQiOiJhNzdhM2NjODY4YWM4ZGU3YmNiOWMxMzJmZWI3YzEzMDY4Nzg3ODk5Yzk3YTk2NWE2ZThkZTFiMzliMmQ2NmQ3IiwiQyI6IjAzMTY0YTMxNWVhNjM0NGE5NWI2NzM1NzBkYzg0YmZlMTQ2NDhmMTQwM2EwMDJiZmJlMDhlNWFhMWE0NDQ0YWE0MCIsInJlc2VydmVkIjpmYWxzZSwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0= fdfdfg
sdfs";
let token = token_from_text(text).unwrap();
let token_str = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjIsInNlY3JldCI6ImI2Zjk1ODIxYmZlNjUyYjYwZGQ2ZjYwMDU4N2UyZjNhOTk4MzVhMGMyNWI4MTQzODNlYWIwY2QzOWFiNDFjNzUiLCJDIjoiMDI1YWU4ZGEyOTY2Y2E5OGVmYjA5ZDcwOGMxM2FiZmEwZDkxNGUwYTk3OTE4MmFjMzQ4MDllMjYxODY5YTBhNDJlIiwicmVzZXJ2ZWQiOmZhbHNlLCJpZCI6IjAwOWExZjI5MzI1M2U0MWUifSx7ImFtb3VudCI6Miwic2VjcmV0IjoiZjU0Y2JjNmNhZWZmYTY5MTUyOTgyM2M1MjU1MDkwYjRhMDZjNGQ3ZDRjNzNhNDFlZTFkNDBlM2ExY2EzZGZhNyIsIkMiOiIwMjMyMTIzN2JlYjcyMWU3NGI1NzcwNWE5MjJjNjUxMGQwOTYyYzAzNzlhZDM0OTJhMDYwMDliZTAyNjA5ZjA3NTAiLCJyZXNlcnZlZCI6ZmFsc2UsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSJ9LHsiYW1vdW50IjoxLCJzZWNyZXQiOiJhNzdhM2NjODY4YWM4ZGU3YmNiOWMxMzJmZWI3YzEzMDY4Nzg3ODk5Yzk3YTk2NWE2ZThkZTFiMzliMmQ2NmQ3IiwiQyI6IjAzMTY0YTMxNWVhNjM0NGE5NWI2NzM1NzBkYzg0YmZlMTQ2NDhmMTQwM2EwMDJiZmJlMDhlNWFhMWE0NDQ0YWE0MCIsInJlc2VydmVkIjpmYWxzZSwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0=";
assert_eq!(token, token_str)
}
}