mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-22 23:25:22 +01:00
feat: cdk-cli
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -31,7 +31,9 @@ jobs:
|
|||||||
-p cdk --no-default-features --features wallet,
|
-p cdk --no-default-features --features wallet,
|
||||||
-p cdk --no-default-features --features mint,
|
-p cdk --no-default-features --features mint,
|
||||||
-p cdk --no-default-features --features wallet --features nostr,
|
-p cdk --no-default-features --features wallet --features nostr,
|
||||||
-p cdk-redb
|
-p cdk-redb,
|
||||||
|
-p cdk-sqlite,
|
||||||
|
--bin cdk-cli,
|
||||||
--examples
|
--examples
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ keywords = ["bitcoin", "e-cash", "cashu"]
|
|||||||
async-trait = "0.1.74"
|
async-trait = "0.1.74"
|
||||||
cdk = { path = "./crates/cdk", default-features = false }
|
cdk = { path = "./crates/cdk", default-features = false }
|
||||||
cdk-rexie = { path = "./crates/cdk-rexie", default-features = false }
|
cdk-rexie = { path = "./crates/cdk-rexie", default-features = false }
|
||||||
|
cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = false }
|
||||||
cdk-redb = { path = "./crates/cdk-redb", default-features = false }
|
cdk-redb = { path = "./crates/cdk-redb", default-features = false }
|
||||||
tokio = { version = "1.32", default-features = false }
|
tokio = { version = "1.32", default-features = false }
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|||||||
24
crates/cdk-cli/Cargo.toml
Normal file
24
crates/cdk-cli/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "cdk-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["CDK Developers"]
|
||||||
|
homepage.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true # MSRV
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.75"
|
||||||
|
cdk = { workspace = true, default-features = false, features = ["wallet", "nostr"] }
|
||||||
|
cdk-redb = { workspace = true, default-features = false, features = ["wallet", "nostr"] }
|
||||||
|
cdk-sqlite = { workspace = true, default-features = false, features = ["wallet", "nostr"] }
|
||||||
|
clap = { version = "4.4.8", features = ["derive", "env"] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
rand = "0.8.5"
|
||||||
14
crates/cdk-cli/README.md
Normal file
14
crates/cdk-cli/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
> **Warning**
|
||||||
|
> This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing.
|
||||||
|
|
||||||
|
cdk-cli is a CLI wallet implementation using of CDK(../cdk)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Code is under the [MIT](../../LICENSE)
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
All contributions welcome.
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any additional terms or conditions.
|
||||||
117
crates/cdk-cli/src/main.rs
Normal file
117
crates/cdk-cli/src/main.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use cdk::cdk_database::WalletDatabase;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
use cdk::{cdk_database, Mnemonic};
|
||||||
|
use cdk_redb::RedbWalletDatabase;
|
||||||
|
use cdk_sqlite::WalletSQLiteDatabase;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
mod sub_commands;
|
||||||
|
|
||||||
|
/// Simple CLI application to interact with cashu
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "cashu-tool")]
|
||||||
|
#[command(author = "thesimplekid <tsk@thesimplekid.com>")]
|
||||||
|
#[command(version = "0.1")]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
/// Database engine to use (sqlite/redb)
|
||||||
|
#[arg(short, long, default_value = "sqlite")]
|
||||||
|
engine: String,
|
||||||
|
/// Path to Seed
|
||||||
|
#[arg(short, long, default_value = "./seed")]
|
||||||
|
seed_path: String,
|
||||||
|
/// File Path to save proofs
|
||||||
|
#[arg(short, long)]
|
||||||
|
db_path: Option<String>,
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_REDB_DB_PATH: &str = "./cashu_tool.redb";
|
||||||
|
const DEFAULT_SQLITE_DB_PATH: &str = "./cashu_tool.redb";
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Decode a token
|
||||||
|
DecodeToken(sub_commands::decode_token::DecodeTokenSubCommand),
|
||||||
|
/// Pay bolt11 invoice
|
||||||
|
Melt(sub_commands::melt::MeltSubCommand),
|
||||||
|
/// Receive token
|
||||||
|
Receive(sub_commands::receive::ReceiveSubCommand),
|
||||||
|
/// Create token from wallet balance
|
||||||
|
CreateToken(sub_commands::create_token::CreateTokenSubCommand),
|
||||||
|
/// Check if wallet balance is spendable
|
||||||
|
CheckSpendable,
|
||||||
|
/// View mint info
|
||||||
|
MintInfo(sub_commands::mint_info::MintInfoSubcommand),
|
||||||
|
/// Mint proofs via bolt11
|
||||||
|
Mint(sub_commands::mint::MintSubCommand),
|
||||||
|
/// Restore proofs from seed
|
||||||
|
Restore(sub_commands::restore::RestoreSubCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::WARN)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// Parse input
|
||||||
|
let args: Cli = Cli::parse();
|
||||||
|
|
||||||
|
let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
|
||||||
|
match args.engine.as_str() {
|
||||||
|
"sqlite" => Arc::new(RedbWalletDatabase::new(DEFAULT_REDB_DB_PATH)?),
|
||||||
|
"redb" => Arc::new(WalletSQLiteDatabase::new(DEFAULT_SQLITE_DB_PATH).await?),
|
||||||
|
_ => bail!("Unknown DB engine"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mnemonic = match fs::metadata(args.seed_path.clone()) {
|
||||||
|
Ok(_) => {
|
||||||
|
let contents = fs::read_to_string(args.seed_path.clone())?;
|
||||||
|
Mnemonic::from_str(&contents)?
|
||||||
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let random_bytes: [u8; 32] = rng.gen();
|
||||||
|
|
||||||
|
let mnemnic = Mnemonic::from_entropy(&random_bytes)?;
|
||||||
|
tracing::info!("Using randomly generated seed you will not be able to restore");
|
||||||
|
|
||||||
|
mnemnic
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let wallet = Wallet::new(localstore, &mnemonic.to_seed_normalized(""), vec![]);
|
||||||
|
|
||||||
|
match &args.command {
|
||||||
|
Commands::DecodeToken(sub_command_args) => {
|
||||||
|
sub_commands::decode_token::decode_token(sub_command_args)
|
||||||
|
}
|
||||||
|
Commands::Melt(sub_command_args) => {
|
||||||
|
sub_commands::melt::melt(wallet, sub_command_args).await
|
||||||
|
}
|
||||||
|
Commands::Receive(sub_command_args) => {
|
||||||
|
sub_commands::receive::receive(wallet, sub_command_args).await
|
||||||
|
}
|
||||||
|
Commands::CreateToken(sub_command_args) => {
|
||||||
|
sub_commands::create_token::create_token(wallet, sub_command_args).await
|
||||||
|
}
|
||||||
|
Commands::CheckSpendable => sub_commands::check_spent::check_spent(wallet).await,
|
||||||
|
Commands::MintInfo(sub_command_args) => {
|
||||||
|
sub_commands::mint_info::mint_info(sub_command_args).await
|
||||||
|
}
|
||||||
|
Commands::Mint(sub_command_args) => {
|
||||||
|
sub_commands::mint::mint(wallet, sub_command_args).await
|
||||||
|
}
|
||||||
|
Commands::Restore(sub_command_args) => {
|
||||||
|
sub_commands::restore::restore(wallet, sub_command_args).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
crates/cdk-cli/src/sub_commands/check_spent.rs
Normal file
41
crates/cdk-cli/src/sub_commands/check_spent.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::{io, println};
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use cdk::url::UncheckedUrl;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
|
||||||
|
pub async fn check_spent(wallet: Wallet) -> Result<()> {
|
||||||
|
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
|
||||||
|
wallet.mint_balances().await?.into_iter().collect();
|
||||||
|
|
||||||
|
for (i, (mint, amount)) in mints_amounts.iter().enumerate() {
|
||||||
|
println!("{}: {}, {:?} sats", i, mint, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Enter mint number to create token");
|
||||||
|
|
||||||
|
let mut user_input = String::new();
|
||||||
|
let stdin = io::stdin();
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
stdin.read_line(&mut user_input)?;
|
||||||
|
|
||||||
|
let mint_number: usize = user_input.trim().parse()?;
|
||||||
|
|
||||||
|
if mint_number.gt(&(mints_amounts.len() - 1)) {
|
||||||
|
bail!("Invalid mint number");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mint_url = mints_amounts[mint_number].0.clone();
|
||||||
|
|
||||||
|
let proofs = wallet.get_proofs(mint_url.clone()).await?.unwrap();
|
||||||
|
|
||||||
|
let send_proofs = wallet.check_proofs_spent(mint_url, proofs.to_vec()).await?;
|
||||||
|
|
||||||
|
for proof in send_proofs {
|
||||||
|
println!("{:#?}", proof);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
164
crates/cdk-cli/src/sub_commands/create_token.rs
Normal file
164
crates/cdk-cli/src/sub_commands/create_token.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{io, println};
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use cdk::amount::SplitTarget;
|
||||||
|
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
|
||||||
|
use cdk::url::UncheckedUrl;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
use cdk::Amount;
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct CreateTokenSubCommand {
|
||||||
|
/// Token Memo
|
||||||
|
#[arg(short, long)]
|
||||||
|
memo: Option<String>,
|
||||||
|
/// Preimage
|
||||||
|
#[arg(long)]
|
||||||
|
preimage: Option<String>,
|
||||||
|
/// Required number of signatures
|
||||||
|
#[arg(long)]
|
||||||
|
required_sigs: Option<u64>,
|
||||||
|
/// Locktime before refund keys can be used
|
||||||
|
#[arg(short, long)]
|
||||||
|
locktime: Option<u64>,
|
||||||
|
/// Publey to lock proofs to
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Append)]
|
||||||
|
pubkey: Vec<String>,
|
||||||
|
/// Publey to lock proofs to
|
||||||
|
#[arg(long, action = clap::ArgAction::Append)]
|
||||||
|
refund_keys: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_token(wallet: Wallet, sub_command_args: &CreateTokenSubCommand) -> Result<()> {
|
||||||
|
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
|
||||||
|
wallet.mint_balances().await?.into_iter().collect();
|
||||||
|
|
||||||
|
for (i, (mint, amount)) in mints_amounts.iter().enumerate() {
|
||||||
|
println!("{}: {}, {:?} sats", i, mint, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Enter mint number to create token");
|
||||||
|
|
||||||
|
let mut user_input = String::new();
|
||||||
|
let stdin = io::stdin();
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
stdin.read_line(&mut user_input)?;
|
||||||
|
|
||||||
|
let mint_number: usize = user_input.trim().parse()?;
|
||||||
|
|
||||||
|
if mint_number.gt(&(mints_amounts.len() - 1)) {
|
||||||
|
bail!("Invalid mint number");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mint_url = mints_amounts[mint_number].0.clone();
|
||||||
|
|
||||||
|
println!("Enter value of token in sats");
|
||||||
|
|
||||||
|
let mut user_input = String::new();
|
||||||
|
let stdin = io::stdin();
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
stdin.read_line(&mut user_input)?;
|
||||||
|
let token_amount = Amount::from(user_input.trim().parse::<u64>()?);
|
||||||
|
|
||||||
|
if token_amount.gt(mints_amounts[mint_number]
|
||||||
|
.1
|
||||||
|
.get(&CurrencyUnit::Sat)
|
||||||
|
.unwrap())
|
||||||
|
{
|
||||||
|
bail!("Not enough funds");
|
||||||
|
}
|
||||||
|
|
||||||
|
let conditions = match &sub_command_args.preimage {
|
||||||
|
Some(preimage) => {
|
||||||
|
let pubkeys = match sub_command_args.pubkey.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(
|
||||||
|
sub_command_args
|
||||||
|
.pubkey
|
||||||
|
.iter()
|
||||||
|
.map(|p| PublicKey::from_str(p).unwrap())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let refund_keys = match sub_command_args.refund_keys.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(
|
||||||
|
sub_command_args
|
||||||
|
.refund_keys
|
||||||
|
.iter()
|
||||||
|
.map(|p| PublicKey::from_str(p).unwrap())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conditions = Conditions::new(
|
||||||
|
sub_command_args.locktime,
|
||||||
|
pubkeys,
|
||||||
|
refund_keys,
|
||||||
|
sub_command_args.required_sigs,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Some(SpendingConditions::new_htlc(preimage.clone(), conditions)?)
|
||||||
|
}
|
||||||
|
None => match sub_command_args.pubkey.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => {
|
||||||
|
let pubkeys: Vec<PublicKey> = sub_command_args
|
||||||
|
.pubkey
|
||||||
|
.iter()
|
||||||
|
.map(|p| PublicKey::from_str(p).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let refund_keys: Vec<PublicKey> = sub_command_args
|
||||||
|
.refund_keys
|
||||||
|
.iter()
|
||||||
|
.map(|p| PublicKey::from_str(p).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let refund_keys = (!refund_keys.is_empty()).then_some(refund_keys);
|
||||||
|
|
||||||
|
let data_pubkey = pubkeys[0];
|
||||||
|
let pubkeys = pubkeys[1..].to_vec();
|
||||||
|
let pubkeys = (!pubkeys.is_empty()).then_some(pubkeys);
|
||||||
|
|
||||||
|
let conditions = Conditions::new(
|
||||||
|
sub_command_args.locktime,
|
||||||
|
pubkeys,
|
||||||
|
refund_keys,
|
||||||
|
sub_command_args.required_sigs,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
tracing::debug!("{}", data_pubkey.to_string());
|
||||||
|
|
||||||
|
Some(SpendingConditions::P2PKConditions {
|
||||||
|
data: data_pubkey,
|
||||||
|
conditions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = wallet
|
||||||
|
.send(
|
||||||
|
&mint_url,
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
sub_command_args.memo.clone(),
|
||||||
|
token_amount,
|
||||||
|
&SplitTarget::default(),
|
||||||
|
conditions,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("{}", token);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
18
crates/cdk-cli/src/sub_commands/decode_token.rs
Normal file
18
crates/cdk-cli/src/sub_commands/decode_token.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cdk::nuts::Token;
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct DecodeTokenSubCommand {
|
||||||
|
/// Cashu Token
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_token(sub_command_args: &DecodeTokenSubCommand) -> Result<()> {
|
||||||
|
let token = Token::from_str(&sub_command_args.token)?;
|
||||||
|
|
||||||
|
println!("{:}", serde_json::to_string_pretty(&token)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
79
crates/cdk-cli/src/sub_commands/melt.rs
Normal file
79
crates/cdk-cli/src/sub_commands/melt.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{io, println};
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use cdk::amount::SplitTarget;
|
||||||
|
use cdk::nuts::CurrencyUnit;
|
||||||
|
use cdk::url::UncheckedUrl;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
use cdk::Bolt11Invoice;
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MeltSubCommand {}
|
||||||
|
|
||||||
|
pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<()> {
|
||||||
|
let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
|
||||||
|
wallet.mint_balances().await?.into_iter().collect();
|
||||||
|
|
||||||
|
for (i, (mint, amount)) in mints_amounts.iter().enumerate() {
|
||||||
|
println!("{}: {}, {:?} sats", i, mint, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Enter mint number to create token");
|
||||||
|
|
||||||
|
let mut user_input = String::new();
|
||||||
|
let stdin = io::stdin();
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
stdin.read_line(&mut user_input)?;
|
||||||
|
|
||||||
|
let mint_number: usize = user_input.trim().parse()?;
|
||||||
|
|
||||||
|
if mint_number.gt(&(mints_amounts.len() - 1)) {
|
||||||
|
bail!("Invalid mint number");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mint_url = mints_amounts[mint_number].0.clone();
|
||||||
|
|
||||||
|
println!("Enter bolt11 invoice request");
|
||||||
|
|
||||||
|
let mut user_input = String::new();
|
||||||
|
let stdin = io::stdin();
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
stdin.read_line(&mut user_input)?;
|
||||||
|
let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
|
||||||
|
|
||||||
|
if bolt11
|
||||||
|
.amount_milli_satoshis()
|
||||||
|
.unwrap()
|
||||||
|
.gt(&(<cdk::Amount as Into<u64>>::into(
|
||||||
|
*mints_amounts[mint_number]
|
||||||
|
.1
|
||||||
|
.get(&CurrencyUnit::Sat)
|
||||||
|
.unwrap(),
|
||||||
|
) * 1000_u64))
|
||||||
|
{
|
||||||
|
bail!("Not enough funds");
|
||||||
|
}
|
||||||
|
let quote = wallet
|
||||||
|
.melt_quote(
|
||||||
|
mint_url.clone(),
|
||||||
|
cdk::nuts::CurrencyUnit::Sat,
|
||||||
|
bolt11.to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let melt = wallet
|
||||||
|
.melt(&mint_url, "e.id, SplitTarget::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Paid invoice: {}", melt.paid);
|
||||||
|
if let Some(preimage) = melt.preimage {
|
||||||
|
println!("Payment preimage: {}", preimage);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
59
crates/cdk-cli/src/sub_commands/mint.rs
Normal file
59
crates/cdk-cli/src/sub_commands/mint.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cdk::amount::SplitTarget;
|
||||||
|
use cdk::nuts::CurrencyUnit;
|
||||||
|
use cdk::url::UncheckedUrl;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
use cdk::Amount;
|
||||||
|
use clap::Args;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MintSubCommand {
|
||||||
|
/// Amount
|
||||||
|
#[arg(short, long)]
|
||||||
|
amount: u64,
|
||||||
|
/// Currency unit e.g. sat
|
||||||
|
#[arg(short, long)]
|
||||||
|
unit: String,
|
||||||
|
/// Mint url
|
||||||
|
#[arg(short, long)]
|
||||||
|
mint_url: UncheckedUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<()> {
|
||||||
|
let mint_url = sub_command_args.mint_url.clone();
|
||||||
|
|
||||||
|
let quote = wallet
|
||||||
|
.mint_quote(
|
||||||
|
mint_url.clone(),
|
||||||
|
Amount::from(sub_command_args.amount),
|
||||||
|
CurrencyUnit::from(&sub_command_args.unit),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Quote: {:#?}", quote);
|
||||||
|
|
||||||
|
println!("Please pay: {}", quote.request);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let status = wallet
|
||||||
|
.mint_quote_status(mint_url.clone(), "e.id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if status.paid {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let receive_amount = wallet
|
||||||
|
.mint(mint_url.clone(), "e.id, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Received {receive_amount} from mint {mint_url}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
23
crates/cdk-cli/src/sub_commands/mint_info.rs
Normal file
23
crates/cdk-cli/src/sub_commands/mint_info.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use cdk::url::UncheckedUrl;
|
||||||
|
use cdk::HttpClient;
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct MintInfoSubcommand {
|
||||||
|
/// Cashu Token
|
||||||
|
#[arg(short, long)]
|
||||||
|
mint_url: UncheckedUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mint_info(sub_command_args: &MintInfoSubcommand) -> Result<()> {
|
||||||
|
let client = HttpClient::default();
|
||||||
|
|
||||||
|
let info = client
|
||||||
|
.get_mint_info(sub_command_args.mint_url.clone().try_into()?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("{:#?}", info);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
8
crates/cdk-cli/src/sub_commands/mod.rs
Normal file
8
crates/cdk-cli/src/sub_commands/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod check_spent;
|
||||||
|
pub mod create_token;
|
||||||
|
pub mod decode_token;
|
||||||
|
pub mod melt;
|
||||||
|
pub mod mint;
|
||||||
|
pub mod mint_info;
|
||||||
|
pub mod receive;
|
||||||
|
pub mod restore;
|
||||||
81
crates/cdk-cli/src/sub_commands/receive.rs
Normal file
81
crates/cdk-cli/src/sub_commands/receive.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use cdk::amount::SplitTarget;
|
||||||
|
use cdk::nuts::SecretKey;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ReceiveSubCommand {
|
||||||
|
/// Cashu Token
|
||||||
|
token: Option<String>,
|
||||||
|
/// Nostr key
|
||||||
|
#[arg(short, long)]
|
||||||
|
nostr_key: Option<String>,
|
||||||
|
/// Signing Key
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Append)]
|
||||||
|
signing_key: Vec<String>,
|
||||||
|
/// Nostr relay
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Append)]
|
||||||
|
relay: Vec<String>,
|
||||||
|
/// Preimage
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Append)]
|
||||||
|
preimage: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn receive(wallet: Wallet, sub_command_args: &ReceiveSubCommand) -> Result<()> {
|
||||||
|
let nostr_key = match sub_command_args.nostr_key.as_ref() {
|
||||||
|
Some(nostr_key) => {
|
||||||
|
let secret_key = SecretKey::from_str(nostr_key)?;
|
||||||
|
wallet.add_p2pk_signing_key(secret_key.clone()).await;
|
||||||
|
Some(secret_key)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !sub_command_args.signing_key.is_empty() {
|
||||||
|
let signing_keys: Vec<SecretKey> = sub_command_args
|
||||||
|
.signing_key
|
||||||
|
.iter()
|
||||||
|
.map(|s| SecretKey::from_str(s).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for signing_key in signing_keys {
|
||||||
|
wallet.add_p2pk_signing_key(signing_key).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let preimage = match sub_command_args.preimage.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(sub_command_args.preimage.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let amount = match nostr_key {
|
||||||
|
Some(nostr_key) => {
|
||||||
|
assert!(!sub_command_args.relay.is_empty());
|
||||||
|
wallet
|
||||||
|
.add_nostr_relays(sub_command_args.relay.clone())
|
||||||
|
.await?;
|
||||||
|
wallet
|
||||||
|
.nostr_receive(nostr_key, SplitTarget::default())
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
wallet
|
||||||
|
.receive(
|
||||||
|
sub_command_args
|
||||||
|
.token
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(anyhow!("Token Required"))?,
|
||||||
|
&SplitTarget::default(),
|
||||||
|
preimage,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Received: {}", amount);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
21
crates/cdk-cli/src/sub_commands/restore.rs
Normal file
21
crates/cdk-cli/src/sub_commands/restore.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use cdk::url::UncheckedUrl;
|
||||||
|
use cdk::wallet::Wallet;
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct RestoreSubCommand {
|
||||||
|
/// Mint Url
|
||||||
|
#[arg(short, long)]
|
||||||
|
mint_url: UncheckedUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restore(wallet: Wallet, sub_command_args: &RestoreSubCommand) -> Result<()> {
|
||||||
|
let mint_url = sub_command_args.mint_url.clone();
|
||||||
|
|
||||||
|
let amount = wallet.restore(mint_url).await?;
|
||||||
|
|
||||||
|
println!("Restored {}", amount);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ buildargs=(
|
|||||||
"-p cdk-redb --no-default-features --features mint"
|
"-p cdk-redb --no-default-features --features mint"
|
||||||
"-p cdk-sqlite --no-default-features --features mint"
|
"-p cdk-sqlite --no-default-features --features mint"
|
||||||
"-p cdk-sqlite --no-default-features --features wallet"
|
"-p cdk-sqlite --no-default-features --features wallet"
|
||||||
|
"--bin cdk-cli"
|
||||||
"--examples"
|
"--examples"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user