Skip to content

Commit 5d2211f

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 4c660fe commit 5d2211f

File tree

4 files changed

+105
-5
lines changed

4 files changed

+105
-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: 88 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 {
@@ -480,13 +486,38 @@ impl TrustedWalletInterface for Cashu {
480486
fn get_lightning_address(
481487
&self,
482488
) -> Pin<Box<dyn Future<Output = Result<Option<String>, TrustedError>> + Send + '_>> {
483-
Box::pin(async { Ok(None) })
489+
Box::pin(async {
490+
#[cfg(feature = "npubcash")]
491+
if let Some(ref url) = self.npubcash_url {
492+
let keys = self.cashu_wallet.get_npubcash_keys().map_err(|e| {
493+
TrustedError::WalletOperationFailed(format!(
494+
"Failed to get npub.cash keys: {e}"
495+
))
496+
})?;
497+
let npub = keys.public_key().to_bech32().map_err(|e| {
498+
TrustedError::WalletOperationFailed(format!(
499+
"Failed to encode npub: {e}"
500+
))
501+
})?;
502+
let domain = url
503+
.trim_start_matches("https://")
504+
.trim_start_matches("http://");
505+
return Ok(Some(format!("{npub}@{domain}")));
506+
}
507+
Ok(None)
508+
})
484509
}
485510

486511
fn register_lightning_address(
487512
&self, _name: String,
488513
) -> Pin<Box<dyn Future<Output = Result<(), TrustedError>> + Send + '_>> {
489514
Box::pin(async {
515+
#[cfg(feature = "npubcash")]
516+
if self.npubcash_url.is_some() {
517+
// npub.cash addresses are deterministic from the Nostr keys,
518+
// and set_mint_url is called during init. Nothing to do here.
519+
return Ok(());
520+
}
490521
Err(TrustedError::UnsupportedOperation(
491522
"register_lightning_address is not supported in Cashu Wallet".to_string(),
492523
))
@@ -637,6 +668,61 @@ impl Cashu {
637668
});
638669
}
639670

671+
// Initialize npub.cash if configured
672+
#[cfg(feature = "npubcash")]
673+
let npubcash_url = cashu_config.npubcash_url.clone();
674+
#[cfg(not(feature = "npubcash"))]
675+
let npubcash_url: Option<String> = None;
676+
677+
#[cfg(feature = "npubcash")]
678+
if let Some(ref url) = npubcash_url {
679+
if let Err(e) = cashu_wallet.enable_npubcash(url.clone()).await {
680+
log_error!(logger, "Failed to enable npub.cash: {e}");
681+
} else {
682+
log_info!(logger, "npub.cash enabled with URL: {url}");
683+
684+
// Start background polling for npub.cash quotes
685+
let wallet_for_npubcash = Arc::clone(&cashu_wallet);
686+
let sender_for_npubcash = mint_quote_sender.clone();
687+
let logger_for_npubcash = Arc::clone(&logger);
688+
let mut shutdown_for_npubcash = shutdown_sender.subscribe();
689+
runtime.spawn_cancellable_background_task(async move {
690+
let poll_interval = Duration::from_secs(30);
691+
loop {
692+
tokio::select! {
693+
_ = shutdown_for_npubcash.changed() => {
694+
log_info!(logger_for_npubcash, "npub.cash polling shutdown");
695+
return;
696+
}
697+
_ = tokio::time::sleep(poll_interval) => {
698+
match wallet_for_npubcash.sync_npubcash_quotes().await {
699+
Ok(quotes) => {
700+
for quote in quotes {
701+
if matches!(quote.state, cdk::nuts::MintQuoteState::Paid) {
702+
let id = quote.id.clone();
703+
if let Err(e) = sender_for_npubcash.send(quote).await {
704+
log_error!(
705+
logger_for_npubcash,
706+
"Failed to send npub.cash quote {id} for monitoring: {e}"
707+
);
708+
}
709+
}
710+
}
711+
},
712+
Err(e) => {
713+
log_error!(
714+
logger_for_npubcash,
715+
"Failed to sync npub.cash quotes: {e}"
716+
);
717+
},
718+
}
719+
}
720+
}
721+
}
722+
});
723+
}
724+
}
725+
640726
Ok(Cashu {
641727
cashu_wallet,
642728
unit: cashu_config.unit,
@@ -649,6 +735,7 @@ impl Cashu {
649735
event_queue,
650736
tx_metadata,
651737
runtime,
738+
npubcash_url,
652739
})
653740
}
654741

orange-sdk/tests/test_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ async fn build_test_nodes() -> TestParams {
489489
extra_config: ExtraConfig::Cashu(orange_sdk::CashuConfig {
490490
mint_url: format!("http://127.0.0.1:{}", mint_addr.port()),
491491
unit: orange_sdk::CurrencyUnit::Sat,
492+
npubcash_url: None,
492493
}),
493494
};
494495
let wallet = Arc::new(Wallet::new(wallet_config).await.unwrap());

0 commit comments

Comments
 (0)