Skip to content

Commit cb21b0e

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 ac19971 commit cb21b0e

File tree

4 files changed

+93
-6
lines changed

4 files changed

+93
-6
lines changed

orange-sdk/Cargo.toml

Lines changed: 2 additions & 1 deletion
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", "nostr-sdk", "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

@@ -32,6 +32,7 @@ breez-sdk-spark = { git = "https://github.com/breez/spark-sdk.git", rev = "ef76a
3232
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"] }
3333
uuid = { version = "1.0", default-features = false, optional = true }
3434
cdk = { version = "0.15.1", default-features = false, features = ["wallet"], optional = true }
35+
nostr-sdk = { version = "0.44.1", default-features = false, optional = true }
3536
serde_json = { version = "1.0", optional = true }
3637
async-trait = "0.1"
3738
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: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use cdk::{Amount as CdkAmount, StreamExt};
3131

3232
use graduated_rebalancer::ReceivedLightningPayment;
3333

34+
use nostr_sdk::ToBech32;
3435
use tokio::sync::{mpsc, watch};
3536

3637
use std::collections::HashMap;
@@ -52,6 +53,8 @@ pub struct CashuConfig {
5253
pub mint_url: String,
5354
/// The currency unit to use (typically Sat)
5455
pub unit: CurrencyUnit,
56+
/// Optional npub.cash URL for lightning address support (e.g., `https://npub.cash`)
57+
pub npubcash_url: Option<String>,
5558
}
5659

5760
/// A wallet implementation using the Cashu (CDK) SDK.
@@ -68,6 +71,7 @@ pub struct Cashu {
6871
event_queue: Arc<EventQueue>,
6972
tx_metadata: TxMetadataStore,
7073
runtime: Arc<Runtime>,
74+
npubcash_url: Option<String>,
7175
}
7276

7377
impl TrustedWalletInterface for Cashu {
@@ -497,13 +501,32 @@ impl TrustedWalletInterface for Cashu {
497501
fn get_lightning_address(
498502
&self,
499503
) -> Pin<Box<dyn Future<Output = Result<Option<String>, TrustedError>> + Send + '_>> {
500-
Box::pin(async { Ok(None) })
504+
Box::pin(async {
505+
if let Some(ref url) = self.npubcash_url {
506+
let keys = self.cashu_wallet.get_npubcash_keys().map_err(|e| {
507+
TrustedError::WalletOperationFailed(format!(
508+
"Failed to get npub.cash keys: {e}"
509+
))
510+
})?;
511+
let npub = keys.public_key().to_bech32().map_err(|e| {
512+
TrustedError::WalletOperationFailed(format!("Failed to encode npub: {e}"))
513+
})?;
514+
let domain = url.trim_start_matches("https://").trim_start_matches("http://");
515+
return Ok(Some(format!("{npub}@{domain}")));
516+
}
517+
Ok(None)
518+
})
501519
}
502520

503521
fn register_lightning_address(
504522
&self, _name: String,
505523
) -> Pin<Box<dyn Future<Output = Result<(), TrustedError>> + Send + '_>> {
506524
Box::pin(async {
525+
if self.npubcash_url.is_some() {
526+
// npub.cash addresses are deterministic from the Nostr keys,
527+
// and set_mint_url is called during init. Nothing to do here.
528+
return Ok(());
529+
}
507530
Err(TrustedError::UnsupportedOperation(
508531
"register_lightning_address is not supported in Cashu Wallet".to_string(),
509532
))
@@ -652,6 +675,57 @@ impl Cashu {
652675
});
653676
}
654677

678+
// Initialize npub.cash if configured
679+
let npubcash_url = cashu_config.npubcash_url.clone();
680+
681+
if let Some(ref url) = npubcash_url {
682+
if let Err(e) = cashu_wallet.enable_npubcash(url.clone()).await {
683+
log_error!(logger, "Failed to enable npub.cash: {e}");
684+
} else {
685+
log_info!(logger, "npub.cash enabled with URL: {url}");
686+
687+
// Start background polling for npub.cash quotes
688+
let wallet_for_npubcash = Arc::clone(&cashu_wallet);
689+
let sender_for_npubcash = mint_quote_sender.clone();
690+
let logger_for_npubcash = Arc::clone(&logger);
691+
let mut shutdown_for_npubcash = shutdown_sender.subscribe();
692+
runtime.spawn_cancellable_background_task(async move {
693+
let poll_interval = Duration::from_secs(30);
694+
loop {
695+
tokio::select! {
696+
_ = shutdown_for_npubcash.changed() => {
697+
log_info!(logger_for_npubcash, "npub.cash polling shutdown");
698+
return;
699+
}
700+
_ = tokio::time::sleep(poll_interval) => {
701+
match wallet_for_npubcash.sync_npubcash_quotes().await {
702+
Ok(quotes) => {
703+
for quote in quotes {
704+
if matches!(quote.state, cdk::nuts::MintQuoteState::Paid) {
705+
let id = quote.id.clone();
706+
if let Err(e) = sender_for_npubcash.send(quote).await {
707+
log_error!(
708+
logger_for_npubcash,
709+
"Failed to send npub.cash quote {id} for monitoring: {e}"
710+
);
711+
}
712+
}
713+
}
714+
},
715+
Err(e) => {
716+
log_error!(
717+
logger_for_npubcash,
718+
"Failed to sync npub.cash quotes: {e}"
719+
);
720+
},
721+
}
722+
}
723+
}
724+
}
725+
});
726+
}
727+
}
728+
655729
Ok(Cashu {
656730
cashu_wallet,
657731
unit: cashu_config.unit,
@@ -664,6 +738,7 @@ impl Cashu {
664738
event_queue,
665739
tx_metadata,
666740
runtime,
741+
npubcash_url,
667742
})
668743
}
669744

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)