mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-04 12:45:55 +01:00
* feat(cdk): add Bolt12 mint quote subscription support Extends subscription to handle Bolt12 payment method alongside existing Bolt11 support across wallet, mint, and CLI components.
300 lines
10 KiB
Rust
300 lines
10 KiB
Rust
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;
|
|
use cdk::wallet::{HttpClient, MultiMintWallet, Wallet, WalletBuilder};
|
|
#[cfg(feature = "redb")]
|
|
use cdk_redb::WalletRedbDatabase;
|
|
use cdk_sqlite::WalletSqliteDatabase;
|
|
use clap::{Parser, Subcommand};
|
|
use tracing::Level;
|
|
use tracing_subscriber::EnvFilter;
|
|
use url::Url;
|
|
|
|
#[cfg(feature = "bip353")]
|
|
mod bip353;
|
|
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");
|
|
|
|
/// Simple CLI application to interact with cashu
|
|
#[derive(Parser)]
|
|
#[command(name = "cdk-cli")]
|
|
#[command(author = "thesimplekid <tsk@thesimplekid.com>")]
|
|
#[command(version = CARGO_PKG_VERSION.unwrap_or("Unknown"))]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct Cli {
|
|
/// Database engine to use (sqlite/redb)
|
|
#[arg(short, long, default_value = "sqlite")]
|
|
engine: String,
|
|
/// Database password for sqlcipher
|
|
#[cfg(feature = "sqlcipher")]
|
|
#[arg(long)]
|
|
password: Option<String>,
|
|
/// Path to working dir
|
|
#[arg(short, long)]
|
|
work_dir: Option<PathBuf>,
|
|
/// Logging level
|
|
#[arg(short, long, default_value = "error")]
|
|
log_level: Level,
|
|
/// NWS Proxy
|
|
#[arg(short, long)]
|
|
proxy: Option<Url>,
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Commands {
|
|
/// Decode a token
|
|
DecodeToken(sub_commands::decode_token::DecodeTokenSubCommand),
|
|
/// Balance
|
|
Balance,
|
|
/// Pay bolt11 invoice
|
|
Melt(sub_commands::melt::MeltSubCommand),
|
|
/// Claim pending mint quotes that have been paid
|
|
MintPending,
|
|
/// Receive token
|
|
Receive(sub_commands::receive::ReceiveSubCommand),
|
|
/// Send
|
|
Send(sub_commands::send::SendSubCommand),
|
|
/// Reclaim pending proofs that are no longer pending
|
|
CheckPending,
|
|
/// View mint info
|
|
MintInfo(sub_commands::mint_info::MintInfoSubcommand),
|
|
/// Mint proofs via bolt11
|
|
Mint(sub_commands::mint::MintSubCommand),
|
|
/// Burn Spent tokens
|
|
Burn(sub_commands::burn::BurnSubCommand),
|
|
/// Restore proofs from seed
|
|
Restore(sub_commands::restore::RestoreSubCommand),
|
|
/// Update Mint Url
|
|
UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand),
|
|
/// Get proofs from mint.
|
|
ListMintProofs,
|
|
/// Decode a payment request
|
|
DecodeRequest(sub_commands::decode_request::DecodePaymentRequestSubCommand),
|
|
/// Pay a payment request
|
|
PayRequest(sub_commands::pay_request::PayRequestSubCommand),
|
|
/// Create Payment request
|
|
CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
|
|
/// Mint blind auth proofs
|
|
MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
|
|
/// Cat login with username/password
|
|
CatLogin(sub_commands::cat_login::CatLoginSubCommand),
|
|
/// Cat login with device code flow
|
|
CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
|
|
}
|
|
|
|
#[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}"));
|
|
|
|
// Parse input
|
|
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
|
|
|
let work_dir = match &args.work_dir {
|
|
Some(work_dir) => work_dir.clone(),
|
|
None => {
|
|
let home_dir = home::home_dir().unwrap();
|
|
home_dir.join(DEFAULT_WORK_DIR)
|
|
}
|
|
};
|
|
|
|
fs::create_dir_all(&work_dir)?;
|
|
|
|
let localstore: Arc<dyn WalletDatabase<Err = 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 mut wallets: Vec<Wallet> = Vec::new();
|
|
|
|
let mints = localstore.get_mints().await?;
|
|
|
|
for (mint_url, mint_info) in mints {
|
|
let units = if let Some(mint_info) = mint_info {
|
|
mint_info.supported_units().into_iter().cloned().collect()
|
|
} else {
|
|
vec![CurrencyUnit::Sat]
|
|
};
|
|
|
|
let proxy_client = if let Some(proxy_url) = args.proxy.as_ref() {
|
|
Some(HttpClient::with_proxy(
|
|
mint_url.clone(),
|
|
proxy_url.clone(),
|
|
None,
|
|
true,
|
|
)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let seed = mnemonic.to_seed_normalized("");
|
|
|
|
for unit in units {
|
|
let mint_url_clone = mint_url.clone();
|
|
let mut builder = WalletBuilder::new()
|
|
.mint_url(mint_url_clone.clone())
|
|
.unit(unit)
|
|
.localstore(localstore.clone())
|
|
.seed(seed);
|
|
|
|
if let Some(http_client) = &proxy_client {
|
|
builder = builder.client(http_client.clone());
|
|
}
|
|
|
|
let wallet = builder.build()?;
|
|
|
|
let wallet_clone = wallet.clone();
|
|
|
|
tokio::spawn(async move {
|
|
// We refresh keysets, this internally gets mint info
|
|
if let Err(err) = wallet_clone.refresh_keysets().await {
|
|
tracing::error!(
|
|
"Could not get mint quote for {}, {}",
|
|
wallet_clone.mint_url,
|
|
err
|
|
);
|
|
}
|
|
});
|
|
|
|
wallets.push(wallet);
|
|
}
|
|
}
|
|
|
|
let multi_mint_wallet = MultiMintWallet::new(localstore, seed, wallets);
|
|
|
|
match &args.command {
|
|
Commands::DecodeToken(sub_command_args) => {
|
|
sub_commands::decode_token::decode_token(sub_command_args)
|
|
}
|
|
Commands::Balance => sub_commands::balance::balance(&multi_mint_wallet).await,
|
|
Commands::Melt(sub_command_args) => {
|
|
sub_commands::melt::pay(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::Receive(sub_command_args) => {
|
|
sub_commands::receive::receive(&multi_mint_wallet, sub_command_args, &work_dir).await
|
|
}
|
|
Commands::Send(sub_command_args) => {
|
|
sub_commands::send::send(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::CheckPending => {
|
|
sub_commands::check_pending::check_pending(&multi_mint_wallet).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(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::MintPending => {
|
|
sub_commands::pending_mints::mint_pending(&multi_mint_wallet).await
|
|
}
|
|
Commands::Burn(sub_command_args) => {
|
|
sub_commands::burn::burn(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::Restore(sub_command_args) => {
|
|
sub_commands::restore::restore(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::UpdateMintUrl(sub_command_args) => {
|
|
sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args)
|
|
.await
|
|
}
|
|
Commands::ListMintProofs => {
|
|
sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).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(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::CreateRequest(sub_command_args) => {
|
|
sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
|
|
}
|
|
Commands::MintBlindAuth(sub_command_args) => {
|
|
sub_commands::mint_blind_auth::mint_blind_auth(
|
|
&multi_mint_wallet,
|
|
sub_command_args,
|
|
&work_dir,
|
|
)
|
|
.await
|
|
}
|
|
Commands::CatLogin(sub_command_args) => {
|
|
sub_commands::cat_login::cat_login(&multi_mint_wallet, sub_command_args, &work_dir)
|
|
.await
|
|
}
|
|
Commands::CatDeviceLogin(sub_command_args) => {
|
|
sub_commands::cat_device_login::cat_device_login(
|
|
&multi_mint_wallet,
|
|
sub_command_args,
|
|
&work_dir,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
}
|