@@ -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+ seed : [ u8 ; 64 ] ,
7175}
7276
7377impl TrustedWalletInterface for Cashu {
@@ -497,13 +501,29 @@ 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 npub = self . derive_npub ( ) . map_err ( |e| {
507+ TrustedError :: WalletOperationFailed ( format ! (
508+ "Failed to derive npub: {e}"
509+ ) )
510+ } ) ?;
511+ let domain = url. trim_start_matches ( "https://" ) . trim_start_matches ( "http://" ) ;
512+ return Ok ( Some ( format ! ( "{npub}@{domain}" ) ) ) ;
513+ }
514+ Ok ( None )
515+ } )
501516 }
502517
503518 fn register_lightning_address (
504519 & self , _name : String ,
505520 ) -> Pin < Box < dyn Future < Output = Result < ( ) , TrustedError > > + Send + ' _ > > {
506521 Box :: pin ( async {
522+ if self . npubcash_url . is_some ( ) {
523+ // npub.cash addresses are deterministic from the Nostr keys,
524+ // and set_mint_url is called during init. Nothing to do here.
525+ return Ok ( ( ) ) ;
526+ }
507527 Err ( TrustedError :: UnsupportedOperation (
508528 "register_lightning_address is not supported in Cashu Wallet" . to_string ( ) ,
509529 ) )
@@ -652,6 +672,57 @@ impl Cashu {
652672 } ) ;
653673 }
654674
675+ // Initialize npub.cash if configured
676+ let npubcash_url = cashu_config. npubcash_url . clone ( ) ;
677+
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+
655726 Ok ( Cashu {
656727 cashu_wallet,
657728 unit : cashu_config. unit ,
@@ -664,9 +735,27 @@ impl Cashu {
664735 event_queue,
665736 tx_metadata,
666737 runtime,
738+ npubcash_url,
739+ seed,
667740 } )
668741 }
669742
743+ /// Derive the npub (bech32-encoded Nostr public key) from the wallet seed.
744+ ///
745+ /// Uses the same derivation as CDK's `derive_npubcash_keys`: the first 32 bytes
746+ /// of the seed as a secp256k1 secret key, then bech32-encodes the x-only public key.
747+ fn derive_npub ( & self ) -> Result < String , String > {
748+ use ldk_node:: bitcoin:: bech32:: { Bech32 , Hrp , encode} ;
749+ use ldk_node:: bitcoin:: secp256k1:: { Secp256k1 , SecretKey } ;
750+
751+ let sk = SecretKey :: from_slice ( & self . seed [ ..32 ] )
752+ . map_err ( |e| format ! ( "Invalid secret key: {e}" ) ) ?;
753+ let secp = Secp256k1 :: new ( ) ;
754+ let ( xonly, _) = sk. public_key ( & secp) . x_only_public_key ( ) ;
755+ let hrp = Hrp :: parse ( "npub" ) . expect ( "valid hrp" ) ;
756+ encode :: < Bech32 > ( hrp, & xonly. serialize ( ) ) . map_err ( |e| format ! ( "bech32 encode: {e}" ) )
757+ }
758+
670759 /// Convert an ID string to a 32-byte array
671760 ///
672761 /// This is a helper function to avoid code duplication when converting various ID types
0 commit comments