Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name = "orange-cli"
path = "src/main.rs"

[dependencies]
orange-sdk = { path = "../../orange-sdk" }
orange-sdk = { path = "../../orange-sdk", features = ["cashu"] }
tokio = { version = "1.0", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }
anyhow = "1.0"
Expand Down
110 changes: 102 additions & 8 deletions examples/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use rustyline::error::ReadlineError;

use orange_sdk::bitcoin_payment_instructions::amount::Amount;
use orange_sdk::{
ChainSource, Event, ExtraConfig, LoggerType, Mnemonic, PaymentInfo, Seed, SparkWalletConfig,
StorageConfig, Tunables, Wallet, WalletConfig, bitcoin::Network,
CashuConfig, ChainSource, CurrencyUnit, Event, ExtraConfig, LoggerType, Mnemonic, PaymentInfo,
Seed, SparkWalletConfig, StorageConfig, Tunables, Wallet, WalletConfig, bitcoin::Network,
};
use rand::RngCore;
use std::fs;
Expand All @@ -23,6 +23,15 @@ const NETWORK: Network = Network::Bitcoin; // Supports Bitcoin and Regtest
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Use Cashu wallet instead of Spark
#[arg(long)]
cashu: bool,
/// Cashu mint URL (requires --cashu)
#[arg(long, requires = "cashu")]
mint_url: String,
/// npub.cash URL for lightning address support (requires --cashu)
#[arg(long, requires = "cashu")]
npubcash_url: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
Expand Down Expand Up @@ -56,6 +65,13 @@ enum Commands {
/// Amount in sats (optional)
amount: Option<u64>,
},
/// Get the current lightning address
GetLightningAddress,
/// Register a lightning address
RegisterLightningAddress {
/// The lightning address name to register
name: String,
},
/// Clear the screen
Clear,
/// Exit the application
Expand All @@ -67,12 +83,22 @@ struct WalletState {
shutdown: Arc<AtomicBool>,
}

fn get_config(network: Network) -> Result<WalletConfig> {
fn get_config(network: Network, cli: &Cli) -> Result<WalletConfig> {
let storage_path = format!("./wallet_data/{network}");

// Generate or load seed
let seed = generate_or_load_seed(&storage_path)?;

let extra_config = if cli.cashu {
ExtraConfig::Cashu(CashuConfig {
mint_url: cli.mint_url.clone(),
unit: CurrencyUnit::Sat,
npubcash_url: cli.npubcash_url.clone(),
})
} else {
ExtraConfig::Spark(SparkWalletConfig::default())
};

match network {
Network::Regtest => {
let lsp_address = "185.150.162.100:3551"
Expand All @@ -96,7 +122,7 @@ fn get_config(network: Network) -> Result<WalletConfig> {
network,
seed,
tunables: Tunables::default(),
extra_config: ExtraConfig::Spark(SparkWalletConfig::default()),
extra_config,
})
},
Network::Bitcoin => {
Expand Down Expand Up @@ -125,17 +151,17 @@ fn get_config(network: Network) -> Result<WalletConfig> {
network,
seed,
tunables: Tunables::default(),
extra_config: ExtraConfig::Spark(SparkWalletConfig::default()),
extra_config,
})
},
_ => Err(anyhow::anyhow!("Unsupported network: {network:?}")),
}
}

