use std::collections::BTreeMap;
#[cfg(feature = "npubcash")]
use std::str::FromStr;
use std::sync::Arc;
use cdk_common::database;
use cdk_common::database::WalletDatabase;
use cdk_common::wallet::WalletKey;
use tokio::sync::RwLock;
use tracing::instrument;
use zeroize::Zeroize;
use super::builder::WalletBuilder;
use super::{Error, MintConnector};
use crate::mint_url::MintUrl;
use crate::nuts::CurrencyUnit;
use crate::wallet::keysets::KeysetFilter;
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
use crate::wallet::mint_connector::transport::tor_transport::TorAsync;
use crate::Wallet;
#[derive(Debug, Clone)]
pub struct TokenData {
pub mint_url: MintUrl,
pub proofs: cdk_common::Proofs,
pub memo: Option<String>,
pub value: cdk_common::Amount,
pub unit: CurrencyUnit,
pub redeem_fee: Option<cdk_common::Amount>,
}
#[derive(Clone, Default, Debug)]
pub struct WalletConfig {
pub mint_connector: Option<Arc<dyn super::MintConnector + Send + Sync>>,
pub auth_connector: Option<Arc<dyn super::auth::AuthMintConnector + Send + Sync>>,
pub target_proof_count: Option<usize>,
pub metadata_cache_ttl: Option<std::time::Duration>,
}
impl WalletConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_mint_connector(
mut self,
connector: Arc<dyn super::MintConnector + Send + Sync>,
) -> Self {
self.mint_connector = Some(connector);
self
}
pub fn with_auth_connector(
mut self,
connector: Arc<dyn super::auth::AuthMintConnector + Send + Sync>,
) -> Self {
self.auth_connector = Some(connector);
self
}
pub fn with_target_proof_count(mut self, count: usize) -> Self {
self.target_proof_count = Some(count);
self
}
pub fn with_metadata_cache_ttl(mut self, ttl: Option<std::time::Duration>) -> Self {
self.metadata_cache_ttl = ttl;
self
}
}
pub struct WalletRepositoryBuilder {
localstore: Option<Arc<dyn WalletDatabase<database::Error> + Send + Sync>>,
seed: Option<[u8; 64]>,
proxy_config: Option<url::Url>,
danger_accept_invalid_certs: bool,
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
use_tor: bool,
}
impl std::fmt::Debug for WalletRepositoryBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WalletRepositoryBuilder")
.field("localstore", &self.localstore.as_ref().map(|_| "..."))
.field("seed", &"[REDACTED]")
.field("proxy_config", &self.proxy_config)
.field(
"danger_accept_invalid_certs",
&self.danger_accept_invalid_certs,
)
.finish()
}
}
impl Default for WalletRepositoryBuilder {
fn default() -> Self {
Self::new()
}
}
impl WalletRepositoryBuilder {
pub fn new() -> Self {
Self {
localstore: None,
seed: None,
proxy_config: None,
danger_accept_invalid_certs: false,
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
use_tor: false,
}
}
pub fn localstore(
mut self,
localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
) -> Self {
self.localstore = Some(localstore);
self
}
pub fn seed(mut self, seed: [u8; 64]) -> Self {
self.seed = Some(seed);
self
}
pub fn proxy_url(mut self, proxy_url: url::Url) -> Self {
self.proxy_config = Some(proxy_url);
self
}
pub fn danger_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
self.danger_accept_invalid_certs = accept_invalid_certs;
self
}
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
pub fn tor(mut self) -> Self {
self.use_tor = true;
self
}
pub async fn build(self) -> Result<WalletRepository, Error> {
let localstore = self
.localstore
.ok_or(Error::Custom("localstore is required".into()))?;
let seed = self.seed.ok_or(Error::Custom("seed is required".into()))?;
let wallet = WalletRepository {
localstore,
seed,
wallets: Arc::new(RwLock::new(BTreeMap::new())),
proxy_config: self.proxy_config,
danger_accept_invalid_certs: self.danger_accept_invalid_certs,
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
shared_tor_transport: if self.use_tor {
Some(TorAsync::new())
} else {
None
},
};
wallet.load_wallets().await?;
Ok(wallet)
}
}
fn proxy_http_client(
mint_url: MintUrl,
proxy_url: &url::Url,
accept_invalid_certs: bool,
) -> Result<crate::wallet::HttpClient, Error> {
match proxy_url.scheme() {
"http" | "https" | "socks4" | "socks4a" | "socks5" | "socks5h" => {}
scheme => {
return Err(Error::HttpError(
None,
format!("Unsupported proxy URL scheme: {scheme}"),
));
}
}
crate::wallet::HttpClient::with_proxy(mint_url, proxy_url.clone(), None, accept_invalid_certs)
}
#[derive(Clone)]
pub struct WalletRepository {
localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
seed: [u8; 64],
wallets: Arc<RwLock<BTreeMap<WalletKey, Wallet>>>,
proxy_config: Option<url::Url>,
danger_accept_invalid_certs: bool,
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
shared_tor_transport: Option<TorAsync>,
}
impl std::fmt::Debug for WalletRepository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WalletRepository").finish_non_exhaustive()
}
}
impl WalletRepository {
pub fn seed(&self) -> &[u8; 64] {
&self.seed
}
#[instrument(skip(self))]
pub async fn get_wallet(
&self,
mint_url: &MintUrl,
unit: &CurrencyUnit,
) -> Result<Wallet, Error> {
let key = WalletKey::new(mint_url.clone(), unit.clone());
self.wallets
.read()
.await
.get(&key)
.cloned()
.ok_or_else(|| Error::UnknownWallet(key))
}
#[instrument(skip(self))]
pub async fn get_wallets_for_mint(&self, mint_url: &MintUrl) -> Vec<Wallet> {
self.wallets
.read()
.await
.iter()
.filter(|(key, _)| &key.mint_url == mint_url)
.map(|(_, wallet)| wallet.clone())
.collect()
}
#[instrument(skip(self))]
pub async fn has_wallet(&self, mint_url: &MintUrl, unit: &CurrencyUnit) -> bool {
let key = WalletKey::new(mint_url.clone(), unit.clone());
self.wallets.read().await.contains_key(&key)
}
#[instrument(skip(self))]
pub async fn add_wallet(&self, mint_url: MintUrl) -> Result<Vec<Wallet>, Error> {
self.add_wallet_with_config(mint_url, None).await
}
#[instrument(skip(self))]
pub async fn add_wallet_with_config(
&self,
mint_url: MintUrl,
config: Option<WalletConfig>,
) -> Result<Vec<Wallet>, Error> {
let mint_info = self.fetch_mint_info(&mint_url).await?;
let supported_units = mint_info.supported_units();
if supported_units.is_empty() {
return Err(Error::Custom(
"Mint does not support any currency units".into(),
));
}
let mut wallets = Vec::new();
for unit in supported_units {
if self.has_wallet(&mint_url, unit).await {
if let Ok(existing) = self.get_wallet(&mint_url, unit).await {
wallets.push(existing);
}
continue;
}
let wallet = self
.create_wallet(mint_url.clone(), unit.clone(), config.clone())
.await?;
wallets.push(wallet);
}
Ok(wallets)
}
#[instrument(skip(self))]
pub async fn set_mint_config(
&self,
mint_url: MintUrl,
unit: CurrencyUnit,
config: WalletConfig,
) -> Result<Wallet, Error> {
self.create_wallet(mint_url, unit, Some(config)).await
}
#[instrument(skip(self))]
pub async fn create_wallet(
&self,
mint_url: MintUrl,
unit: CurrencyUnit,
config: Option<WalletConfig>,
) -> Result<Wallet, Error> {
let wallet = self
.create_wallet_internal(mint_url.clone(), unit.clone(), config.as_ref())
.await?;
let key = WalletKey::new(mint_url, unit);
let mut wallets = self.wallets.write().await;
wallets.insert(key, wallet.clone());
Ok(wallet)
}
#[instrument(skip(self))]
pub async fn remove_wallet(
&self,
mint_url: MintUrl,
currency_unit: CurrencyUnit,
) -> Result<(), Error> {
let key = WalletKey::new(mint_url, currency_unit);
let mut wallets = self.wallets.write().await;
if !wallets.contains_key(&key) {
return Err(Error::UnknownWallet(key));
}
wallets.remove(&key);
Ok(())
}
#[instrument(skip(self))]
pub async fn get_wallets(&self) -> Vec<Wallet> {
self.wallets.read().await.values().cloned().collect()
}
#[instrument(skip(self))]
pub async fn has_mint(&self, mint_url: &MintUrl) -> bool {
self.wallets
.read()
.await
.keys()
.any(|key| &key.mint_url == mint_url)
}
#[instrument(skip(self))]
pub async fn get_balances(&self) -> Result<BTreeMap<WalletKey, cdk_common::Amount>, Error> {
let wallets = self.wallets.read().await;
let mut balances = BTreeMap::new();
for (key, wallet) in wallets.iter() {
let balance = wallet.total_balance().await?;
balances.insert(key.clone(), balance);
}
Ok(balances)
}
#[instrument(skip(self))]
pub async fn total_balance(&self) -> Result<BTreeMap<CurrencyUnit, cdk_common::Amount>, Error> {
let balances = self.get_balances().await?;
let mut by_unit: BTreeMap<CurrencyUnit, cdk_common::Amount> = BTreeMap::new();
for (key, amount) in balances {
let entry = by_unit.entry(key.unit).or_insert(cdk_common::Amount::ZERO);
*entry += amount;
}
Ok(by_unit)
}
pub async fn fetch_mint_info(
&self,
mint_url: &MintUrl,
) -> Result<crate::nuts::MintInfo, Error> {
let client: Arc<dyn MintConnector + Send + Sync> =
if let Some(proxy_url) = &self.proxy_config {
Arc::new(proxy_http_client(
mint_url.clone(),
proxy_url,
self.danger_accept_invalid_certs,
)?)
} else {
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
if let Some(tor) = &self.shared_tor_transport {
let transport = tor.clone();
Arc::new(crate::wallet::TorHttpClient::with_transport(
mint_url.clone(),
transport,
None,
))
} else {
Arc::new(crate::wallet::HttpClient::new(mint_url.clone(), None))
}
#[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
{
Arc::new(crate::wallet::HttpClient::new(mint_url.clone(), None))
}
};
client.get_mint_info().await
}
async fn create_wallet_internal(
&self,
mint_url: MintUrl,
unit: CurrencyUnit,
config: Option<&WalletConfig>,
) -> Result<Wallet, Error> {
if let Some(cfg) = config {
if let Some(custom_connector) = &cfg.mint_connector {
let mut builder = WalletBuilder::new()
.mint_url(mint_url.clone())
.unit(unit.clone())
.localstore(self.localstore.clone())
.seed(self.seed)
.target_proof_count(cfg.target_proof_count.unwrap_or(3))
.shared_client(custom_connector.clone());
if let Some(ttl) = cfg.metadata_cache_ttl {
builder = builder.set_metadata_cache_ttl(Some(ttl));
}
return builder.build();
}
}
let target_proof_count = config.and_then(|c| c.target_proof_count).unwrap_or(3);
let metadata_cache_ttl = config.and_then(|c| c.metadata_cache_ttl);
let wallet = if let Some(proxy_url) = &self.proxy_config {
let client = proxy_http_client(
mint_url.clone(),
proxy_url,
self.danger_accept_invalid_certs,
)?;
let mut builder = WalletBuilder::new()
.mint_url(mint_url.clone())
.unit(unit.clone())
.localstore(self.localstore.clone())
.seed(self.seed)
.target_proof_count(target_proof_count)
.client(client);
if let Some(ttl) = metadata_cache_ttl {
builder = builder.set_metadata_cache_ttl(Some(ttl));
}
builder.build()?
} else {
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
if let Some(tor) = &self.shared_tor_transport {
let client = crate::wallet::TorHttpClient::with_transport(
mint_url.clone(),
tor.clone(),
None,
);
let mut builder = WalletBuilder::new()
.mint_url(mint_url.clone())
.unit(unit.clone())
.localstore(self.localstore.clone())
.seed(self.seed)
.target_proof_count(target_proof_count)
.client(client);
if let Some(ttl) = metadata_cache_ttl {
builder = builder.set_metadata_cache_ttl(Some(ttl));
}
builder.build()?
} else {
let wallet = Wallet::new(
&mint_url.to_string(),
unit.clone(),
self.localstore.clone(),
self.seed,
Some(target_proof_count),
)?;
if let Some(ttl) = metadata_cache_ttl {
wallet.set_metadata_cache_ttl(Some(ttl));
}
wallet
}
#[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
{
let wallet = Wallet::new(
&mint_url.to_string(),
unit.clone(),
self.localstore.clone(),
self.seed,
Some(target_proof_count),
)?;
if let Some(ttl) = metadata_cache_ttl {
wallet.set_metadata_cache_ttl(Some(ttl));
}
wallet
}
};
Ok(wallet)
}
#[instrument(skip(self))]
async fn load_wallets(&self) -> Result<(), Error> {
let mints = self.localstore.get_mints().await.map_err(Error::Database)?;
for (mint_url, _mint_info) in mints {
let units = match self.fetch_mint_info(&mint_url).await {
Ok(info) => {
let supported = info.supported_units();
if supported.is_empty() {
vec![CurrencyUnit::Sat]
} else {
supported.into_iter().cloned().collect()
}
}
Err(_) => {
vec![CurrencyUnit::Sat]
}
};
for unit in units {
let key = WalletKey::new(mint_url.clone(), unit.clone());
if self.wallets.read().await.contains_key(&key) {
continue;
}
let wallet = self
.create_wallet_internal(mint_url.clone(), unit, None)
.await?;
let mut wallets = self.wallets.write().await;
wallets.insert(key, wallet);
}
}
Ok(())
}
#[cfg(feature = "npubcash")]
pub async fn get_active_npubcash_mint(&self) -> Result<Option<MintUrl>, Error> {
use super::npubcash::{ACTIVE_MINT_KEY, NPUBCASH_KV_NAMESPACE};
let value = self
.localstore
.kv_read(NPUBCASH_KV_NAMESPACE, "", ACTIVE_MINT_KEY)
.await?;
match value {
Some(bytes) => {
let s = String::from_utf8(bytes)
.map_err(|_| Error::Custom("Invalid active mint URL".into()))?;
Ok(Some(MintUrl::from_str(&s)?))
}
None => Ok(None),
}
}
#[cfg(feature = "npubcash")]
pub async fn set_active_npubcash_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
use super::npubcash::{ACTIVE_MINT_KEY, NPUBCASH_KV_NAMESPACE};
self.localstore
.kv_write(
NPUBCASH_KV_NAMESPACE,
"",
ACTIVE_MINT_KEY,
mint_url.to_string().as_bytes(),
)
.await?;
Ok(())
}
#[cfg(feature = "npubcash")]
pub async fn sync_npubcash_quotes(
&self,
) -> Result<Vec<crate::wallet::types::MintQuote>, Error> {
let active_mint = self.get_active_npubcash_mint().await?;
if let Some(mint_url) = active_mint {
let wallet = self.get_wallet(&mint_url, &CurrencyUnit::Sat).await?;
wallet.sync_npubcash_quotes().await
} else {
Err(Error::Custom("No active NpubCash mint set".into()))
}
}
#[instrument(skip(self, token))]
pub async fn get_token_data(
&self,
token: &crate::nuts::nut00::Token,
) -> Result<TokenData, Error> {
let mint_url = token.mint_url()?;
let unit = token.unit().unwrap_or_default();
let wallet = self.get_wallet(&mint_url, &unit).await?;
let keysets = wallet.get_mint_keysets(KeysetFilter::Active).await?;
let proofs = token.proofs(&keysets)?;
let memo = token.memo().clone();
let redeem_fee = wallet.get_proofs_fee(&proofs).await?;
Ok(TokenData {
value: cdk_common::nuts::nut00::ProofsMethods::total_amount(&proofs)?,
mint_url,
proofs,
memo,
unit,
redeem_fee: Some(redeem_fee.total),
})
}
#[instrument(skip(self))]
pub async fn list_proofs(
&self,
) -> Result<std::collections::BTreeMap<WalletKey, Vec<cdk_common::Proof>>, Error> {
let mut mint_proofs = std::collections::BTreeMap::new();
for (key, wallet) in self.wallets.read().await.iter() {
let wallet_proofs = wallet.get_unspent_proofs().await?;
mint_proofs.insert(key.clone(), wallet_proofs);
}
Ok(mint_proofs)
}
#[instrument(skip(self))]
pub async fn list_transactions(
&self,
direction: Option<cdk_common::wallet::TransactionDirection>,
) -> Result<Vec<cdk_common::wallet::Transaction>, Error> {
let mut transactions = Vec::new();
for wallet in self.wallets.read().await.values() {
let wallet_transactions = wallet.list_transactions(direction).await?;
transactions.extend(wallet_transactions);
}
transactions.sort();
Ok(transactions)
}
#[instrument(skip(self))]
pub async fn check_all_mint_quotes(
&self,
mint_url: Option<MintUrl>,
) -> Result<cdk_common::Amount, Error> {
let mut total_minted = cdk_common::Amount::ZERO;
let wallets = self.wallets.read().await;
let wallets_to_check: Vec<_> = match &mint_url {
Some(url) => {
let filtered: Vec<_> = wallets
.iter()
.filter(|(key, _)| &key.mint_url == url)
.map(|(_, wallet)| wallet.clone())
.collect();
if filtered.is_empty() {
return Err(Error::UnknownMint {
mint_url: url.to_string(),
});
}
filtered
}
None => wallets.values().cloned().collect(),
};
drop(wallets);
for wallet in wallets_to_check {
let minted = wallet.mint_unissued_quotes().await?;
total_minted += minted;
}
Ok(total_minted)
}
}
impl Drop for WalletRepository {
fn drop(&mut self) {
self.seed.zeroize();
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use cdk_common::database::WalletDatabase;
use tokio::net::TcpListener;
use super::*;
async fn create_test_repository() -> WalletRepository {
let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
cdk_sqlite::wallet::memory::empty()
.await
.expect("Failed to create in-memory database"),
);
let seed = [0u8; 64];
WalletRepositoryBuilder::new()
.localstore(localstore)
.seed(seed)
.build()
.await
.expect("Failed to create WalletRepository")
}
async fn create_test_repository_with_proxy(proxy_url: url::Url) -> WalletRepository {
let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
cdk_sqlite::wallet::memory::empty()
.await
.expect("Failed to create in-memory database"),
);
let seed = [0u8; 64];
WalletRepositoryBuilder::new()
.localstore(localstore)
.seed(seed)
.proxy_url(proxy_url)
.build()
.await
.expect("Failed to create WalletRepository")
}
async fn local_mint_url_with_connection_counter(
) -> (MintUrl, Arc<AtomicUsize>, tokio::task::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("Failed to bind test mint listener");
let address = listener
.local_addr()
.expect("Failed to get test mint listener address");
let direct_connections = Arc::new(AtomicUsize::new(0));
let connection_count = Arc::clone(&direct_connections);
let handle = tokio::spawn(async move {
while let Ok((_stream, _address)) = listener.accept().await {
connection_count.fetch_add(1, Ordering::SeqCst);
}
});
(
format!("http://{address}")
.parse()
.expect("Failed to parse test mint URL"),
direct_connections,
handle,
)
}
fn unsupported_proxy_url() -> url::Url {
"gopher://127.0.0.1:1080"
.parse()
.expect("Failed to parse proxy URL")
}
#[test]
fn builder_verifies_proxy_tls_certificates_by_default() {
let builder = WalletRepositoryBuilder::new();
assert!(!builder.danger_accept_invalid_certs);
}
#[test]
fn builder_can_explicitly_accept_invalid_proxy_tls_certificates() {
let builder = WalletRepositoryBuilder::new().danger_accept_invalid_certs(true);
assert!(builder.danger_accept_invalid_certs);
}
#[tokio::test]
async fn test_wallet_repository_creation() {
let repo = create_test_repository().await;
assert!(repo.wallets.try_read().is_ok());
}
#[tokio::test]
async fn test_has_mint_empty() {
let repo = create_test_repository().await;
let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
assert!(!repo.has_mint(&mint_url).await);
}
#[tokio::test]
async fn test_create_and_get_wallet() {
let repo = create_test_repository().await;
let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
let wallet = repo
.create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
.await
.expect("Failed to create wallet");
assert_eq!(wallet.mint_url, mint_url);
assert_eq!(wallet.unit, CurrencyUnit::Sat);
assert!(repo.has_mint(&mint_url).await);
assert!(repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
let retrieved = repo.get_wallet(&mint_url, &CurrencyUnit::Sat).await;
assert!(retrieved.is_ok());
}
#[tokio::test]
async fn test_fetch_mint_info_returns_error_when_proxy_setup_fails() {
let repo = create_test_repository_with_proxy(unsupported_proxy_url()).await;
let (mint_url, direct_connections, listener_handle) =
local_mint_url_with_connection_counter().await;
let result = repo.fetch_mint_info(&mint_url).await;
listener_handle.abort();
assert!(result.is_err());
assert_eq!(direct_connections.load(Ordering::SeqCst), 0);
}
#[tokio::test]
async fn test_create_wallet_returns_error_when_proxy_setup_fails() {
let repo = create_test_repository_with_proxy(unsupported_proxy_url()).await;
let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
let result = repo
.create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
.await;
assert!(result.is_err());
assert!(!repo.has_mint(&mint_url).await);
assert!(!repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
}
#[tokio::test]
async fn test_remove_wallet() {
let repo = create_test_repository().await;
let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
repo.create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
.await
.expect("Failed to create wallet");
assert!(repo.has_mint(&mint_url).await);
assert!(repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
let _ = repo
.remove_wallet(mint_url.clone(), CurrencyUnit::Sat)
.await;
assert!(!repo.has_mint(&mint_url).await);
assert!(!repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
}
#[tokio::test]
async fn test_get_wallets() {
let repo = create_test_repository().await;
let mint1: MintUrl = "https://mint1.example.com".parse().unwrap();
let mint2: MintUrl = "https://mint2.example.com".parse().unwrap();
repo.create_wallet(mint1, CurrencyUnit::Sat, None)
.await
.expect("Failed to create wallet 1");
repo.create_wallet(mint2, CurrencyUnit::Sat, None)
.await
.expect("Failed to create wallet 2");
let wallets = repo.get_wallets().await;
assert_eq!(wallets.len(), 2);
}
#[tokio::test]
async fn test_remove_wallet_does_not_touch_db() {
let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
cdk_sqlite::wallet::memory::empty()
.await
.expect("Failed to create in-memory database"),
);
let seed = [0u8; 64];
let repo = WalletRepositoryBuilder::new()
.localstore(localstore.clone())
.seed(seed)
.build()
.await
.expect("Failed to create WalletRepository");
let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
localstore.add_mint(mint_url.clone(), None).await.unwrap();
repo.create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
.await
.expect("Failed to create wallet");
repo.remove_wallet(mint_url.clone(), CurrencyUnit::Sat)
.await
.expect("Failed to remove wallet");
assert!(!repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
assert!(localstore
.get_mint(mint_url.clone())
.await
.unwrap()
.is_some());
}
}