main.rs 8.9 KB

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