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 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)
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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, "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
|
// 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, "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
|
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
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