Skip to content

Commit 8e10b63

Browse files
authored
Merge pull request #34 from lightningdevkit/ln-addr
2 parents ad7773e + 7a52c4d commit 8e10b63

13 files changed

Lines changed: 717 additions & 102 deletions

File tree

examples/cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "orange-cli"
88
path = "src/main.rs"
99

1010
[dependencies]
11-
orange-sdk = { path = "../../orange-sdk" }
11+
orange-sdk = { path = "../../orange-sdk", features = ["cashu"] }
1212
tokio = { version = "1.0", features = ["full"] }
1313
clap = { version = "4.0", features = ["derive"] }
1414
anyhow = "1.0"

examples/cli/src/main.rs

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use rustyline::error::ReadlineError;
66

77
use orange_sdk::bitcoin_payment_instructions::amount::Amount;
88
use orange_sdk::{
9-
ChainSource, Event, ExtraConfig, LoggerType, Mnemonic, PaymentInfo, Seed, SparkWalletConfig,
10-
StorageConfig, Tunables, Wallet, WalletConfig, bitcoin::Network,
9+
CashuConfig, ChainSource, CurrencyUnit, Event, ExtraConfig, LoggerType, Mnemonic, PaymentInfo,
10+
Seed, SparkWalletConfig, StorageConfig, Tunables, Wallet, WalletConfig, bitcoin::Network,
1111
};
1212
use rand::RngCore;
1313
use std::fs;
@@ -23,6 +23,15 @@ const NETWORK: Network = Network::Bitcoin; // Supports Bitcoin and Regtest
2323
#[derive(Parser)]
2424
#[command(author, version, about, long_about = None)]
2525
struct Cli {
26+
/// Use Cashu wallet instead of Spark
27+
#[arg(long)]
28+
cashu: bool,
29+
/// Cashu mint URL (requires --cashu)
30+
#[arg(long, requires = "cashu")]
31+
mint_url: String,
32+
/// npub.cash URL for lightning address support (requires --cashu)
33+
#[arg(long, requires = "cashu")]
34+
npubcash_url: Option<String>,
2635
#[command(subcommand)]
2736
command: Option<Commands>,
2837
}
@@ -56,6 +65,13 @@ enum Commands {
5665
/// Amount in sats (optional)
5766
amount: Option<u64>,
5867
},
68+
/// Get the current lightning address
69+
GetLightningAddress,
70+
/// Register a lightning address
71+
RegisterLightningAddress {
72+
/// The lightning address name to register
73+
name: String,
74+
},
5975
/// Clear the screen
6076
Clear,
6177
/// Exit the application
@@ -67,12 +83,22 @@ struct WalletState {
6783
shutdown: Arc<AtomicBool>,
6884
}
6985

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

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

92+
let extra_config = if cli.cashu {
93+
ExtraConfig::Cashu(CashuConfig {
94+
mint_url: cli.mint_url.clone(),
95+
unit: CurrencyUnit::Sat,
96+
npubcash_url: cli.npubcash_url.clone(),
97+
})
98+
} else {
99+
ExtraConfig::Spark(SparkWalletConfig::default())
100+
};
101+
76102
match network {
77103
Network::Regtest => {
78104
let lsp_address = "185.150.162.100:3551"
@@ -96,7 +122,7 @@ fn get_config(network: Network) -> Result<WalletConfig> {
96122
network,
97123
seed,
98124
tunables: Tunables::default(),
99-
extra_config: ExtraConfig::Spark(SparkWalletConfig::default()),
125+
extra_config,
100126
})
101127
},
102128
Network::Bitcoin => {
@@ -125,17 +151,17 @@ fn get_config(network: Network) -> Result<WalletConfig> {
125151
network,
126152
seed,
127153
tunables: Tunables::default(),
128-
extra_config: ExtraConfig::Spark(SparkWalletConfig::default()),
154+
extra_config,
129155
})
130156
},
131157
_ => Err(anyhow::anyhow!("Unsupported network: {network:?}")),
132158
}
133159
}
134160

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

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

236262
// Initialize wallet once at startup
237-
let mut state = WalletState::new().await?;
263+
let mut state = WalletState::new(&cli).await?;
238264

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

