@@ -52,6 +52,8 @@ pub struct CashuConfig {
5252 pub mint_url : String ,
5353 /// The currency unit to use (typically Sat)
5454 pub unit : CurrencyUnit ,
55+ /// Optional npub.cash URL for lightning address support (e.g., `https://npub.cash`)
56+ pub npubcash_url : Option < String > ,
5557}
5658
5759/// A wallet implementation using the Cashu (CDK) SDK.
@@ -68,6 +70,8 @@ pub struct Cashu {
6870 event_queue : Arc < EventQueue > ,
6971 tx_metadata : TxMetadataStore ,
7072 runtime : Arc < Runtime > ,
73+ npubcash_url : Option < String > ,
74+ npub : Option < String > ,
7175}
7276
7377impl TrustedWalletInterface for Cashu {
@@ -497,16 +501,30 @@ 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+ match ( & self . npubcash_url , & self . npub ) {
506+ ( Some ( url) , Some ( npub) ) => {
507+ let domain =
508+ url. trim_start_matches ( "https://" ) . trim_start_matches ( "http://" ) ;
509+ Ok ( Some ( format ! ( "{npub}@{domain}" ) ) )
510+ } ,
511+ _ => Ok ( None ) ,
512+ }
513+ } )
501514 }
502515
503516 fn register_lightning_address (
504517 & self , _name : String ,
505518 ) -> Pin < Box < dyn Future < Output = Result < ( ) , TrustedError > > + Send + ' _ > > {
506519 Box :: pin ( async {
507- Err ( TrustedError :: UnsupportedOperation (
508- "register_lightning_address is not supported in Cashu Wallet" . to_string ( ) ,
509- ) )
520+ if self . npubcash_url . is_none ( ) {
521+ return Err ( TrustedError :: UnsupportedOperation (
522+ "npubcash_url is not configured" . to_string ( ) ,
523+ ) ) ;
524+ }
525+ // npub.cash addresses are deterministic from the Nostr keys,
526+ // and set_mint_url is called during init. Nothing to do here.
527+ Ok ( ( ) )
510528 } )
511529 }
512530
@@ -652,6 +670,65 @@ impl Cashu {
652670 } ) ;
653671 }
654672
673+ // Initialize npub.cash if configured
674+ let npubcash_url = cashu_config. npubcash_url . clone ( ) ;
675+ let mut npub: Option < String > = None ;
676+
677+ if let Some ( ref url) = npubcash_url {
678+ npub = Some ( Self :: derive_npub ( & seed) . map_err ( |e| {
679+ InitFailure :: TrustedFailure ( TrustedError :: WalletOperationFailed ( format ! (
680+ "Failed to derive npub: {e}"
681+ ) ) )
682+ } ) ?) ;
683+
684+ if let Err ( e) = cashu_wallet. enable_npubcash ( url. clone ( ) ) . await {
685+ log_error ! ( logger, "Failed to enable npub.cash: {e}" ) ;
686+ } else {
687+ log_info ! ( logger, "npub.cash enabled with URL: {url}" ) ;
688+
689+ // Start background polling for npub.cash quotes
690+ let wallet_for_npubcash = Arc :: clone ( & cashu_wallet) ;
691+ let sender_for_npubcash = mint_quote_sender. clone ( ) ;
692+ let logger_for_npubcash = Arc :: clone ( & logger) ;
693+ let mut shutdown_for_npubcash = shutdown_sender. subscribe ( ) ;
694+ runtime. spawn_cancellable_background_task ( async move {
695+ let poll_interval = Duration :: from_secs ( 30 ) ;
696+ let mut interval = tokio:: time:: interval ( poll_interval) ;
697+ loop {
698+ tokio:: select! {
699+ _ = shutdown_for_npubcash. changed( ) => {
700+ log_info!( logger_for_npubcash, "npub.cash polling shutdown" ) ;
701+ return ;
702+ }
703+ _ = interval. tick( ) => {
704+ match wallet_for_npubcash. sync_npubcash_quotes( ) . await {
705+ Ok ( quotes) => {
706+ for quote in quotes {
707+ if matches!( quote. state, cdk:: nuts:: MintQuoteState :: Paid ) {
708+ let id = quote. id. clone( ) ;
709+ if let Err ( e) = sender_for_npubcash. send( quote) . await {
710+ log_error!(
711+ logger_for_npubcash,
712+ "Failed to send npub.cash quote {id} for monitoring: {e}"
713+ ) ;
714+ }
715+ }
716+ }
717+ } ,
718+ Err ( e) => {
719+ log_error!(
720+ logger_for_npubcash,
721+ "Failed to sync npub.cash quotes: {e}"
722+ ) ;
723+ } ,
724+ }
725+ }
726+ }
727+ }
728+ } ) ;
729+ }
730+ }
731+
655732 Ok ( Cashu {
656733 cashu_wallet,
657734 unit : cashu_config. unit ,
@@ -664,9 +741,27 @@ impl Cashu {
664741 event_queue,
665742 tx_metadata,
666743 runtime,
744+ npubcash_url,
745+ npub,
667746 } )
668747 }
669748
749+ /// Derive the npub (bech32-encoded Nostr public key) from the wallet seed.
750+ ///
751+ /// Uses the same derivation as CDK's `derive_npubcash_keys`: the first 32 bytes
752+ /// of the seed as a secp256k1 secret key, then bech32-encodes the x-only public key.
753+ fn derive_npub ( seed : & [ u8 ; 64 ] ) -> Result < String , String > {
754+ use ldk_node:: bitcoin:: bech32:: { Bech32 , Hrp , encode} ;
755+ use ldk_node:: bitcoin:: secp256k1:: { Secp256k1 , SecretKey } ;
756+
757+ let sk = SecretKey :: from_slice ( & seed[ ..32 ] )
758+ . map_err ( |e| format ! ( "Invalid secret key: {e}" ) ) ?;
759+ let secp = Secp256k1 :: new ( ) ;
760+ let ( xonly, _) = sk. public_key ( & secp) . x_only_public_key ( ) ;
761+ let hrp = Hrp :: parse ( "npub" ) . expect ( "valid hrp" ) ;
762+ encode :: < Bech32 > ( hrp, & xonly. serialize ( ) ) . map_err ( |e| format ! ( "bech32 encode: {e}" ) )
763+ }
764+
670765 /// Convert an ID string to a 32-byte array
671766 ///
672767 /// This is a helper function to avoid code duplication when converting various ID types
0 commit comments