impl WalletState {
async fn new() -> Result<Self> {
async fn new(cli: &Cli) -> Result<Self> {
let shutdown = Arc::new(AtomicBool::new(false));
let config = get_config(NETWORK)
let config = get_config(NETWORK, cli)
.with_context(|| format!("Failed to get wallet config for network: {NETWORK:?}"))?;

println!("{} Initializing wallet...", "⚡".bright_yellow());
Expand Down Expand Up @@ -234,7 +260,7 @@ async fn main() -> Result<()> {
println!();

// Initialize wallet once at startup
let mut state = WalletState::new().await?;
let mut state = WalletState::new(&cli).await?;

// Set up signal handling for graceful shutdown
let shutdown_state = state.shutdown.clone();
Expand Down Expand Up @@ -367,6 +393,14 @@ fn parse_command(input: &str) -> Result<Commands> {

Ok(Commands::EstimateFee { destination, amount })
},
"get-lightning-address" | "get-ln-addr" | "ln-addr" => Ok(Commands::GetLightningAddress),
"register-lightning-address" | "register-ln-addr" => {
if parts.len() < 2 {
return Err(anyhow::anyhow!("Usage: register-lightning-address <name>"));
}
let name = parts[1].to_string();
Ok(Commands::RegisterLightningAddress { name })
},
"clear" | "cls" => Ok(Commands::Clear),
"exit" | "quit" | "q" => Ok(Commands::Exit),
"help" => {
Expand Down Expand Up @@ -590,6 +624,60 @@ async fn execute_command(command: Commands, state: &mut WalletState) -> Result<(
},
}
},
Commands::GetLightningAddress => {
let wallet = state.wallet();

println!("{} Fetching lightning address...", "⚡".bright_yellow());

match wallet.get_lightning_address().await {
Ok(Some(address)) => {
println!(
"{} Lightning address: {}",
"⚡".bright_green(),
address.bright_cyan()
);
},
Ok(None) => {
println!("{} No lightning address registered yet.", "⚡".bright_yellow());
println!(
"{} Use 'register-lightning-address <name>' to register one",
"Hint:".bright_yellow().bold()
);
},
Err(e) => {
return Err(anyhow::anyhow!("Failed to get lightning address: {:?}", e));
},
}
},
Commands::RegisterLightningAddress { name } => {
let wallet = state.wallet();

println!(
"{} Registering lightning address: {}...",
"⚡".bright_yellow(),
name.bright_cyan()
);

match wallet.register_lightning_address(name.clone()).await {
Ok(()) => {
println!("{} Lightning address registered successfully!", "✅".bright_green());
// Fetch and display the full address
match wallet.get_lightning_address().await {
Ok(Some(address)) => {
println!(
"{} Your lightning address: {}",
"⚡".bright_green(),
address.bright_cyan()
);
},
_ => {},
}
},
Err(e) => {
return Err(anyhow::anyhow!("Failed to register lightning address: {:?}", e));
},
}
},
Commands::Clear => {
print!("\x1B[2J\x1B[1;1H");
std::io::stdout().flush().unwrap();
Expand Down Expand Up @@ -628,6 +716,12 @@ fn print_help() {
println!(" {} <destination> [amount]", "estimate-fee".bright_green().bold());
println!(" Estimate the fee for a payment");
println!();
println!(" {}", "get-lightning-address".bright_green().bold());
println!(" Get the current lightning address");
println!();
println!(" {} <name>", "register-lightning-address".bright_green().bold());
println!(" Register a lightning address");
println!();
println!(" {}", "clear".bright_green().bold());
println!(" Clear the terminal screen");
println!();
Expand Down
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ test-cashu *args:
cli:
cd examples/cli && cargo run

cli-cashu *args:
cd examples/cli && cargo run -- --cashu --npubcash-url https://npubx.cash --mint-url {{ args }}

cli-logs:
tail -n 50 -f examples/cli/wallet_data/bitcoin/wallet.log

Expand Down
10 changes: 5 additions & 5 deletions orange-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ name = "orange_sdk"
default = ["spark"]
uniffi = ["dep:uniffi", "spark", "cashu", "rand", "pin-project-lite"]
spark = ["breez-sdk-spark", "uuid", "serde_json"]
cashu = ["cdk", "serde_json"]
cashu = ["cdk", "cdk/npubcash", "serde_json"]
_test-utils = ["corepc-node", 'electrsd', "cashu", "uuid/v7", "rand"]
_cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-axum", "axum"]

Expand All @@ -31,16 +31,16 @@ rand = { version = "0.8.5", optional = true }
breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "ef76a0bc517bea38fafaff8f657e82b5b52569b9", default-features = false, features = ["rustls-tls"], optional = true }
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"] }
uuid = { version = "1.0", default-features = false, optional = true }
cdk = { version = "0.14.2", default-features = false, features = ["wallet"], optional = true }
cdk = { version = "0.15.1", default-features = false, features = ["wallet"], optional = true }
serde_json = { version = "1.0", optional = true }
async-trait = "0.1"
log = "0.4.28"

corepc-node = { version = "0.10.1", features = ["29_0", "download"], optional = true }
electrsd = { version = "0.36.1", default-features = false, features = ["esplora_a33e97e1", "corepc-node_29_0"], optional = true }
cdk-ldk-node = { version = "0.14.2", optional = true }
cdk-sqlite = { version = "0.14.2", optional = true }
cdk-axum = { version = "0.14.2", optional = true }
cdk-ldk-node = { version = "0.15.1", optional = true }
cdk-sqlite = { version = "0.15.1", optional = true }
cdk-axum = { version = "0.15.1", optional = true }
axum = { version = "0.8.1", optional = true }

uniffi = { version = "0.29", default-features = false, features = ["cli"], optional = true }
Expand Down
18 changes: 14 additions & 4 deletions orange-sdk/src/ffi/cashu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,34 @@ pub struct CashuConfig {
pub mint_url: String,
/// The currency unit to use (typically Sat)
pub unit: CurrencyUnit,
/// Optional npub.cash URL for lightning address support
pub npubcash_url: Option<String>,
}

#[uniffi::export]
impl CashuConfig {
#[uniffi::constructor]
pub fn new(mint_url: String, unit: CurrencyUnit) -> Self {
CashuConfig { mint_url, unit }
pub fn new(mint_url: String, unit: CurrencyUnit, npubcash_url: Option<String>) -> Self {
CashuConfig { mint_url, unit, npubcash_url }
}
}

impl From<CashuConfig> for OrangeCashuConfig {
fn from(config: CashuConfig) -> Self {
OrangeCashuConfig { mint_url: config.mint_url, unit: config.unit.into() }
OrangeCashuConfig {
mint_url: config.mint_url,
unit: config.unit.into(),
npubcash_url: config.npubcash_url,
}
}
}

impl From<OrangeCashuConfig> for CashuConfig {
fn from(config: OrangeCashuConfig) -> Self {
CashuConfig { mint_url: config.mint_url, unit: config.unit.into() }
CashuConfig {
mint_url: config.mint_url,
unit: config.unit.into(),
npubcash_url: config.npubcash_url,
}
}
}
12 changes: 12 additions & 0 deletions orange-sdk/src/ffi/orange/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,16 @@ impl Wallet {
pub fn event_handled(&self) -> bool {
self.inner.event_handled().is_ok()
}

/// Gets the lightning address for this wallet, if one is set.
pub async fn get_lightning_address(&self) -> Result<Option<String>, WalletError> {
let result = self.inner.get_lightning_address().await?;
Ok(result)
}

/// Attempts to register the lightning address for this wallet.
pub async fn register_lightning_address(&self, name: String) -> Result<(), WalletError> {
self.inner.register_lightning_address(name).await?;
Ok(())
}
}
46 changes: 33 additions & 13 deletions orange-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,14 +529,35 @@ impl Wallet {

let tx_metadata = TxMetadataStore::new(Arc::clone(&store)).await;

// Cashu must init before LDK Node because CashuKvDatabase does
// synchronous SQLite reads that deadlock with LDK Node's background
// store writes. Other backends can init concurrently.
#[cfg(feature = "cashu")]
let cashu_wallet = if let ExtraConfig::Cashu(cashu) = &config.extra_config {
Some(
Cashu::init(
&config,
cashu.clone(),
Arc::clone(&store),
Arc::clone(&event_queue),
tx_metadata.clone(),
Arc::clone(&logger),
Arc::clone(&runtime),
)
.await?,
)
} else {
None
};

let (trusted, ln_wallet) = tokio::join!(
async {
let trusted: Arc<Box<DynTrustedWalletInterface>> = match &config.extra_config {
#[cfg(feature = "spark")]
ExtraConfig::Spark(sp) => Arc::new(Box::new(
Spark::init(
&config,
*sp,
sp.clone(),
Arc::clone(&store),
Arc::clone(&event_queue),
tx_metadata.clone(),
Expand All @@ -546,18 +567,7 @@ impl Wallet {
.await?,
)),
#[cfg(feature = "cashu")]
ExtraConfig::Cashu(cashu) => Arc::new(Box::new(
Cashu::init(
&config,
cashu.clone(),
Arc::clone(&store),
Arc::clone(&event_queue),
tx_metadata.clone(),
Arc::clone(&logger),
Arc::clone(&runtime),
)
.await?,
)),
ExtraConfig::Cashu(_) => Arc::new(Box::new(cashu_wallet.expect("initialized above"))),
#[cfg(feature = "_test-utils")]
ExtraConfig::Dummy(cfg) => Arc::new(Box::new(
DummyTrustedWallet::new(
Expand Down Expand Up @@ -1371,6 +1381,16 @@ impl Wallet {
res
}

/// Gets the lightning address for this wallet, if one is set.
pub async fn get_lightning_address(&self) -> Result<Option<String>, WalletError> {
Ok(self.inner.trusted.get_lightning_address().await?)
}

/// Attempts to register the lightning address for this wallet.
pub async fn register_lightning_address(&self, name: String) -> Result<(), WalletError> {
Ok(self.inner.trusted.register_lightning_address(name).await?)
}

/// Stops the wallet, which will stop the underlying LDK node and any background tasks.
/// This will ensure that any critical tasks have completed before stopping.
pub async fn stop(&self) {
Expand Down
Loading
Loading