use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{bail, Result};
use bip39::rand::{thread_rng, Rng};
use bip39::Mnemonic;
use cdk::cdk_database;
use cdk::cdk_database::WalletDatabase;
use cdk::nuts::CurrencyUnit;
#[cfg(feature = "redb")]
use cdk_redb::WalletRedbDatabase;
use cdk_sqlite::WalletSqliteDatabase;
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
use clap::ValueEnum;
use clap::{Parser, Subcommand};
use tracing::Level;
use tracing_subscriber::EnvFilter;
use url::Url;
mod nostr_storage;
mod sub_commands;
mod token_storage;
mod utils;
const DEFAULT_WORK_DIR: &str = ".cdk-cli";
const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
#[derive(Copy, Clone, Debug, ValueEnum)]
enum TorToggle {
On,
Off,
}
#[derive(Parser)]
#[command(name = "cdk-cli", author = "thesimplekid <tsk@thesimplekid.com>", version = CARGO_PKG_VERSION.unwrap_or("Unknown"), about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = "sqlite")]
engine: String,
#[cfg(feature = "sqlcipher")]
#[arg(long)]
password: Option<String>,
#[arg(short, long)]
work_dir: Option<PathBuf>,
#[arg(short, long, default_value = "error")]
log_level: Level,
#[arg(short, long)]
proxy: Option<Url>,
#[arg(short, long, default_value = "sat")]
unit: String,
#[cfg(feature = "npubcash")]
#[arg(long, default_value = "https://npubx.cash")]
npubcash_url: String,
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
#[arg(long = "tor", value_enum, default_value_t = TorToggle::On)]
transport: TorToggle,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
DecodeToken(sub_commands::decode_token::DecodeTokenSubCommand),
Balance,
Melt(sub_commands::melt::MeltSubCommand),
MintPending,
Receive(sub_commands::receive::ReceiveSubCommand),
Send(sub_commands::send::SendSubCommand),
Transfer(sub_commands::transfer::TransferSubCommand),
CheckPending,
CheckRequests,
MintInfo(sub_commands::mint_info::MintInfoSubcommand),
Mint(sub_commands::mint::MintSubCommand),
MintBatch(sub_commands::mint_batch::MintBatchSubCommand),
Burn(sub_commands::burn::BurnSubCommand),
Restore(sub_commands::restore::RestoreSubCommand),
UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand),
ListMintProofs,
DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand),
PayRequest(sub_commands::pay_request::PayRequestSubCommand),
Resolve(sub_commands::resolve::ResolveSubCommand),
CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
CatLogin(sub_commands::cat_login::CatLoginSubCommand),
CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
#[cfg(feature = "npubcash")]
NpubCash {
#[arg(short, long)]
mint_url: String,
#[command(subcommand)]
command: sub_commands::npubcash::NpubCashSubCommand,
},
GeneratePublicKey(sub_commands::generate_public_key::GeneratePublicKeySubCommand),
GetPublicKeys(sub_commands::get_public_keys::GetPublicKeysSubCommand),
}
#[tokio::main]
async fn main() -> Result<()> {
let args: Cli = Cli::parse();
let default_filter = args.log_level;
let filter = "rustls=warn,hyper_util=warn,reqwest=warn";
let env_filter = EnvFilter::new(format!("{default_filter},{filter}"));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_ansi(false)
.init();
let work_dir = match &args.work_dir {
Some(work_dir) => work_dir.clone(),
None => {
let home_dir = home::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
home_dir.join(DEFAULT_WORK_DIR)
}
};
if !work_dir.exists() {
fs::create_dir_all(&work_dir)?;
}
let localstore: Arc<dyn WalletDatabase<cdk_database::Error> + Send + Sync> =
match args.engine.as_str() {
"sqlite" => {
let sql_path = work_dir.join("cdk-cli.sqlite");
#[cfg(not(feature = "sqlcipher"))]
let sql = WalletSqliteDatabase::new(&sql_path).await?;
#[cfg(feature = "sqlcipher")]
let sql = {
match args.password {
Some(pass) => WalletSqliteDatabase::new((sql_path, pass)).await?,
None => bail!("Missing database password"),
}
};
Arc::new(sql)
}
"redb" => {
#[cfg(feature = "redb")]
{
let redb_path = work_dir.join("cdk-cli.redb");
Arc::new(WalletRedbDatabase::new(&redb_path)?)
}
#[cfg(not(feature = "redb"))]
{
bail!("redb feature not enabled");
}
}
_ => bail!("Unknown DB engine"),
};
let seed_path = work_dir.join("seed");
let mnemonic = match fs::metadata(seed_path.clone()) {
Ok(_) => {
let contents = fs::read_to_string(seed_path.clone())?;
Mnemonic::from_str(&contents)?
}
Err(_e) => {
let mut rng = thread_rng();
let random_bytes: [u8; 32] = rng.gen();
let mnemonic = Mnemonic::from_entropy(&random_bytes)?;
tracing::info!("Creating new seed");
fs::write(seed_path, mnemonic.to_string())?;
mnemonic
}
};
let seed = mnemonic.to_seed_normalized("");
let currency_unit = CurrencyUnit::from_str(&args.unit)
.unwrap_or_else(|_| CurrencyUnit::Custom(args.unit.clone()));
let wallet_repository = {
let mut builder = cdk::wallet::WalletRepositoryBuilder::new()
.localstore(localstore.clone())
.seed(seed);
if let Some(proxy_url) = &args.proxy {
builder = builder.proxy_url(proxy_url.clone());
}
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
if matches!(args.transport, TorToggle::On) {
builder = builder.tor();
}
builder.build().await?
};
let wallets = wallet_repository.get_wallets().await;
for wallet in wallets {
let recovery = wallet.recover_incomplete_sagas().await?;
println!(
"Recovered {} operations, {} compensated, {} skipped, {} failed",
recovery.recovered, recovery.compensated, recovery.skipped, recovery.failed
);
}
match &args.command {
Commands::DecodeToken(sub_command_args) => {
sub_commands::decode_token::decode_token(sub_command_args)
}
Commands::Balance => sub_commands::balance::balance(&wallet_repository).await,
Commands::Melt(sub_command_args) => {
sub_commands::melt::pay(&wallet_repository, sub_command_args, ¤cy_unit).await
}
Commands::Receive(sub_command_args) => {
sub_commands::receive::receive(
&wallet_repository,
sub_command_args,
&work_dir,
¤cy_unit,
)
.await
}
Commands::Send(sub_command_args) => {
sub_commands::send::send(&wallet_repository, sub_command_args, ¤cy_unit).await
}
Commands::Transfer(sub_command_args) => {
sub_commands::transfer::transfer(&wallet_repository, sub_command_args, ¤cy_unit)
.await
}
Commands::CheckPending => {
sub_commands::check_pending::check_pending(&wallet_repository).await
}
Commands::CheckRequests => {
sub_commands::check_requests::check_requests(&wallet_repository).await
}
Commands::MintInfo(sub_command_args) => {
sub_commands::mint_info::mint_info(args.proxy, sub_command_args).await
}
Commands::Mint(sub_command_args) => {
sub_commands::mint::mint(&wallet_repository, sub_command_args, ¤cy_unit).await
}
Commands::MintBatch(sub_command_args) => {
sub_commands::mint_batch::mint_batch(
&wallet_repository,
sub_command_args,
¤cy_unit,
)
.await
}
Commands::MintPending => {
sub_commands::pending_mints::mint_pending(&wallet_repository).await
}
Commands::Burn(sub_command_args) => {
sub_commands::burn::burn(&wallet_repository, sub_command_args).await
}
Commands::Restore(sub_command_args) => {
sub_commands::restore::restore(&wallet_repository, sub_command_args, ¤cy_unit)
.await
}
Commands::UpdateMintUrl(sub_command_args) => {
sub_commands::update_mint_url::update_mint_url(
&wallet_repository,
sub_command_args,
¤cy_unit,
)
.await
}
Commands::ListMintProofs => {
sub_commands::list_mint_proofs::proofs(&wallet_repository).await
}
Commands::DecodeRequest(sub_command_args) => {
sub_commands::decode_request::decode_payment_request(sub_command_args)
}
Commands::PayRequest(sub_command_args) => {
sub_commands::pay_request::pay_request(&wallet_repository, sub_command_args).await
}
Commands::Resolve(sub_command_args) => {
sub_commands::resolve::resolve(&wallet_repository, sub_command_args, ¤cy_unit)
.await
}
Commands::CreateRequest(sub_command_args) => {
sub_commands::create_request::create_request(
&wallet_repository,
sub_command_args,
¤cy_unit,
)
.await
}
Commands::MintBlindAuth(sub_command_args) => {
sub_commands::mint_blind_auth::mint_blind_auth(
&wallet_repository,
sub_command_args,
&work_dir,
¤cy_unit,
)
.await
}
Commands::CatLogin(sub_command_args) => {
sub_commands::cat_login::cat_login(&wallet_repository, sub_command_args, &work_dir)
.await
}
Commands::CatDeviceLogin(sub_command_args) => {
sub_commands::cat_device_login::cat_device_login(
&wallet_repository,
sub_command_args,
&work_dir,
)
.await
}
#[cfg(feature = "npubcash")]
Commands::NpubCash { mint_url, command } => {
sub_commands::npubcash::npubcash(
&wallet_repository,
mint_url,
command,
Some(args.npubcash_url.clone()),
)
.await
}
Commands::GeneratePublicKey(sub_command_args) => {
sub_commands::generate_public_key::generate_public_key(
&wallet_repository,
sub_command_args,
¤cy_unit,
)
.await
}
Commands::GetPublicKeys(sub_command_args) => {
sub_commands::get_public_keys::get_public_keys(
&wallet_repository,
sub_command_args,
¤cy_unit,
)
.await
}
}
}