main.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. //! CDK CLI
  2. use std::fs;
  3. use std::path::PathBuf;
  4. use std::str::FromStr;
  5. use std::sync::Arc;
  6. use anyhow::{bail, Result};
  7. use bip39::rand::{thread_rng, Rng};
  8. use bip39::Mnemonic;
  9. use cdk::cdk_database;
  10. use cdk::cdk_database::WalletDatabase;
  11. use cdk::nuts::CurrencyUnit;
  12. #[cfg(feature = "redb")]
  13. use cdk_redb::WalletRedbDatabase;
  14. use cdk_sqlite::WalletSqliteDatabase;
  15. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  16. use clap::ValueEnum;
  17. use clap::{Parser, Subcommand};
  18. use tracing::Level;
  19. use tracing_subscriber::EnvFilter;
  20. use url::Url;
  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. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  29. #[derive(Copy, Clone, Debug, ValueEnum)]
  30. enum TorToggle {
  31. On,
  32. Off,
  33. }
  34. #[derive(Parser)]
  35. #[command(name = "cdk-cli", author = "thesimplekid <tsk@thesimplekid.com>", version = CARGO_PKG_VERSION.unwrap_or("Unknown"), about, long_about = None)]
  36. struct Cli {
  37. /// Database engine to use (sqlite/redb)
  38. #[arg(short, long, default_value = "sqlite")]
  39. engine: String,
  40. /// Database password for sqlcipher
  41. #[cfg(feature = "sqlcipher")]
  42. #[arg(long)]
  43. password: Option<String>,
  44. /// Path to working dir
  45. #[arg(short, long)]
  46. work_dir: Option<PathBuf>,
  47. /// Logging level
  48. #[arg(short, long, default_value = "error")]
  49. log_level: Level,
  50. /// NWS Proxy
  51. #[arg(short, long)]
  52. proxy: Option<Url>,
  53. /// Currency unit to use for the wallet
  54. #[arg(short, long, default_value = "sat")]
  55. unit: String,
  56. /// NpubCash API URL
  57. #[cfg(feature = "npubcash")]
  58. #[arg(long, default_value = "https://npubx.cash")]
  59. npubcash_url: String,
  60. /// Use Tor transport (only when built with --features tor). Defaults to 'on' when feature is enabled.
  61. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  62. #[arg(long = "tor", value_enum, default_value_t = TorToggle::On)]
  63. transport: TorToggle,
  64. /// Subcommand to run
  65. #[command(subcommand)]
  66. command: Commands,
  67. }
  68. #[derive(Subcommand)]
  69. enum Commands {
  70. /// Decode a token
  71. DecodeToken(sub_commands::decode_token::DecodeTokenSubCommand),
  72. /// Balance
  73. Balance,
  74. /// Pay bolt11 invoice
  75. Melt(sub_commands::melt::MeltSubCommand),
  76. /// Claim pending mint quotes that have been paid
  77. MintPending,
  78. /// Receive token
  79. Receive(sub_commands::receive::ReceiveSubCommand),
  80. /// Send
  81. Send(sub_commands::send::SendSubCommand),
  82. /// Transfer tokens between mints
  83. Transfer(sub_commands::transfer::TransferSubCommand),
  84. /// Reclaim pending proofs that are no longer pending
  85. CheckPending,
  86. /// View mint info
  87. MintInfo(sub_commands::mint_info::MintInfoSubcommand),
  88. /// Mint proofs via bolt11
  89. Mint(sub_commands::mint::MintSubCommand),
  90. /// Burn Spent tokens
  91. Burn(sub_commands::burn::BurnSubCommand),
  92. /// Restore proofs from seed
  93. Restore(sub_commands::restore::RestoreSubCommand),
  94. /// Update Mint Url
  95. UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand),
  96. /// Get proofs from mint.
  97. ListMintProofs,
  98. /// Decode a payment request
  99. DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand),
  100. /// Pay a payment request
  101. PayRequest(sub_commands::pay_request::PayRequestSubCommand),
  102. /// Create Payment request
  103. CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
  104. /// Mint blind auth proofs
  105. MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
  106. /// Cat login with username/password
  107. CatLogin(sub_commands::cat_login::CatLoginSubCommand),
  108. /// Cat login with device code flow
  109. CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
  110. /// NpubCash integration commands
  111. #[cfg(feature = "npubcash")]
  112. NpubCash {
  113. /// Mint URL to use for npubcash operations
  114. #[arg(short, long)]
  115. mint_url: String,
  116. #[command(subcommand)]
  117. command: sub_commands::npubcash::NpubCashSubCommand,
  118. },
  119. }
  120. #[tokio::main]
  121. async fn main() -> Result<()> {
  122. let args: Cli = Cli::parse();
  123. let default_filter = args.log_level;
  124. let filter = "rustls=warn,hyper_util=warn,reqwest=warn";
  125. let env_filter = EnvFilter::new(format!("{default_filter},{filter}"));
  126. // Parse input
  127. tracing_subscriber::fmt()
  128. .with_env_filter(env_filter)
  129. .with_ansi(false)
  130. .init();
  131. let work_dir = match &args.work_dir {
  132. Some(work_dir) => work_dir.clone(),
  133. None => {
  134. let home_dir = home::home_dir()
  135. .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
  136. home_dir.join(DEFAULT_WORK_DIR)
  137. }
  138. };
  139. // Create work directory if it doesn't exist
  140. if !work_dir.exists() {
  141. fs::create_dir_all(&work_dir)?;
  142. }
  143. let localstore: Arc<dyn WalletDatabase<cdk_database::Error> + Send + Sync> =
  144. match args.engine.as_str() {
  145. "sqlite" => {
  146. let sql_path = work_dir.join("cdk-cli.sqlite");
  147. #[cfg(not(feature = "sqlcipher"))]
  148. let sql = WalletSqliteDatabase::new(&sql_path).await?;
  149. #[cfg(feature = "sqlcipher")]
  150. let sql = {
  151. match args.password {
  152. Some(pass) => WalletSqliteDatabase::new((sql_path, pass)).await?,
  153. None => bail!("Missing database password"),
  154. }
  155. };
  156. Arc::new(sql)
  157. }
  158. "redb" => {
  159. #[cfg(feature = "redb")]
  160. {
  161. let redb_path = work_dir.join("cdk-cli.redb");
  162. Arc::new(WalletRedbDatabase::new(&redb_path)?)
  163. }
  164. #[cfg(not(feature = "redb"))]
  165. {
  166. bail!("redb feature not enabled");
  167. }
  168. }
  169. _ => bail!("Unknown DB engine"),
  170. };
  171. let seed_path = work_dir.join("seed");
  172. let mnemonic = match fs::metadata(seed_path.clone()) {
  173. Ok(_) => {
  174. let contents = fs::read_to_string(seed_path.clone())?;
  175. Mnemonic::from_str(&contents)?
  176. }
  177. Err(_e) => {
  178. let mut rng = thread_rng();
  179. let random_bytes: [u8; 32] = rng.gen();
  180. let mnemonic = Mnemonic::from_entropy(&random_bytes)?;
  181. tracing::info!("Creating new seed");
  182. fs::write(seed_path, mnemonic.to_string())?;
  183. mnemonic
  184. }
  185. };
  186. let seed = mnemonic.to_seed_normalized("");
  187. // Parse currency unit from args
  188. let currency_unit = CurrencyUnit::from_str(&args.unit)
  189. .unwrap_or_else(|_| CurrencyUnit::Custom(args.unit.clone()));
  190. // Create WalletRepository using builder pattern
  191. let wallet_repository = {
  192. let mut builder = cdk::wallet::WalletRepositoryBuilder::new()
  193. .localstore(localstore.clone())
  194. .seed(seed);
  195. if let Some(proxy_url) = &args.proxy {
  196. builder = builder.proxy_url(proxy_url.clone());
  197. }
  198. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  199. if matches!(args.transport, TorToggle::On) {
  200. builder = builder.tor();
  201. }
  202. builder.build().await?
  203. };
  204. match &args.command {
  205. Commands::DecodeToken(sub_command_args) => {
  206. sub_commands::decode_token::decode_token(sub_command_args)
  207. }
  208. Commands::Balance => sub_commands::balance::balance(&wallet_repository).await,
  209. Commands::Melt(sub_command_args) => {
  210. sub_commands::melt::pay(&wallet_repository, sub_command_args, &currency_unit).await
  211. }
  212. Commands::Receive(sub_command_args) => {
  213. sub_commands::receive::receive(
  214. &wallet_repository,
  215. sub_command_args,
  216. &work_dir,
  217. &currency_unit,
  218. )
  219. .await
  220. }
  221. Commands::Send(sub_command_args) => {
  222. sub_commands::send::send(&wallet_repository, sub_command_args, &currency_unit).await
  223. }
  224. Commands::Transfer(sub_command_args) => {
  225. sub_commands::transfer::transfer(&wallet_repository, sub_command_args, &currency_unit)
  226. .await
  227. }
  228. Commands::CheckPending => {
  229. sub_commands::check_pending::check_pending(&wallet_repository).await
  230. }
  231. Commands::MintInfo(sub_command_args) => {
  232. sub_commands::mint_info::mint_info(args.proxy, sub_command_args).await
  233. }
  234. Commands::Mint(sub_command_args) => {
  235. sub_commands::mint::mint(&wallet_repository, sub_command_args, &currency_unit).await
  236. }
  237. Commands::MintPending => {
  238. sub_commands::pending_mints::mint_pending(&wallet_repository).await
  239. }
  240. Commands::Burn(sub_command_args) => {
  241. sub_commands::burn::burn(&wallet_repository, sub_command_args).await
  242. }
  243. Commands::Restore(sub_command_args) => {
  244. sub_commands::restore::restore(&wallet_repository, sub_command_args, &currency_unit)
  245. .await
  246. }
  247. Commands::UpdateMintUrl(sub_command_args) => {
  248. sub_commands::update_mint_url::update_mint_url(
  249. &wallet_repository,
  250. sub_command_args,
  251. &currency_unit,
  252. )
  253. .await
  254. }
  255. Commands::ListMintProofs => {
  256. sub_commands::list_mint_proofs::proofs(&wallet_repository).await
  257. }
  258. Commands::DecodeRequest(sub_command_args) => {
  259. sub_commands::decode_request::decode_payment_request(sub_command_args)
  260. }
  261. Commands::PayRequest(sub_command_args) => {
  262. sub_commands::pay_request::pay_request(&wallet_repository, sub_command_args).await
  263. }
  264. Commands::CreateRequest(sub_command_args) => {
  265. sub_commands::create_request::create_request(
  266. &wallet_repository,
  267. sub_command_args,
  268. &currency_unit,
  269. )
  270. .await
  271. }
  272. Commands::MintBlindAuth(sub_command_args) => {
  273. sub_commands::mint_blind_auth::mint_blind_auth(
  274. &wallet_repository,
  275. sub_command_args,
  276. &work_dir,
  277. &currency_unit,
  278. )
  279. .await
  280. }
  281. Commands::CatLogin(sub_command_args) => {
  282. sub_commands::cat_login::cat_login(&wallet_repository, sub_command_args, &work_dir)
  283. .await
  284. }
  285. Commands::CatDeviceLogin(sub_command_args) => {
  286. sub_commands::cat_device_login::cat_device_login(
  287. &wallet_repository,
  288. sub_command_args,
  289. &work_dir,
  290. )
  291. .await
  292. }
  293. #[cfg(feature = "npubcash")]
  294. Commands::NpubCash { mint_url, command } => {
  295. sub_commands::npubcash::npubcash(
  296. &wallet_repository,
  297. mint_url,
  298. command,
  299. Some(args.npubcash_url.clone()),
  300. )
  301. .await
  302. }
  303. }
  304. }