main.rs 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. use std::fs;
  2. use std::path::PathBuf;
  3. use std::str::FromStr;
  4. use std::sync::Arc;
  5. use anyhow::{bail, Result};
  6. use bip39::rand::{thread_rng, Rng};
  7. use bip39::Mnemonic;
  8. use cdk::cdk_database;
  9. use cdk::cdk_database::WalletDatabase;
  10. use cdk::nuts::CurrencyUnit;
  11. use cdk::wallet::{HttpClient, MultiMintWallet, Wallet, WalletBuilder};
  12. #[cfg(feature = "redb")]
  13. use cdk_redb::WalletRedbDatabase;
  14. use cdk_sqlite::WalletSqliteDatabase;
  15. use clap::{Parser, Subcommand};
  16. use tracing::Level;
  17. use tracing_subscriber::EnvFilter;
  18. use url::Url;
  19. #[cfg(feature = "bip353")]
  20. mod bip353;
  21. mod nostr_storage;
  22. mod sub_commands;
  23. mod token_storage;
  24. mod utils;
  25. const DEFAULT_WORK_DIR: &str = ".cdk-cli";
  26. const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
  27. /// Simple CLI application to interact with cashu
  28. #[derive(Parser)]
  29. #[command(name = "cdk-cli")]
  30. #[command(author = "thesimplekid <tsk@thesimplekid.com>")]
  31. #[command(version = CARGO_PKG_VERSION.unwrap_or("Unknown"))]
  32. #[command(author, version, about, long_about = None)]
  33. struct Cli {
  34. /// Database engine to use (sqlite/redb)
  35. #[arg(short, long, default_value = "sqlite")]
  36. engine: String,
  37. /// Database password for sqlcipher
  38. #[cfg(feature = "sqlcipher")]
  39. #[arg(long)]
  40. password: Option<String>,
  41. /// Path to working dir
  42. #[arg(short, long)]
  43. work_dir: Option<PathBuf>,
  44. /// Logging level
  45. #[arg(short, long, default_value = "error")]
  46. log_level: Level,
  47. /// NWS Proxy
  48. #[arg(short, long)]
  49. proxy: Option<Url>,
  50. #[command(subcommand)]
  51. command: Commands,
  52. }
  53. #[derive(Subcommand)]
  54. enum Commands {
  55. /// Decode a token
  56. DecodeToken(sub_commands::decode_token::DecodeTokenSubCommand),
  57. /// Balance
  58. Balance,
  59. /// Pay bolt11 invoice
  60. Melt(sub_commands::melt::MeltSubCommand),
  61. /// Claim pending mint quotes that have been paid
  62. MintPending,
  63. /// Receive token
  64. Receive(sub_commands::receive::ReceiveSubCommand),
  65. /// Send
  66. Send(sub_commands::send::SendSubCommand),
  67. /// Reclaim pending proofs that are no longer pending
  68. CheckPending,
  69. /// View mint info
  70. MintInfo(sub_commands::mint_info::MintInfoSubcommand),
  71. /// Mint proofs via bolt11
  72. Mint(sub_commands::mint::MintSubCommand),
  73. /// Burn Spent tokens
  74. Burn(sub_commands::burn::BurnSubCommand),
  75. /// Restore proofs from seed
  76. Restore(sub_commands::restore::RestoreSubCommand),
  77. /// Update Mint Url
  78. UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand),
  79. /// Get proofs from mint.
  80. ListMintProofs,
  81. /// Decode a payment request
  82. DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand),
  83. /// Pay a payment request
  84. PayRequest(sub_commands::pay_request::PayRequestSubCommand),
  85. /// Create Payment request
  86. CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
  87. /// Mint blind auth proofs
  88. MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
  89. /// Cat login with username/password
  90. CatLogin(sub_commands::cat_login::CatLoginSubCommand),
  91. /// Cat login with device code flow
  92. CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
  93. }
  94. #[tokio::main]
  95. async fn main() -> Result<()> {
  96. let args: Cli = Cli::parse();
  97. let default_filter = args.log_level;
  98. let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn";
  99. let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter}"));
  100. // Parse input
  101. tracing_subscriber::fmt().with_env_filter(env_filter).init();
  102. let work_dir = match &args.work_dir {
  103. Some(work_dir) => work_dir.clone(),
  104. None => {
  105. let home_dir = home::home_dir().unwrap();
  106. home_dir.join(DEFAULT_WORK_DIR)
  107. }
  108. };
  109. fs::create_dir_all(&work_dir)?;
  110. let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
  111. match args.engine.as_str() {
  112. "sqlite" => {
  113. let sql_path = work_dir.join("cdk-cli.sqlite");
  114. #[cfg(not(feature = "sqlcipher"))]
  115. let sql = WalletSqliteDatabase::new(&sql_path).await?;
  116. #[cfg(feature = "sqlcipher")]
  117. let sql = {
  118. match args.password {
  119. Some(pass) => WalletSqliteDatabase::new(&sql_path, pass).await?,
  120. None => bail!("Missing database password"),
  121. }
  122. };
  123. Arc::new(sql)
  124. }
  125. "redb" => {
  126. #[cfg(feature = "redb")]
  127. {
  128. let redb_path = work_dir.join("cdk-cli.redb");
  129. Arc::new(WalletRedbDatabase::new(&redb_path)?)
  130. }
  131. #[cfg(not(feature = "redb"))]
  132. {
  133. bail!("redb feature not enabled");
  134. }
  135. }
  136. _ => bail!("Unknown DB engine"),
  137. };
  138. let seed_path = work_dir.join("seed");
  139. let mnemonic = match fs::metadata(seed_path.clone()) {
  140. Ok(_) => {
  141. let contents = fs::read_to_string(seed_path.clone())?;
  142. Mnemonic::from_str(&contents)?
  143. }
  144. Err(_e) => {
  145. let mut rng = thread_rng();
  146. let random_bytes: [u8; 32] = rng.gen();
  147. let mnemonic = Mnemonic::from_entropy(&random_bytes)?;
  148. tracing::info!("Creating new seed");
  149. fs::write(seed_path, mnemonic.to_string())?;
  150. mnemonic
  151. }
  152. };
  153. let seed = mnemonic.to_seed_normalized("");
  154. let mut wallets: Vec<Wallet> = Vec::new();
  155. let mints = localstore.get_mints().await?;
  156. for (mint_url, mint_info) in mints {
  157. let units = if let Some(mint_info) = mint_info {
  158. mint_info.supported_units().into_iter().cloned().collect()
  159. } else {
  160. vec![CurrencyUnit::Sat]
  161. };
  162. let proxy_client = if let Some(proxy_url) = args.proxy.as_ref() {
  163. Some(HttpClient::with_proxy(
  164. mint_url.clone(),
  165. proxy_url.clone(),
  166. None,
  167. true,
  168. )?)
  169. } else {
  170. None
  171. };
  172. let seed = mnemonic.to_seed_normalized("");
  173. for unit in units {
  174. let mint_url_clone = mint_url.clone();
  175. let mut builder = WalletBuilder::new()
  176. .mint_url(mint_url_clone.clone())
  177. .unit(unit)
  178. .localstore(localstore.clone())
  179. .seed(&seed);
  180. if let Some(http_client) = &proxy_client {
  181. builder = builder.client(http_client.clone());
  182. }
  183. let wallet = builder.build()?;
  184. let wallet_clone = wallet.clone();
  185. tokio::spawn(async move {
  186. if let Err(err) = wallet_clone.get_mint_info().await {
  187. tracing::error!(
  188. "Could not get mint quote for {}, {}",
  189. wallet_clone.mint_url,
  190. err
  191. );
  192. }
  193. });
  194. wallets.push(wallet);
  195. }
  196. }
  197. let multi_mint_wallet = MultiMintWallet::new(localstore, Arc::new(seed), wallets);
  198. match &args.command {
  199. Commands::DecodeToken(sub_command_args) => {
  200. sub_commands::decode_token::decode_token(sub_command_args)
  201. }
  202. Commands::Balance => sub_commands::balance::balance(&multi_mint_wallet).await,
  203. Commands::Melt(sub_command_args) => {
  204. sub_commands::melt::pay(&multi_mint_wallet, sub_command_args).await
  205. }
  206. Commands::Receive(sub_command_args) => {
  207. sub_commands::receive::receive(&multi_mint_wallet, sub_command_args, &work_dir).await
  208. }
  209. Commands::Send(sub_command_args) => {
  210. sub_commands::send::send(&multi_mint_wallet, sub_command_args).await
  211. }
  212. Commands::CheckPending => {
  213. sub_commands::check_pending::check_pending(&multi_mint_wallet).await
  214. }
  215. Commands::MintInfo(sub_command_args) => {
  216. sub_commands::mint_info::mint_info(args.proxy, sub_command_args).await
  217. }
  218. Commands::Mint(sub_command_args) => {
  219. sub_commands::mint::mint(&multi_mint_wallet, sub_command_args).await
  220. }
  221. Commands::MintPending => {
  222. sub_commands::pending_mints::mint_pending(&multi_mint_wallet).await
  223. }
  224. Commands::Burn(sub_command_args) => {
  225. sub_commands::burn::burn(&multi_mint_wallet, sub_command_args).await
  226. }
  227. Commands::Restore(sub_command_args) => {
  228. sub_commands::restore::restore(&multi_mint_wallet, sub_command_args).await
  229. }
  230. Commands::UpdateMintUrl(sub_command_args) => {
  231. sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args)
  232. .await
  233. }
  234. Commands::ListMintProofs => {
  235. sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).await
  236. }
  237. Commands::DecodeRequest(sub_command_args) => {
  238. sub_commands::decode_request::decode_payment_request(sub_command_args)
  239. }
  240. Commands::PayRequest(sub_command_args) => {
  241. sub_commands::pay_request::pay_request(&multi_mint_wallet, sub_command_args).await
  242. }
  243. Commands::CreateRequest(sub_command_args) => {
  244. sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
  245. }
  246. Commands::MintBlindAuth(sub_command_args) => {
  247. sub_commands::mint_blind_auth::mint_blind_auth(
  248. &multi_mint_wallet,
  249. sub_command_args,
  250. &work_dir,
  251. )
  252. .await
  253. }
  254. Commands::CatLogin(sub_command_args) => {
  255. sub_commands::cat_login::cat_login(&multi_mint_wallet, sub_command_args, &work_dir)
  256. .await
  257. }
  258. Commands::CatDeviceLogin(sub_command_args) => {
  259. sub_commands::cat_device_login::cat_device_login(
  260. &multi_mint_wallet,
  261. sub_command_args,
  262. &work_dir,
  263. )
  264. .await
  265. }
  266. }
  267. }