Skip to content

Commit 250e839

Browse files
benthecarmanclaude
andcommitted
Add lightning address support for Cashu via npub.cash
Implement get_lightning_address and register_lightning_address for the Cashu wallet using CDK's built-in npub.cash integration. When configured with a npubcash_url, the wallet enables npub.cash during init, starts background polling for incoming quotes, and returns deterministic lightning addresses derived from the wallet's Nostr keys. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cd0b7b2 commit 250e839

File tree

4 files changed

+101
-5
lines changed

4 files changed

+101
-5
lines changed

orange-sdk/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ default = ["spark"]
1717
uniffi = ["dep:uniffi", "spark", "cashu", "rand", "pin-project-lite"]
1818
spark = ["breez-sdk-spark", "uuid", "serde_json"]
1919
cashu = ["cdk", "serde_json"]
20+
npubcash = ["cashu", "cdk/npubcash", "nostr-sdk"]
2021
_test-utils = ["corepc-node", 'electrsd', "cashu", "uuid/v7", "rand"]
2122
_cashu-tests = ["_test-utils", "cdk-ldk-node", "cdk/mint", "cdk-sqlite", "cdk-axum", "axum"]
2223

@@ -32,6 +33,7 @@ breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "ef76a
3233
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"] }
3334
uuid = { version = "1.0", default-features = false, optional = true }
3435
cdk = { version = "0.15.1", default-features = false, features = ["wallet"], optional = true }
36+
nostr-sdk = { version = "0.44.1", default-features = false, optional = true }
3537
serde_json = { version = "1.0", optional = true }
3638
async-trait = "0.1"
3739
log = "0.4.28"

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/trusted_wallet/cashu/mod.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ use cdk::{Amount as CdkAmount, StreamExt};
3131

3232
use graduated_rebalancer::ReceivedLightningPayment;
3333

34+
#[cfg(feature = "npubcash")]
35+
use nostr_sdk::ToBech32;
3436
use tokio::sync::{mpsc, watch};
3537

3638
use std::collections::HashMap;
@@ -52,6 +54,8 @@ pub struct CashuConfig {
5254
pub mint_url: String,
5355
/// The currency unit to use (typically Sat)
5456
pub unit: CurrencyUnit,
57+
/// Optional npub.cash URL for lightning address support (e.g., "https://npub.cash")
58+
pub npubcash_url: Option<String>,
5559
}
5660

5761
/// A wallet implementation using the Cashu (CDK) SDK.
@@ -68,6 +72,8 @@ pub struct Cashu {
6872
event_queue: Arc<EventQueue>,
6973
tx_metadata: TxMetadataStore,
7074
runtime: Arc<Runtime>,
75+
#[cfg_attr(not(feature = "npubcash"), allow(dead_code))]
76+
npubcash_url: Option<String>,
7177
}
7278

7379
impl TrustedWalletInterface for Cashu {
@@ -497,13 +503,34 @@ impl TrustedWalletInterface for Cashu {
497503
fn get_lightning_address(
498504
&self,
499505
) -> Pin<Box<dyn Future<Output = Result<Option<String>, TrustedError>> + Send + '_>> {
500-
Box::pin(async { Ok(None) })
506+
Box::pin(async {
507+
#[cfg(feature = "npubcash")]
508+
if let Some(ref url) = self.npubcash_url {
509+
let keys = self.cashu_wallet.get_npubcash_keys().map_err(|e| {
510+
TrustedError::WalletOperationFailed(format!(
511+
"Failed to get npub.cash keys: {e}"
512+
))
513+
})?;
514+
let npub = keys.public_key().to_bech32().map_err(|e| {
515+
TrustedError::WalletOperationFailed(format!("Failed to encode npub: {e}"))
516+
})?;
517+
let domain = url.trim_start_matches("https://").trim_start_matches("http://");
518+
return Ok(Some(format!("{npub}@{domain}")));
519+
}
520+
Ok(None)
521+
})
501522
}
502523

503524
fn register_lightning_address(
504525
&self, _name: String,
505526
) -> Pin<Box<dyn Future<Output = Result<(), TrustedError>> + Send + '_>> {
506527
Box::pin(async {
528+
#[cfg(feature = "npubcash")]
529+
if self.npubcash_url.is_some() {
530+
// npub.cash addresses are deterministic from the Nostr keys,
531+
// and set_mint_url is called during init. Nothing to do here.
532+
return Ok(());
533+
}
507534
Err(TrustedError::UnsupportedOperation(
508535
"register_lightning_address is not supported in Cashu Wallet".to_string(),
509536
))
@@ -652,6 +679,61 @@ impl Cashu {
652679
});
653680
}
654681

682+
// Initialize npub.cash if configured
683+
#[cfg(feature = "npubcash")]
684+
let npubcash_url = cashu_config.npubcash_url.clone();
685+
#[cfg(not(feature = "npubcash"))]
686+
let npubcash_url: Option<String> = None;
687+
688+
#[cfg(feature = "npubcash")]
689+
if let Some(ref url) = npubcash_url {
690+
if let Err(e) = cashu_wallet.enable_npubcash(url.clone()).await {
691+
log_error!(logger, "Failed to enable npub.cash: {e}");
692+
} else {
693+
log_info!(logger, "npub.cash enabled with URL: {url}");
694+
695+
// Start background polling for npub.cash quotes
696+
let wallet_for_npubcash = Arc::clone(&cashu_wallet);
697+
let sender_for_npubcash = mint_quote_sender.clone();
698+
let logger_for_npubcash = Arc::clone(&logger);
699+
let mut shutdown_for_npubcash = shutdown_sender.subscribe();
700+
runtime.spawn_cancellable_background_task(async move {
701+
let poll_interval = Duration::from_secs(30);
702+
loop {
703+
tokio::select! {
704+
_ = shutdown_for_npubcash.changed() => {
705+
log_info!(logger_for_npubcash, "npub.cash polling shutdown");
706+
return;
707+
}
708+
_ = tokio::time::sleep(poll_interval) => {
709+
match wallet_for_npubcash.sync_npubcash_quotes().await {
710+
Ok(quotes) => {
711+
for quote in quotes {
712+
if matches!(quote.state, cdk::nuts::MintQuoteState::Paid) {
713+
let id = quote.id.clone();
714+
if let Err(e) = sender_for_npubcash.send(quote).await {
715+
log_error!(
716+
logger_for_npubcash,
717+
"Failed to send npub.cash quote {id} for monitoring: {e}"
718+
);
719+
}
720+
}
721+
}
722+
},
723+
Err(e) => {
724+
log_error!(
725+
logger_for_npubcash,
726+
"Failed to sync npub.cash quotes: {e}"
727+
);
728+
},
729+
}
730+
}
731+
}
732+
}
733+
});
734+
}
735+
}
736+
655737
Ok(Cashu {
656738
cashu_wallet,
657739
unit: cashu_config.unit,
@@ -664,6 +746,7 @@ impl Cashu {
664746
event_queue,
665747
tx_metadata,
666748
runtime,
749+
npubcash_url,
667750
})
668751
}
669752

orange-sdk/tests/test_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ async fn build_test_nodes() -> TestParams {
498498
extra_config: ExtraConfig::Cashu(orange_sdk::CashuConfig {
499499
mint_url: format!("http://127.0.0.1:{}", mint_addr.port()),
500500
unit: orange_sdk::CurrencyUnit::Sat,
501+
npubcash_url: None,
501502
}),
502503
};
503504
let wallet = Arc::new(Wallet::new(wallet_config).await.unwrap());

0 commit comments

Comments
 (0)