main.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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::MultiMintWallet;
  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. /// Use Tor transport (only when built with --features tor). Defaults to 'on' when feature is enabled.
  57. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  58. #[arg(long = "tor", value_enum, default_value_t = TorToggle::On)]
  59. transport: TorToggle,
  60. /// Subcommand to run
  61. #[command(subcommand)]
  62. command: Commands,
  63. }
  64. #[derive(Subcommand)]
  65. enum Commands {
  66. /// Decode a token
  67. DecodeToken(sub_commands::decode_token::DecodeTokenSubCommand),
  68. /// Balance
  69. Balance,
  70. /// Pay bolt11 invoice
  71. Melt(sub_commands::melt::MeltSubCommand),
  72. /// Claim pending mint quotes that have been paid
  73. MintPending,
  74. /// Receive token
  75. Receive(sub_commands::receive::ReceiveSubCommand),
  76. /// Send
  77. Send(sub_commands::send::SendSubCommand),
  78. /// Transfer tokens between mints
  79. Transfer(sub_commands::transfer::TransferSubCommand),
  80. /// Reclaim pending proofs that are no longer pending
  81. CheckPending,
  82. /// View mint info
  83. MintInfo(sub_commands::mint_info::MintInfoSubcommand),
  84. /// Mint proofs via bolt11
  85. Mint(sub_commands::mint::MintSubCommand),
  86. /// Burn Spent tokens
  87. Burn(sub_commands::burn::BurnSubCommand),
  88. /// Restore proofs from seed
  89. Restore(sub_commands::restore::RestoreSubCommand),
  90. /// Update Mint Url
  91. UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand),
  92. /// Get proofs from mint.
  93. ListMintProofs,
  94. /// Decode a payment request
  95. DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand),
  96. /// Pay a payment request
  97. PayRequest(sub_commands::pay_request::PayRequestSubCommand),
  98. /// Create Payment request
  99. CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
  100. /// Mint blind auth proofs
  101. MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
  102. /// Cat login with username/password
  103. CatLogin(sub_commands::cat_login::CatLoginSubCommand),
  104. /// Cat login with device code flow
  105. CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
  106. }
  107. #[tokio::main]
  108. async fn main() -> Result<()> {
  109. let args: Cli = Cli::parse();
  110. let default_filter = args.log_level;
  111. let filter = "rustls=warn,hyper_util=warn,reqwest=warn";
  112. let env_filter = EnvFilter::new(format!("{default_filter},{filter}"));
  113. // Parse input
  114. tracing_subscriber::fmt().with_env_filter(env_filter).init();
  115. let work_dir = match &args.work_dir {
  116. Some(work_dir) => work_dir.clone(),
  117. None => {
  118. let home_dir = home::home_dir().unwrap();
  119. home_dir.join(DEFAULT_WORK_DIR)
  120. }
  121. };
  122. // Create work directory if it doesn't exist
  123. if !work_dir.exists() {
  124. fs::create_dir_all(&work_dir)?;
  125. }
  126. let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
  127. match args.engine.as_str() {
  128. "sqlite" => {
  129. let sql_path = work_dir.join("cdk-cli.sqlite");
  130. #[cfg(not(feature = "sqlcipher"))]
  131. let sql = WalletSqliteDatabase::new(&sql_path).await?;
  132. #[cfg(feature = "sqlcipher")]
  133. let sql = {
  134. match args.password {
  135. Some(pass) => WalletSqliteDatabase::new((sql_path, pass)).await?,
  136. None => bail!("Missing database password"),
  137. }
  138. };
  139. Arc::new(sql)
  140. }
  141. "redb" => {
  142. #[cfg(feature = "redb")]
  143. {
  144. let redb_path = work_dir.join("cdk-cli.redb");
  145. Arc::new(WalletRedbDatabase::new(&redb_path)?)
  146. }
  147. #[cfg(not(feature = "redb"))]
  148. {
  149. bail!("redb feature not enabled");
  150. }
  151. }
  152. _ => bail!("Unknown DB engine"),
  153. };
  154. let seed_path = work_dir.join("seed");
  155. let mnemonic = match fs::metadata(seed_path.clone()) {
  156. Ok(_) => {
  157. let contents = fs::read_to_string(seed_path.clone())?;
  158. Mnemonic::from_str(&contents)?
  159. }
  160. Err(_e) => {
  161. let mut rng = thread_rng();
  162. let random_bytes: [u8; 32] = rng.gen();
  163. let mnemonic = Mnemonic::from_entropy(&random_bytes)?;
  164. tracing::info!("Creating new seed");
  165. fs::write(seed_path, mnemonic.to_string())?;
  166. mnemonic
  167. }
  168. };
  169. let seed = mnemonic.to_seed_normalized("");
  170. // Parse currency unit from args
  171. let currency_unit = CurrencyUnit::from_str(&args.unit)
  172. .unwrap_or_else(|_| CurrencyUnit::Custom(args.unit.clone()));
  173. // Create MultiMintWallet with specified currency unit
  174. // The constructor will automatically load wallets for this currency unit
  175. let multi_mint_wallet = match &args.proxy {
  176. Some(proxy_url) => {
  177. MultiMintWallet::new_with_proxy(
  178. localstore.clone(),
  179. seed,
  180. currency_unit.clone(),
  181. proxy_url.clone(),
  182. )
  183. .await?
  184. }
  185. None => {
  186. #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
  187. {
  188. match args.transport {
  189. TorToggle::On => {
  190. MultiMintWallet::new_with_tor(
  191. localstore.clone(),
  192. seed,
  193. currency_unit.clone(),
  194. )
  195. .await?
  196. }
  197. TorToggle::Off => {
  198. MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone())
  199. .await?
  200. }
  201. }
  202. }
  203. #[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
  204. {
  205. MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone()).await?
  206. }
  207. }
  208. };
  209. match &args.command {
  210. Commands::DecodeToken(sub_command_args) => {
  211. sub_commands::decode_token::decode_token(sub_command_args)
  212. }
  213. Commands::Balance => sub_commands::balance::balance(&multi_mint_wallet).await,
  214. Commands::Melt(sub_command_args) => {
  215. sub_commands::melt::pay(&multi_mint_wallet, sub_command_args).await
  216. }
  217. Commands::Receive(sub_command_args) => {
  218. sub_commands::receive::receive(&multi_mint_wallet, sub_command_args, &work_dir).await
  219. }
  220. Commands::Send(sub_command_args) => {
  221. sub_commands::send::send(&multi_mint_wallet, sub_command_args).await
  222. }
  223. Commands::Transfer(sub_command_args) => {
  224. sub_commands::transfer::transfer(&multi_mint_wallet, sub_command_args).await
  225. }
  226. Commands::CheckPending => {
  227. sub_commands::check_pending::check_pending(&multi_mint_wallet).await
  228. }
  229. Commands::MintInfo(sub_command_args) => {
  230. sub_commands::mint_info::mint_info(args.proxy, sub_command_args).await
  231. }
  232. Commands::Mint(sub_command_args) => {
  233. sub_commands::mint::mint(&multi_mint_wallet, sub_command_args).await
  234. }
  235. Commands::MintPending => {
  236. sub_commands::pending_mints::mint_pending(&multi_mint_wallet).await
  237. }
  238. Commands::Burn(sub_command_args) => {
  239. sub_commands::burn::burn(&multi_mint_wallet, sub_command_args).await
  240. }
  241. Commands::Restore(sub_command_args) => {
  242. sub_commands::restore::restore(&multi_mint_wallet, sub_command_args).await
  243. }
  244. Commands::UpdateMintUrl(sub_command_args) => {
  245. sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args)
  246. .await
  247. }
  248. Commands::ListMintProofs => {
  249. sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).await
  250. }
  251. Commands::DecodeRequest(sub_command_args) => {
  252. sub_commands::decode_request::decode_payment_request(sub_command_args)
  253. }
  254. Commands::PayRequest(sub_command_args) => {
  255. sub_commands::pay_request::pay_request(&multi_mint_wallet, sub_command_args).await
  256. }
  257. Commands::CreateRequest(sub_command_args) => {
  258. sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
  259. }
  260. Commands::MintBlindAuth(sub_command_args) => {
  261. sub_commands::mint_blind_auth::mint_blind_auth(
  262. &multi_mint_wallet,
  263. sub_command_args,
  264. &work_dir,
  265. )
  266. .await
  267. }
  268. Commands::CatLogin(sub_command_args) => {
  269. sub_commands::cat_login::cat_login(&multi_mint_wallet, sub_command_args, &work_dir)
  270. .await
  271. }
  272. Commands::CatDeviceLogin(sub_command_args) => {
  273. sub_commands::cat_device_login::cat_device_login(
  274. &multi_mint_wallet,
  275. sub_command_args,
  276. &work_dir,
  277. )
  278. .await
  279. }
  280. }
  281. }