mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 15:44:50 +01:00
refactor: wallet clean up
This commit is contained in:
@@ -149,7 +149,7 @@ impl JsWallet {
|
||||
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
|
||||
let quote = self
|
||||
.inner
|
||||
.mint_quote(mint_url, amount.into(), unit.into())
|
||||
.mint_quote(mint_url, unit.into(), amount.into())
|
||||
.await
|
||||
.map_err(into_err)?;
|
||||
|
||||
@@ -311,10 +311,10 @@ impl JsWallet {
|
||||
.send(
|
||||
&mint_url,
|
||||
unit.into(),
|
||||
memo,
|
||||
Amount::from(amount),
|
||||
&target,
|
||||
memo,
|
||||
conditions,
|
||||
&target,
|
||||
)
|
||||
.await
|
||||
.map_err(into_err)
|
||||
|
||||
@@ -28,8 +28,8 @@ pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<(
|
||||
let quote = wallet
|
||||
.mint_quote(
|
||||
mint_url.clone(),
|
||||
Amount::from(sub_command_args.amount),
|
||||
CurrencyUnit::from(&sub_command_args.unit),
|
||||
Amount::from(sub_command_args.amount),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -146,10 +146,10 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
|
||||
.send(
|
||||
&mint_url,
|
||||
CurrencyUnit::Sat,
|
||||
sub_command_args.memo.clone(),
|
||||
token_amount,
|
||||
&SplitTarget::default(),
|
||||
sub_command_args.memo.clone(),
|
||||
conditions,
|
||||
&SplitTarget::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ async fn main() -> Result<(), Error> {
|
||||
let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]);
|
||||
|
||||
let quote = wallet
|
||||
.mint_quote(mint_url.clone(), amount, unit.clone())
|
||||
.mint_quote(mint_url.clone(), unit.clone(), amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -52,7 +52,7 @@ async fn main() -> Result<(), Error> {
|
||||
println!("Received {receive_amount} from mint {mint_url}");
|
||||
|
||||
let token = wallet
|
||||
.send(&mint_url, unit, None, amount, &SplitTarget::None, None)
|
||||
.send(&mint_url, unit, amount, None, None, &SplitTarget::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ async fn main() -> Result<(), Error> {
|
||||
let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]);
|
||||
|
||||
let quote = wallet
|
||||
.mint_quote(mint_url.clone(), amount, unit.clone())
|
||||
.mint_quote(mint_url.clone(), unit.clone(), amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -58,10 +58,10 @@ async fn main() -> Result<(), Error> {
|
||||
.send(
|
||||
&mint_url,
|
||||
unit,
|
||||
None,
|
||||
amount,
|
||||
&SplitTarget::None,
|
||||
None,
|
||||
Some(spending_conditions),
|
||||
&SplitTarget::None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -11,10 +11,6 @@ use bitcoin::hashes::Hash;
|
||||
use bitcoin::secp256k1::XOnlyPublicKey;
|
||||
use bitcoin::Network;
|
||||
use error::Error;
|
||||
#[cfg(feature = "nostr")]
|
||||
use nostr_sdk::nips::nip04;
|
||||
#[cfg(feature = "nostr")]
|
||||
use nostr_sdk::{Filter, Timestamp};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -34,6 +30,9 @@ use crate::{Amount, Bolt11Invoice, HttpClient};
|
||||
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
#[cfg(feature = "nostr")]
|
||||
pub mod nostr;
|
||||
pub mod util;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wallet {
|
||||
@@ -90,14 +89,6 @@ impl Wallet {
|
||||
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
|
||||
#[instrument(skip(self))]
|
||||
pub async fn unit_balance(&self, unit: CurrencyUnit) -> Result<Amount, Error> {
|
||||
@@ -180,6 +171,7 @@ impl Wallet {
|
||||
Ok(balances)
|
||||
}
|
||||
|
||||
/// Total balance by mint
|
||||
#[instrument(skip(self))]
|
||||
pub async fn mint_balances(
|
||||
&self,
|
||||
@@ -212,6 +204,7 @@ impl Wallet {
|
||||
Ok(mint_balances)
|
||||
}
|
||||
|
||||
/// Get unspent proofs for mint
|
||||
#[instrument(skip(self), fields(mint_url = %mint_url))]
|
||||
pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
|
||||
Ok(self
|
||||
@@ -221,6 +214,7 @@ impl Wallet {
|
||||
.map(|p| p.into_iter().map(|p| p.proof).collect()))
|
||||
}
|
||||
|
||||
/// Add mint to wallet
|
||||
#[instrument(skip(self), fields(mint_url = %mint_url))]
|
||||
pub async fn add_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Error> {
|
||||
let mint_info = match self
|
||||
@@ -242,6 +236,7 @@ impl Wallet {
|
||||
Ok(mint_info)
|
||||
}
|
||||
|
||||
/// Get keys for mint keyset
|
||||
#[instrument(skip(self), fields(mint_url = %mint_url))]
|
||||
pub async fn get_keyset_keys(
|
||||
&self,
|
||||
@@ -264,6 +259,7 @@ impl Wallet {
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Get keysets for mint
|
||||
#[instrument(skip(self), fields(mint_url = %mint_url))]
|
||||
pub async fn get_mint_keysets(
|
||||
&self,
|
||||
@@ -338,6 +334,62 @@ impl Wallet {
|
||||
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
|
||||
#[instrument(skip(self, proofs), fields(mint_url = %mint_url))]
|
||||
pub async fn check_proofs_spent(
|
||||
@@ -423,8 +475,8 @@ impl Wallet {
|
||||
pub async fn mint_quote(
|
||||
&self,
|
||||
mint_url: UncheckedUrl,
|
||||
amount: Amount,
|
||||
unit: CurrencyUnit,
|
||||
amount: Amount,
|
||||
) -> Result<MintQuote, Error> {
|
||||
let quote_res = self
|
||||
.client
|
||||
@@ -500,62 +552,6 @@ impl Wallet {
|
||||
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
|
||||
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
|
||||
pub async fn mint(
|
||||
@@ -880,10 +876,10 @@ impl Wallet {
|
||||
&self,
|
||||
mint_url: &UncheckedUrl,
|
||||
unit: CurrencyUnit,
|
||||
memo: Option<String>,
|
||||
amount: Amount,
|
||||
amount_split_target: &SplitTarget,
|
||||
memo: Option<String>,
|
||||
conditions: Option<SpendingConditions>,
|
||||
amount_split_target: &SplitTarget,
|
||||
) -> Result<String, Error> {
|
||||
let (condition_input_proofs, input_proofs) = self
|
||||
.select_proofs(
|
||||
@@ -956,9 +952,10 @@ impl Wallet {
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.proof_to_token(mint_url.clone(), send_proofs, memo, Some(unit.clone()))?
|
||||
.to_string())
|
||||
Ok(
|
||||
util::proof_to_token(mint_url.clone(), send_proofs, memo, Some(unit.clone()))?
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Melt Quote
|
||||
@@ -1020,6 +1017,118 @@ impl Wallet {
|
||||
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, "e_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, "e_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("e_info.id).await?;
|
||||
|
||||
self.localstore.remove_proofs(&proofs).await?;
|
||||
|
||||
Ok(melted)
|
||||
}
|
||||
|
||||
// Select proofs
|
||||
#[instrument(skip(self), fields(mint_url = %mint_url))]
|
||||
pub async fn select_proofs(
|
||||
@@ -1142,118 +1251,6 @@ impl Wallet {
|
||||
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, "e_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, "e_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("e_info.id).await?;
|
||||
|
||||
self.localstore.remove_proofs(&proofs).await?;
|
||||
|
||||
Ok(melted)
|
||||
}
|
||||
|
||||
/// Receive
|
||||
#[instrument(skip_all)]
|
||||
pub async fn receive(
|
||||
@@ -1406,105 +1403,6 @@ impl Wallet {
|
||||
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))]
|
||||
pub async fn restore(&self, mint_url: UncheckedUrl) -> Result<Amount, Error> {
|
||||
// Check that mint is in store of mints
|
||||
@@ -1761,21 +1659,3 @@ impl Wallet {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
118
crates/cdk/src/wallet/nostr.rs
Normal file
118
crates/cdk/src/wallet/nostr.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
47
crates/cdk/src/wallet/util.rs
Normal file
47
crates/cdk/src/wallet/util.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user