init_pure_tests.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. use std::collections::{HashMap, HashSet};
  2. use std::fmt::{Debug, Formatter};
  3. use std::path::PathBuf;
  4. use std::str::FromStr;
  5. use std::sync::Arc;
  6. use std::{env, fs};
  7. use anyhow::{anyhow, bail, Result};
  8. use async_trait::async_trait;
  9. use bip39::Mnemonic;
  10. use cashu::quote_id::QuoteId;
  11. use cashu::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
  12. use cdk::amount::SplitTarget;
  13. use cdk::cdk_database::{self, WalletDatabase};
  14. use cdk::mint::{MintBuilder, MintMeltLimits};
  15. use cdk::nuts::nut00::ProofsMethods;
  16. use cdk::nuts::{
  17. CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
  18. MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
  19. MintQuoteBolt11Response, MintRequest, MintResponse, PaymentMethod, RestoreRequest,
  20. RestoreResponse, SwapRequest, SwapResponse,
  21. };
  22. use cdk::types::{FeeReserve, QuoteTTL};
  23. use cdk::util::unix_time;
  24. use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
  25. use cdk::{Amount, Error, Mint, StreamExt};
  26. use cdk_fake_wallet::FakeWallet;
  27. use tokio::sync::RwLock;
  28. use tracing_subscriber::EnvFilter;
  29. use uuid::Uuid;
  30. pub struct DirectMintConnection {
  31. pub mint: Mint,
  32. auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
  33. }
  34. impl DirectMintConnection {
  35. pub fn new(mint: Mint) -> Self {
  36. Self {
  37. mint,
  38. auth_wallet: Arc::new(RwLock::new(None)),
  39. }
  40. }
  41. }
  42. impl Debug for DirectMintConnection {
  43. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  44. write!(f, "DirectMintConnection",)
  45. }
  46. }
  47. /// Implements the generic [MintConnector] (i.e. use the interface that expects to communicate
  48. /// to a generic mint, where we don't know that quote ID's are [Uuid]s) for [DirectMintConnection],
  49. /// where we know we're dealing with a mint that uses [Uuid]s for quotes.
  50. /// Convert the requests and responses between the [String] and [Uuid] variants as necessary.
  51. #[async_trait]
  52. impl MintConnector for DirectMintConnection {
  53. async fn resolve_dns_txt(&self, _domain: &str) -> Result<Vec<String>, Error> {
  54. panic!("Not implemented");
  55. }
  56. async fn fetch_lnurl_pay_request(
  57. &self,
  58. _url: &str,
  59. ) -> Result<cdk::wallet::LnurlPayResponse, Error> {
  60. unimplemented!("Lightning address not supported in DirectMintConnection")
  61. }
  62. async fn fetch_lnurl_invoice(
  63. &self,
  64. _url: &str,
  65. ) -> Result<cdk::wallet::LnurlPayInvoiceResponse, Error> {
  66. unimplemented!("Lightning address not supported in DirectMintConnection")
  67. }
  68. async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
  69. Ok(self.mint.pubkeys().keysets)
  70. }
  71. async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
  72. self.mint.keyset(&keyset_id).ok_or(Error::UnknownKeySet)
  73. }
  74. async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
  75. Ok(self.mint.keysets())
  76. }
  77. async fn post_mint_quote(
  78. &self,
  79. request: MintQuoteBolt11Request,
  80. ) -> Result<MintQuoteBolt11Response<String>, Error> {
  81. self.mint
  82. .get_mint_quote(request.into())
  83. .await
  84. .map(Into::into)
  85. }
  86. async fn get_mint_quote_status(
  87. &self,
  88. quote_id: &str,
  89. ) -> Result<MintQuoteBolt11Response<String>, Error> {
  90. self.mint
  91. .check_mint_quote(&QuoteId::from_str(quote_id)?)
  92. .await
  93. .map(Into::into)
  94. }
  95. async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
  96. let request_id: MintRequest<QuoteId> = request.try_into().unwrap();
  97. self.mint.process_mint_request(request_id).await
  98. }
  99. async fn post_melt_quote(
  100. &self,
  101. request: MeltQuoteBolt11Request,
  102. ) -> Result<MeltQuoteBolt11Response<String>, Error> {
  103. self.mint
  104. .get_melt_quote(request.into())
  105. .await
  106. .map(Into::into)
  107. }
  108. async fn get_melt_quote_status(
  109. &self,
  110. quote_id: &str,
  111. ) -> Result<MeltQuoteBolt11Response<String>, Error> {
  112. self.mint
  113. .check_melt_quote(&QuoteId::from_str(quote_id)?)
  114. .await
  115. .map(Into::into)
  116. }
  117. async fn post_melt(
  118. &self,
  119. request: MeltRequest<String>,
  120. ) -> Result<MeltQuoteBolt11Response<String>, Error> {
  121. let request_uuid = request.try_into().unwrap();
  122. self.mint.melt(&request_uuid).await.map(Into::into)
  123. }
  124. async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
  125. self.mint.process_swap_request(swap_request).await
  126. }
  127. async fn get_mint_info(&self) -> Result<MintInfo, Error> {
  128. Ok(self.mint.mint_info().await?.clone().time(unix_time()))
  129. }
  130. async fn post_check_state(
  131. &self,
  132. request: CheckStateRequest,
  133. ) -> Result<CheckStateResponse, Error> {
  134. self.mint.check_state(&request).await
  135. }
  136. async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
  137. self.mint.restore(request).await
  138. }
  139. /// Get the auth wallet for the client
  140. async fn get_auth_wallet(&self) -> Option<AuthWallet> {
  141. self.auth_wallet.read().await.clone()
  142. }
  143. /// Set auth wallet on client
  144. async fn set_auth_wallet(&self, wallet: Option<AuthWallet>) {
  145. let mut auth_wallet = self.auth_wallet.write().await;
  146. *auth_wallet = wallet;
  147. }
  148. async fn post_mint_bolt12_quote(
  149. &self,
  150. request: MintQuoteBolt12Request,
  151. ) -> Result<MintQuoteBolt12Response<String>, Error> {
  152. let res: MintQuoteBolt12Response<QuoteId> =
  153. self.mint.get_mint_quote(request.into()).await?.try_into()?;
  154. Ok(res.into())
  155. }
  156. async fn get_mint_quote_bolt12_status(
  157. &self,
  158. quote_id: &str,
  159. ) -> Result<MintQuoteBolt12Response<String>, Error> {
  160. let quote: MintQuoteBolt12Response<QuoteId> = self
  161. .mint
  162. .check_mint_quote(&QuoteId::from_str(quote_id)?)
  163. .await?
  164. .try_into()?;
  165. Ok(quote.into())
  166. }
  167. /// Melt Quote [NUT-23]
  168. async fn post_melt_bolt12_quote(
  169. &self,
  170. request: MeltQuoteBolt12Request,
  171. ) -> Result<MeltQuoteBolt11Response<String>, Error> {
  172. self.mint
  173. .get_melt_quote(request.into())
  174. .await
  175. .map(Into::into)
  176. }
  177. /// Melt Quote Status [NUT-23]
  178. async fn get_melt_bolt12_quote_status(
  179. &self,
  180. quote_id: &str,
  181. ) -> Result<MeltQuoteBolt11Response<String>, Error> {
  182. self.mint
  183. .check_melt_quote(&QuoteId::from_str(quote_id)?)
  184. .await
  185. .map(Into::into)
  186. }
  187. /// Melt [NUT-23]
  188. async fn post_melt_bolt12(
  189. &self,
  190. _request: MeltRequest<String>,
  191. ) -> Result<MeltQuoteBolt11Response<String>, Error> {
  192. // Implementation to be added later
  193. Err(Error::UnsupportedPaymentMethod)
  194. }
  195. }
  196. pub fn setup_tracing() {
  197. let default_filter = "debug";
  198. let h2_filter = "h2=warn";
  199. let hyper_filter = "hyper=warn";
  200. let env_filter = EnvFilter::new(format!("{default_filter},{h2_filter},{hyper_filter}"));
  201. // Ok if successful, Err if already initialized
  202. // Allows us to setup tracing at the start of several parallel tests
  203. let _ = tracing_subscriber::fmt()
  204. .with_env_filter(env_filter)
  205. .try_init();
  206. }
  207. pub async fn create_and_start_test_mint() -> Result<Mint> {
  208. // Read environment variable to determine database type
  209. let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
  210. let localstore = match db_type.to_lowercase().as_str() {
  211. "memory" => Arc::new(cdk_sqlite::mint::memory::empty().await?),
  212. _ => {
  213. // Create a temporary directory for SQLite database
  214. let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
  215. let path = temp_dir.join("mint.db").to_str().unwrap().to_string();
  216. Arc::new(
  217. cdk_sqlite::MintSqliteDatabase::new(path.as_str())
  218. .await
  219. .expect("Could not create sqlite db"),
  220. )
  221. }
  222. };
  223. let mut mint_builder = MintBuilder::new(localstore.clone());
  224. let fee_reserve = FeeReserve {
  225. min_fee_reserve: 1.into(),
  226. percent_fee_reserve: 0.02,
  227. };
  228. let ln_fake_backend = FakeWallet::new(
  229. fee_reserve.clone(),
  230. HashMap::default(),
  231. HashSet::default(),
  232. 2,
  233. CurrencyUnit::Sat,
  234. );
  235. mint_builder
  236. .add_payment_processor(
  237. CurrencyUnit::Sat,
  238. PaymentMethod::Bolt11,
  239. MintMeltLimits::new(1, 10_000),
  240. Arc::new(ln_fake_backend),
  241. )
  242. .await?;
  243. let mnemonic = Mnemonic::generate(12)?;
  244. mint_builder = mint_builder
  245. .with_name("pure test mint".to_string())
  246. .with_description("pure test mint".to_string())
  247. .with_urls(vec!["https://aaa".to_string()]);
  248. let quote_ttl = QuoteTTL::new(10000, 10000);
  249. let mint = mint_builder
  250. .build_with_seed(localstore.clone(), &mnemonic.to_seed_normalized(""))
  251. .await?;
  252. mint.set_quote_ttl(quote_ttl).await?;
  253. mint.start().await?;
  254. Ok(mint)
  255. }
  256. pub async fn create_test_wallet_for_mint(mint: Mint) -> Result<Wallet> {
  257. let connector = DirectMintConnection::new(mint.clone());
  258. let mint_info = mint.mint_info().await?;
  259. let mint_url = mint_info
  260. .urls
  261. .as_ref()
  262. .ok_or(anyhow!("Test mint URLs list is unset"))?
  263. .first()
  264. .ok_or(anyhow!("Test mint has empty URLs list"))?;
  265. let seed = Mnemonic::generate(12)?.to_seed_normalized("");
  266. let unit = CurrencyUnit::Sat;
  267. // Read environment variable to determine database type
  268. let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
  269. let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
  270. match db_type.to_lowercase().as_str() {
  271. "sqlite" => {
  272. // Create a temporary directory for SQLite database
  273. let temp_dir = create_temp_dir("cdk-test-sqlite-wallet")?;
  274. let path = temp_dir.join("wallet.db").to_str().unwrap().to_string();
  275. let database = cdk_sqlite::WalletSqliteDatabase::new(path.as_str())
  276. .await
  277. .expect("Could not create sqlite db");
  278. Arc::new(database)
  279. }
  280. "redb" => {
  281. // Create a temporary directory for ReDB database
  282. let temp_dir = create_temp_dir("cdk-test-redb-wallet")?;
  283. let path = temp_dir.join("wallet.redb");
  284. let database = cdk_redb::WalletRedbDatabase::new(&path)
  285. .expect("Could not create redb mint database");
  286. Arc::new(database)
  287. }
  288. "memory" => {
  289. let database = cdk_sqlite::wallet::memory::empty().await?;
  290. Arc::new(database)
  291. }
  292. _ => {
  293. bail!("Db type not set")
  294. }
  295. };
  296. let wallet = WalletBuilder::new()
  297. .mint_url(mint_url.parse().unwrap())
  298. .unit(unit)
  299. .localstore(localstore)
  300. .seed(seed)
  301. .client(connector)
  302. .build()?;
  303. Ok(wallet)
  304. }
  305. /// Creates a mint quote for the given amount and checks its state in a loop. Returns when
  306. /// amount is minted.
  307. /// Creates a temporary directory with a unique name based on the prefix
  308. fn create_temp_dir(prefix: &str) -> Result<PathBuf> {
  309. let temp_dir = env::temp_dir();
  310. let unique_dir = temp_dir.join(format!("{}-{}", prefix, Uuid::new_v4()));
  311. fs::create_dir_all(&unique_dir)?;
  312. Ok(unique_dir)
  313. }
  314. pub async fn fund_wallet(
  315. wallet: Wallet,
  316. amount: u64,
  317. split_target: Option<SplitTarget>,
  318. ) -> Result<Amount> {
  319. let desired_amount = Amount::from(amount);
  320. let quote = wallet.mint_quote(desired_amount, None).await?;
  321. Ok(wallet
  322. .proof_stream(quote, split_target.unwrap_or_default(), None)
  323. .next()
  324. .await
  325. .expect("proofs")?
  326. .total_amount()?)
  327. }