368394
Ok(Commands::EstimateFee { destination, amount })
369395
},
396+
"get-lightning-address" | "get-ln-addr" | "ln-addr" => Ok(Commands::GetLightningAddress),
397+
"register-lightning-address" | "register-ln-addr" => {
398+
if parts.len() < 2 {
399+
return Err(anyhow::anyhow!("Usage: register-lightning-address <name>"));
400+
}
401+
let name = parts[1].to_string();
402+
Ok(Commands::RegisterLightningAddress { name })
403+
},
370404
"clear" | "cls" => Ok(Commands::Clear),
371405
"exit" | "quit" | "q" => Ok(Commands::Exit),
372406
"help" => {
@@ -590,6 +624,60 @@ async fn execute_command(command: Commands, state: &mut WalletState) -> Result<(
590624
},
591625
}
592626
},
627+
Commands::GetLightningAddress => {
628+
let wallet = state.wallet();
629+
630+
println!("{} Fetching lightning address...", "⚡".bright_yellow());
631+
632+
match wallet.get_lightning_address().await {
633+
Ok(Some(address)) => {
634+
println!(
635+
"{} Lightning address: {}",
636+
"⚡".bright_green(),
637+
address.bright_cyan()
638+
);
639+
},
640+
Ok(None) => {
641+
println!("{} No lightning address registered yet.", "⚡".bright_yellow());
642+
println!(
643+
"{} Use 'register-lightning-address <name>' to register one",
644+
"Hint:".bright_yellow().bold()
645+
);
646+
},
647+
Err(e) => {
648+
return Err(anyhow::anyhow!("Failed to get lightning address: {:?}", e));
649+
},
650+
}
651+
},
652+
Commands::RegisterLightningAddress { name } => {
653+
let wallet = state.wallet();
654+
655+
println!(
656+
"{} Registering lightning address: {}...",
657+
"⚡".bright_yellow(),
658+
name.bright_cyan()
659+
);
660+
661+
match wallet.register_lightning_address(name.clone()).await {
662+
Ok(()) => {
663+
println!("{} Lightning address registered successfully!", "✅".bright_green());
664+
// Fetch and display the full address
665+
match wallet.get_lightning_address().await {
666+
Ok(Some(address)) => {
667+
println!(
668+
"{} Your lightning address: {}",
669+
"⚡".bright_green(),
670+
address.bright_cyan()
671+
);
672+
},
673+
_ => {},
674+
}
675+
},
676+
Err(e) => {
677+
return Err(anyhow::anyhow!("Failed to register lightning address: {:?}", e));
678+
},
679+
}
680+
},
593681
Commands::Clear => {
594682
print!("\x1B[2J\x1B[1;1H");
595683
std::io::stdout().flush().unwrap();
@@ -628,6 +716,12 @@ fn print_help() {
628716
println!(" {} <destination> [amount]", "estimate-fee".bright_green().bold());
629717
println!(" Estimate the fee for a payment");
630718
println!();
719+
println!(" {}", "get-lightning-address".bright_green().bold());
720+
println!(" Get the current lightning address");
721+
println!();
722+
println!(" {} <name>", "register-lightning-address".bright_green().bold());
723+
println!(" Register a lightning address");
724+
println!();
631725
println!(" {}", "clear".bright_green().bold());
632726
println!(" Clear the terminal screen");
633727
println!();

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ test-cashu *args:
1616
cli:
1717
cd examples/cli && cargo run
1818

19+
cli-cashu *args:
20+
cd examples/cli && cargo run -- --cashu --npubcash-url https://npubx.cash --mint-url {{ args }}
21+
1922
cli-logs:
2023
tail -n 50 -f examples/cli/wallet_data/bitcoin/wallet.log
2124

orange-sdk/Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "orange_sdk"
1616
default = ["spark"]
1717
uniffi = ["dep:uniffi", "spark", "cashu", "rand", "pin-project-lite"]
1818
spark = ["breez-sdk-spark", "uuid", "serde_json"]
19-
cashu = ["cdk", "serde_json"]
19+
cashu = ["cdk", "cdk/npubcash", "serde_json"]
2020
_test-utils = ["corepc-node", 'electrsd', "cashu", "uuid/v7", "rand"]
2121
_cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-axum", "axum"]
2222

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

3939
corepc-node = { version = "0.10.1", features = ["29_0", "download"], optional = true }
4040
electrsd = { version = "0.36.1", default-features = false, features = ["esplora_a33e97e1", "corepc-node_29_0"], optional = true }
41-
cdk-ldk-node = { version = "0.14.2", optional = true }
42-
cdk-sqlite = { version = "0.14.2", optional = true }
43-
cdk-axum = { version = "0.14.2", optional = true }
41+
cdk-ldk-node = { version = "0.15.1", optional = true }
42+
cdk-sqlite = { version = "0.15.1", optional = true }
43+
cdk-axum = { version = "0.15.1", optional = true }
4444
axum = { version = "0.8.1", optional = true }
4545

4646
uniffi = { version = "0.29", default-features = false, features = ["cli"], optional = true }

orange-sdk/src/ffi/cashu.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,34 @@ pub struct CashuConfig {
4545
pub mint_url: String,
4646
/// The currency unit to use (typically Sat)
4747
pub unit: CurrencyUnit,
48+
/// Optional npub.cash URL for lightning address support
49+
pub npubcash_url: Option<String>,
4850
}
4951

5052
#[uniffi::export]
5153
impl CashuConfig {
5254
#[uniffi::constructor]
53-
pub fn new(mint_url: String, unit: CurrencyUnit) -> Self {
54-
CashuConfig { mint_url, unit }
55+
pub fn new(mint_url: String, unit: CurrencyUnit, npubcash_url: Option<String>) -> Self {
56+
CashuConfig { mint_url, unit, npubcash_url }
5557
}
5658
}
5759

5860
impl From<CashuConfig> for OrangeCashuConfig {
5961
fn from(config: CashuConfig) -> Self {
60-
OrangeCashuConfig { mint_url: config.mint_url, unit: config.unit.into() }
62+
OrangeCashuConfig {
63+
mint_url: config.mint_url,
64+
unit: config.unit.into(),
65+
npubcash_url: config.npubcash_url,
66+
}
6167
}
6268
}
6369

6470
impl From<OrangeCashuConfig> for CashuConfig {
6571
fn from(config: OrangeCashuConfig) -> Self {
66-
CashuConfig { mint_url: config.mint_url, unit: config.unit.into() }
72+
CashuConfig {
73+
mint_url: config.mint_url,
74+
unit: config.unit.into(),
75+
npubcash_url: config.npubcash_url,
76+
}
6777
}
6878
}

orange-sdk/src/ffi/orange/wallet.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,16 @@ impl Wallet {
296296
pub fn event_handled(&self) -> bool {
297297
self.inner.event_handled().is_ok()
298298
}
299+
300+
/// Gets the lightning address for this wallet, if one is set.
301+
pub async fn get_lightning_address(&self) -> Result<Option<String>, WalletError> {
302+
let result = self.inner.get_lightning_address().await?;
303+
Ok(result)
304+
}
305+
306+
/// Attempts to register the lightning address for this wallet.
307+
pub async fn register_lightning_address(&self, name: String) -> Result<(), WalletError> {
308+
self.inner.register_lightning_address(name).await?;
309+
Ok(())
310+
}
299311
}

orange-sdk/src/lib.rs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -529,14 +529,35 @@ impl Wallet {
529529

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

532+
// Cashu must init before LDK Node because CashuKvDatabase does
533+
// synchronous SQLite reads that deadlock with LDK Node's background
534+
// store writes. Other backends can init concurrently.
535+
#[cfg(feature = "cashu")]
536+
let cashu_wallet = if let ExtraConfig::Cashu(cashu) = &config.extra_config {
537+
Some(
538+
Cashu::init(
539+
&config,
540+
cashu.clone(),
541+
Arc::clone(&store),
542+
Arc::clone(&event_queue),
543+
tx_metadata.clone(),
544+
Arc::clone(&logger),
545+
Arc::clone(&runtime),
546+
)
547+
.await?,
548+
)
549+
} else {
550+
None
551+
};
552+
532553
let (trusted, ln_wallet) = tokio::join!(
533554
async {
534555
let trusted: Arc<Box<DynTrustedWalletInterface>> = match &config.extra_config {
535556
#[cfg(feature = "spark")]
536557
ExtraConfig::Spark(sp) => Arc::new(Box::new(
537558
Spark::init(
538559
&config,
539-
*sp,
560+
sp.clone(),
540561
Arc::clone(&store),
541562
Arc::clone(&event_queue),
542563
tx_metadata.clone(),
@@ -546,18 +567,7 @@ impl Wallet {
546567
.await?,
547568
)),
548569
#[cfg(feature = "cashu")]
549-
ExtraConfig::Cashu(cashu) => Arc::new(Box::new(
550-
Cashu::init(
551-
&config,
552-
cashu.clone(),
553-
Arc::clone(&store),
554-
Arc::clone(&event_queue),
555-
tx_metadata.clone(),
556-
Arc::clone(&logger),
557-
Arc::clone(&runtime),
558-
)
559-
.await?,
560-
)),
570+
ExtraConfig::Cashu(_) => Arc::new(Box::new(cashu_wallet.expect("initialized above"))),
561571
#[cfg(feature = "_test-utils")]
562572
ExtraConfig::Dummy(cfg) => Arc::new(Box::new(
563573
DummyTrustedWallet::new(
@@ -1371,6 +1381,16 @@ impl Wallet {
13711381
res
13721382
}
13731383

1384+
/// Gets the lightning address for this wallet, if one is set.
1385+
pub async fn get_lightning_address(&self) -> Result<Option<String>, WalletError> {
1386+
Ok(self.inner.trusted.get_lightning_address().await?)
1387+
}
1388+
1389+
/// Attempts to register the lightning address for this wallet.
1390+
pub async fn register_lightning_address(&self, name: String) -> Result<(), WalletError> {
1391+
Ok(self.inner.trusted.register_lightning_address(name).await?)
1392+
}
1393+
13741394
/// Stops the wallet, which will stop the underlying LDK node and any background tasks.
13751395
/// This will ensure that any critical tasks have completed before stopping.
13761396
pub async fn stop(&self) {

0 commit comments

Comments
 (0)