From e3bf36f9b12769b2801c4d1541093c136bee1aaa Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Wed, 21 Jan 2026 17:32:59 +0100 Subject: [PATCH] Work on Nostr client --- .../assets/nostr_app.py | 174 ++----- .../assets/nostr_client.py | 449 +++++------------- 2 files changed, 177 insertions(+), 446 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_app.py b/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_app.py index 7a11a895..83a63eab 100644 --- a/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_app.py +++ b/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_app.py @@ -1,61 +1,43 @@ import lvgl as lv -from mpos import Activity, Intent, ConnectivityManager, MposKeyboard, pct_of_display_width, pct_of_display_height, SharedPreferences, SettingsActivity -from mpos.ui.anim import WidgetAnimator - -from fullscreen_qr import FullscreenQR +from mpos import Activity, Intent, ConnectivityManager, pct_of_display_width, pct_of_display_height, SharedPreferences, SettingsActivity class NostrApp(Activity): wallet = None - receive_qr_data = None - destination = None - balance_mode = 0 # 0=sats, 1=bits, 2=μBTC, 3=mBTC, 4=BTC - payments_label_current_font = 2 - payments_label_fonts = [ lv.font_montserrat_10, lv.font_unscii_8, lv.font_montserrat_16, lv.font_montserrat_24, lv.font_unscii_16, lv.font_montserrat_28_compressed, lv.font_montserrat_40] + events_label_current_font = 2 + events_label_fonts = [ lv.font_montserrat_10, lv.font_unscii_8, lv.font_montserrat_16, lv.font_montserrat_24, lv.font_unscii_16, lv.font_montserrat_28_compressed, lv.font_montserrat_40] # screens: main_screen = None # widgets balance_label = None - receive_qr = None - payments_label = None - - # activities - fullscreenqr = FullscreenQR() # need a reference to be able to finish() it + events_label = None def onCreate(self): self.prefs = SharedPreferences("com.micropythonos.nostr") self.main_screen = lv.obj() self.main_screen.set_style_pad_all(10, 0) - # This line needs to be drawn first, otherwise it's over the balance label and steals all the clicks! - balance_line = lv.line(self.main_screen) - balance_line.set_points([{'x':0,'y':35},{'x':200,'y':35}],2) - balance_line.add_flag(lv.obj.FLAG.CLICKABLE) + # Header line + header_line = lv.line(self.main_screen) + header_line.set_points([{'x':0,'y':35},{'x':200,'y':35}],2) + header_line.add_flag(lv.obj.FLAG.CLICKABLE) + # Header label showing which npub we're following self.balance_label = lv.label(self.main_screen) self.balance_label.set_text("") self.balance_label.align(lv.ALIGN.TOP_LEFT, 0, 0) self.balance_label.set_style_text_font(lv.font_montserrat_24, 0) self.balance_label.add_flag(lv.obj.FLAG.CLICKABLE) - self.balance_label.set_width(pct_of_display_width(75)) # 100 - receive_qr - self.balance_label.add_event_cb(self.balance_label_clicked_cb,lv.EVENT.CLICKED,None) - self.receive_qr = lv.qrcode(self.main_screen) - self.receive_qr.set_size(pct_of_display_width(20)) # bigger QR results in simpler code (less error correction?) - self.receive_qr.set_dark_color(lv.color_black()) - self.receive_qr.set_light_color(lv.color_white()) - self.receive_qr.align(lv.ALIGN.TOP_RIGHT,0,0) - self.receive_qr.set_style_border_color(lv.color_white(), 0) - self.receive_qr.set_style_border_width(1, 0); - self.receive_qr.add_flag(lv.obj.FLAG.CLICKABLE) - self.receive_qr.add_event_cb(self.qr_clicked_cb,lv.EVENT.CLICKED,None) - self.payments_label = lv.label(self.main_screen) - self.payments_label.set_text("") - self.payments_label.align_to(balance_line,lv.ALIGN.OUT_BOTTOM_LEFT,0,10) - self.update_payments_label_font() - self.payments_label.set_width(pct_of_display_width(75)) # 100 - receive_qr - self.payments_label.add_flag(lv.obj.FLAG.CLICKABLE) - self.payments_label.add_event_cb(self.payments_label_clicked,lv.EVENT.CLICKED,None) + self.balance_label.set_width(pct_of_display_width(100)) + # Events label + self.events_label = lv.label(self.main_screen) + self.events_label.set_text("") + self.events_label.align_to(header_line,lv.ALIGN.OUT_BOTTOM_LEFT,0,10) + self.update_events_label_font() + self.events_label.set_width(pct_of_display_width(100)) + self.events_label.add_flag(lv.obj.FLAG.CLICKABLE) + self.events_label.add_event_cb(self.events_label_clicked,lv.EVENT.CLICKED,None) settings_button = lv.button(self.main_screen) settings_button.set_size(lv.pct(20), lv.pct(25)) settings_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) @@ -64,15 +46,6 @@ class NostrApp(Activity): settings_label.set_text(lv.SYMBOL.SETTINGS) settings_label.set_style_text_font(lv.font_montserrat_24, 0) settings_label.center() - if False: # send button disabled for now, not implemented - send_button = lv.button(self.main_screen) - send_button.set_size(lv.pct(20), lv.pct(25)) - send_button.align_to(settings_button, lv.ALIGN.OUT_TOP_MID, 0, -pct_of_display_height(2)) - send_button.add_event_cb(self.send_button_tap,lv.EVENT.CLICKED,None) - send_label = lv.label(send_button) - send_label.set_text(lv.SYMBOL.UPLOAD) - send_label.set_style_text_font(lv.font_montserrat_24, 0) - send_label.center() self.setContentView(self.main_screen) def onStart(self, main_screen): @@ -85,9 +58,8 @@ class NostrApp(Activity): self.network_changed(cm.is_online()) def onPause(self, main_screen): - if self.wallet and self.destination != FullscreenQR: - self.wallet.stop() # don't stop the wallet for the fullscreen QR activity - self.destination = None + if self.wallet: + self.wallet.stop() cm = ConnectivityManager.get() cm.unregister_callback(self.network_changed) @@ -104,88 +76,52 @@ class NostrApp(Activity): return try: from nostr_client import NostrClient - self.wallet = NostrClient(self.prefs.get_string("nostr_nsec")) - self.wallet.follow_npub = self.prefs.get_string("nostr_follow_npub") - self.redraw_static_receive_code_cb() + nsec = self.prefs.get_string("nostr_nsec") + # Generate a random nsec if not configured + if not nsec: + from nostr.key import PrivateKey + random_key = PrivateKey() + nsec = random_key.bech32() + self.prefs.edit().put_string("nostr_nsec", nsec).commit() + print(f"Generated random nsec: {nsec}") + follow_npub = self.prefs.get_string("nostr_follow_npub") + relay = self.prefs.get_string("nostr_relay") + self.wallet = NostrClient(nsec, follow_npub, relay) except Exception as e: self.error_cb(f"Couldn't initialize Nostr client because: {e}") import sys sys.print_exception(e) return - self.balance_label.set_text(lv.SYMBOL.REFRESH) - self.payments_label.set_text(f"\nConnecting to backend.\n\nIf this takes too long, it might be down or something's wrong with the settings.") + self.balance_label.set_text("Events from " + self.prefs.get_string("nostr_follow_npub")[:16] + "...") + self.events_label.set_text(f"\nConnecting to relay.\n\nIf this takes too long, the relay might be down or something's wrong with the settings.") # by now, self.wallet can be assumed - self.wallet.start(self.balance_updated_cb, self.redraw_payments_cb, self.redraw_static_receive_code_cb, self.error_cb) + self.wallet.start(self.redraw_events_cb, self.error_cb) def went_offline(self): if self.wallet: self.wallet.stop() # don't stop the wallet for the fullscreen QR activity - self.payments_label.set_text(f"WiFi is not connected, can't talk to wallet...") + self.events_label.set_text(f"WiFi is not connected, can't talk to relay...") - def update_payments_label_font(self): - self.payments_label.set_style_text_font(self.payments_label_fonts[self.payments_label_current_font], 0) + def update_events_label_font(self): + self.events_label.set_style_text_font(self.events_label_fonts[self.events_label_current_font], 0) - def payments_label_clicked(self, event): - self.payments_label_current_font = (self.payments_label_current_font + 1) % len(self.payments_label_fonts) - self.update_payments_label_font() + def events_label_clicked(self, event): + self.events_label_current_font = (self.events_label_current_font + 1) % len(self.events_label_fonts) + self.update_events_label_font() - def float_to_string(self, value): - # Format float to string with fixed-point notation, up to 6 decimal places - s = "{:.8f}".format(value) - # Remove trailing zeros and decimal point if no decimals remain - return s.rstrip("0").rstrip(".") - - def display_balance(self, balance): - #print(f"displaying balance {balance}") - if self.balance_mode == 0: # sats - #balance_text = "丰 " + str(balance) # font doesnt support it - balance_text = str(balance) + " sat" - if balance > 1: - balance_text += "s" - elif self.balance_mode == 1: # bits (1 bit = 100 sats) - balance_bits = balance / 100 - balance_text = self.float_to_string(balance_bits) + " bit" - if balance_bits != 1: - balance_text += "s" - elif self.balance_mode == 2: # micro-BTC (1 μBTC = 100 sats) - balance_ubtc = balance / 100 - balance_text = self.float_to_string(balance_ubtc) + " micro-BTC" - elif self.balance_mode == 3: # milli-BTC (1 mBTC = 100000 sats) - balance_mbtc = balance / 100000 - balance_text = self.float_to_string(balance_mbtc) + " milli-BTC" - elif self.balance_mode == 4: # BTC (1 BTC = 100000000 sats) - balance_btc = balance / 100000000 - #balance_text = "₿ " + str(balance) # font doesnt support it - although it should https://fonts.google.com/specimen/Montserrat - balance_text = self.float_to_string(balance_btc) + " BTC" - self.balance_label.set_text(balance_text) - #print("done displaying balance") - - def balance_updated_cb(self, sats_added=0): - print(f"balance_updated_cb(sats_added={sats_added})") - if self.fullscreenqr.has_foreground(): - self.fullscreenqr.finish() - balance = self.wallet.last_known_balance - print(f"balance: {balance}") - if balance is not None: - WidgetAnimator.change_widget(self.balance_label, anim_type="interpolate", duration=5000, delay=0, begin_value=balance-sats_added, end_value=balance, display_change=self.display_balance) - else: - print("Not drawing balance because it's None") - - def redraw_payments_cb(self): + def redraw_events_cb(self): # this gets called from another thread (the wallet) so make sure it happens in the LVGL thread using lv.async_call(): - self.payments_label.set_text(str(self.wallet.payment_list)) - - def redraw_static_receive_code_cb(self): - # this gets called from another thread (the wallet) so make sure it happens in the LVGL thread using lv.async_call(): - self.receive_qr_data = self.wallet.static_receive_code - if self.receive_qr_data: - self.receive_qr.update(self.receive_qr_data, len(self.receive_qr_data)) + events_text = "" + if self.wallet.event_list: + for event in self.wallet.event_list: + events_text += f"{event.content}\n\n" else: - print("Warning: redraw_static_receive_code_cb() was called while self.wallet.static_receive_code is None...") + events_text = "No events yet..." + self.events_label.set_text(events_text) def error_cb(self, error): if self.wallet and self.wallet.is_running(): - self.payments_label.set_text(str(error)) + self.events_label.set_text(str(error)) def should_show_setting(self, setting): return True @@ -202,16 +138,4 @@ class NostrApp(Activity): def main_ui_set_defaults(self): self.balance_label.set_text("Welcome!") - self.payments_label.set_text(lv.SYMBOL.REFRESH) - - def balance_label_clicked_cb(self, event): - print("Balance clicked") - self.balance_mode = (self.balance_mode + 1) % 5 - self.display_balance(self.wallet.last_known_balance) - - def qr_clicked_cb(self, event): - print("QR clicked") - if not self.receive_qr_data: - return - self.destination = FullscreenQR - self.startActivity(Intent(activity_class=self.fullscreenqr).putExtra("receive_qr_data", self.receive_qr_data)) + self.events_label.set_text(lv.SYMBOL.REFRESH) diff --git a/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_client.py b/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_client.py index 3ac1484c..002c69ae 100644 --- a/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_client.py +++ b/internal_filesystem/apps/com.micropythonos.nostr/assets/nostr_client.py @@ -2,368 +2,175 @@ import ssl import json import time -from mpos.util import urldecode from mpos import TaskManager from nostr.relay_manager import RelayManager from nostr.message_type import ClientMessageType from nostr.filter import Filter, Filters -from nostr.event import EncryptedDirectMessage from nostr.key import PrivateKey -from payment import Payment -from unique_sorted_list import UniqueSortedList +class NostrEvent: + """Simple wrapper for a Nostr event""" + def __init__(self, event_obj): + self.event = event_obj + self.created_at = event_obj.created_at + self.content = event_obj.content + self.public_key = event_obj.public_key + + def __str__(self): + return f"{self.content}" class NostrClient(): + """Simple Nostr event subscriber that connects to a relay and subscribes to a public key's events""" - PAYMENTS_TO_SHOW = 6 - PERIODIC_FETCH_BALANCE_SECONDS = 60 # seconds + EVENTS_TO_SHOW = 10 - relays = [] - secret = None - wallet_pubkey = None + relay = None + nsec = None + follow_npub = None + private_key = None + relay_manager = None - def __init__(self, nwc_url): + def __init__(self, nsec, follow_npub, relay): super().__init__() - self.nwc_url = nwc_url - self.payment_list = UniqueSortedList() - if not nwc_url: - raise ValueError('NWC URL is not set.') + self.nsec = nsec + self.follow_npub = follow_npub + self.relay = relay + self.event_list = [] + + if not nsec: + raise ValueError('Nostr private key (nsec) is not set.') + if not follow_npub: + raise ValueError('Nostr follow public key (npub) is not set.') + if not relay: + raise ValueError('Nostr relay is not set.') + self.connected = False - self.relays, self.wallet_pubkey, self.secret, self.lud16 = self.parse_nwc_url(self.nwc_url) - if not self.relays: - raise ValueError('Missing relay in NWC URL.') - if not self.wallet_pubkey: - raise ValueError('Missing public key in NWC URL.') - if not self.secret: - raise ValueError('Missing "secret" in NWC URL.') - #if not self.lud16: - # raise ValueError('Missing lud16 (= lightning address) in NWC URL.') - def getCommentFromTransaction(self, transaction): - comment = "" + async def async_event_manager_task(self): + """Main event loop: connect to relay and subscribe to events""" try: - comment = transaction["description"] - if comment is None: - return comment - json_comment = json.loads(comment) - for field in json_comment: - if field[0] == "text/plain": - comment = field[1] + # Initialize private key from nsec + # nsec can be in bech32 format (nsec1...) or hex format + if self.nsec.startswith("nsec1"): + self.private_key = PrivateKey.from_nsec(self.nsec) + else: + self.private_key = PrivateKey(bytes.fromhex(self.nsec)) + + # Initialize relay manager + self.relay_manager = RelayManager() + self.relay_manager.add_relay(self.relay) + + print(f"DEBUG: Opening relay connection to {self.relay}") + await self.relay_manager.open_connections({"cert_reqs": ssl.CERT_NONE}) + + self.connected = False + for _ in range(100): + await TaskManager.sleep(0.1) + nrconnected = self.relay_manager.connected_or_errored_relays() + if nrconnected == 1 or not self.keep_running: break - else: - print("text/plain field is missing from JSON description") - except Exception as e: - print(f"Info: comment {comment} is not JSON, this is fine, using as-is ({e})") - return comment - - async def async_wallet_manager_task(self): - if self.lud16: - self.handle_new_static_receive_code(self.lud16) - - self.private_key = PrivateKey(bytes.fromhex(self.secret)) - self.relay_manager = RelayManager() - for relay in self.relays: - self.relay_manager.add_relay(relay) - - print(f"DEBUG: Opening relay connections") - await self.relay_manager.open_connections({"cert_reqs": ssl.CERT_NONE}) - self.connected = False - nrconnected = 0 - for _ in range(100): - await TaskManager.sleep(0.1) - nrconnected = self.relay_manager.connected_or_errored_relays() - #print(f"Waiting for relay connections, currently: {nrconnected}/{len(self.relays)}") - if nrconnected == len(self.relays) or not self.keep_running: - break - if nrconnected == 0: - self.handle_error("Could not connect to any Nostr Wallet Connect relays.") - return - if not self.keep_running: - print(f"async_wallet_manager_task does not have self.keep_running, returning...") - return - - print(f"{nrconnected} relays connected") - - # Set up subscription to receive response - self.subscription_id = "micropython_nwc_" + str(round(time.time())) - print(f"DEBUG: Setting up subscription with ID: {self.subscription_id}") - self.filters = Filters([Filter( - #event_ids=[self.subscription_id], would be nice to filter, but not like this - kinds=[23195, 23196], # NWC reponses and notifications - authors=[self.wallet_pubkey], - pubkey_refs=[self.private_key.public_key.hex()] - )]) - print(f"DEBUG: Subscription filters: {self.filters.to_json_array()}") - self.relay_manager.add_subscription(self.subscription_id, self.filters) - print(f"DEBUG: Creating subscription request") - request_message = [ClientMessageType.REQUEST, self.subscription_id] - request_message.extend(self.filters.to_json_array()) - print(f"DEBUG: Publishing subscription request") - self.relay_manager.publish_message(json.dumps(request_message)) - print(f"DEBUG: Published subscription request") - - last_fetch_balance = time.time() - self.PERIODIC_FETCH_BALANCE_SECONDS - while True: # handle incoming events and do periodic fetch_balance - #print(f"checking for incoming events...") - await TaskManager.sleep(0.1) - if not self.keep_running: - print("NWCWallet: not keep_running, closing connections...") - await self.relay_manager.close_connections() - break - - if time.time() - last_fetch_balance >= self.PERIODIC_FETCH_BALANCE_SECONDS: - last_fetch_balance = time.time() - try: - await self.fetch_balance() - except Exception as e: - print(f"fetch_balance got exception {e}") # fetch_balance got exception 'NoneType' object isn't iterable?! - - start_time = time.ticks_ms() - if self.relay_manager.message_pool.has_events(): - print(f"DEBUG: Event received from message pool after {time.ticks_ms()-start_time}ms") - event_msg = self.relay_manager.message_pool.get_event() - event_created_at = event_msg.event.created_at - print(f"Received at {time.localtime()} a message with timestamp {event_created_at} after {time.ticks_ms()-start_time}ms") - try: - # This takes a very long time, even for short messages: - decrypted_content = self.private_key.decrypt_message( - event_msg.event.content, - event_msg.event.public_key, - ) - print(f"DEBUG: Decrypted content: {decrypted_content} after {time.ticks_ms()-start_time}ms") - response = json.loads(decrypted_content) - print(f"DEBUG: Parsed response: {response}") - result = response.get("result") - if result: - if result.get("balance") is not None: - new_balance = round(int(result["balance"]) / 1000) - print(f"Got balance: {new_balance}") - self.handle_new_balance(new_balance) - elif result.get("transactions") is not None: - print("Response contains transactions!") - new_payment_list = UniqueSortedList() - for transaction in result["transactions"]: - amount = transaction["amount"] - amount = round(amount / 1000) - comment = self.getCommentFromTransaction(transaction) - epoch_time = transaction["created_at"] - paymentObj = Payment(epoch_time, amount, comment) - new_payment_list.add(paymentObj) - if len(new_payment_list) > 0: - # do them all in one shot instead of one-by-one because the lv_async() isn't always chronological, - # so when a long list of payments is added, it may be overwritten by a short list - self.handle_new_payments(new_payment_list) - else: - notification = response.get("notification") - if notification: - amount = notification["amount"] - amount = round(amount / 1000) - type = notification["type"] - if type == "outgoing": - amount = -amount - elif type == "incoming": - new_balance = self.last_known_balance + amount - self.handle_new_balance(new_balance, False) # don't trigger full fetch because payment info is in notification - epoch_time = notification["created_at"] - comment = self.getCommentFromTransaction(notification) - paymentObj = Payment(epoch_time, amount, comment) - self.handle_new_payment(paymentObj) - else: - print(f"WARNING: invalid notification type {type}, ignoring.") - else: - print("Unsupported response, ignoring.") - except Exception as e: - print(f"DEBUG: Error processing response: {e}") - import sys - sys.print_exception(e) # Full traceback on MicroPython - else: - #print(f"pool has no events after {time.ticks_ms()-start_time}ms") # completes in 0-1ms - pass - - def fetch_balance(self): - try: - if not self.keep_running: + + if nrconnected == 0: + self.handle_error("Could not connect to Nostr relay.") return - # Create get_balance request - balance_request = { - "method": "get_balance", - "params": {} - } - print(f"DEBUG: Created balance request: {balance_request}") - print(f"DEBUG: Creating encrypted DM to wallet pubkey: {self.wallet_pubkey}") - dm = EncryptedDirectMessage( - recipient_pubkey=self.wallet_pubkey, - cleartext_content=json.dumps(balance_request), - kind=23194 - ) - print(f"DEBUG: Signing DM {json.dumps(dm)} with private key") - self.private_key.sign_event(dm) # sign also does encryption if it's a encrypted dm - print(f"DEBUG: Publishing encrypted DM") - self.relay_manager.publish_event(dm) + + if not self.keep_running: + print(f"async_event_manager_task: not keep_running, returning...") + return + + print(f"Relay connected") + self.connected = True + + # Set up subscription to receive events from follow_npub + self.subscription_id = "micropython_nostr_" + str(round(time.time())) + print(f"DEBUG: Setting up subscription with ID: {self.subscription_id}") + + # Create filter for events from follow_npub + self.filters = Filters([Filter( + kinds=[1], # Text notes + authors=[self.follow_npub], + )]) + print(f"DEBUG: Subscription filters: {self.filters.to_json_array()}") + self.relay_manager.add_subscription(self.subscription_id, self.filters) + + print(f"DEBUG: Creating subscription request") + request_message = [ClientMessageType.REQUEST, self.subscription_id] + request_message.extend(self.filters.to_json_array()) + print(f"DEBUG: Publishing subscription request") + self.relay_manager.publish_message(json.dumps(request_message)) + print(f"DEBUG: Published subscription request") + + # Main event loop + while True: + await TaskManager.sleep(0.1) + if not self.keep_running: + print("NostrClient: not keep_running, closing connections...") + await self.relay_manager.close_connections() + break + + start_time = time.ticks_ms() + if self.relay_manager.message_pool.has_events(): + print(f"DEBUG: Event received from message pool after {time.ticks_ms()-start_time}ms") + event_msg = self.relay_manager.message_pool.get_event() + event_created_at = event_msg.event.created_at + print(f"Received at {time.localtime()} a message with timestamp {event_created_at} after {time.ticks_ms()-start_time}ms") + try: + # Create NostrEvent wrapper + nostr_event = NostrEvent(event_msg.event) + print(f"DEBUG: Event content: {nostr_event.content}") + + # Add to event list + self.handle_new_event(nostr_event) + + except Exception as e: + print(f"DEBUG: Error processing event: {e}") + import sys + sys.print_exception(e) + except Exception as e: - print(f"inside fetch_balance exception: {e}") + print(f"async_event_manager_task exception: {e}") + import sys + sys.print_exception(e) + self.handle_error(f"Error in event manager: {e}") - def fetch_payments(self): - if not self.keep_running: - return - # Create get_balance request - list_transactions = { - "method": "list_transactions", - "params": { - "limit": self.PAYMENTS_TO_SHOW - } - } - dm = EncryptedDirectMessage( - recipient_pubkey=self.wallet_pubkey, - cleartext_content=json.dumps(list_transactions), - kind=23194 - ) - self.private_key.sign_event(dm) # sign also does encryption if it's a encrypted dm - print("\nPublishing DM to fetch payments...") - self.relay_manager.publish_event(dm) - - def parse_nwc_url(self, nwc_url): - """Parse Nostr Wallet Connect URL to extract pubkey, relays, secret, and lud16.""" - print(f"DEBUG: Starting to parse NWC URL: {nwc_url}") - try: - # Remove 'nostr+walletconnect://' or 'nwc:' prefix - if nwc_url.startswith('nostr+walletconnect://'): - print(f"DEBUG: Removing 'nostr+walletconnect://' prefix") - nwc_url = nwc_url[22:] - elif nwc_url.startswith('nwc:'): - print(f"DEBUG: Removing 'nwc:' prefix") - nwc_url = nwc_url[4:] - else: - print(f"DEBUG: No recognized prefix found in URL") - raise ValueError("Invalid NWC URL: missing 'nostr+walletconnect://' or 'nwc:' prefix") - print(f"DEBUG: URL after prefix removal: {nwc_url}") - # urldecode because the relay might have %3A%2F%2F etc - nwc_url = urldecode(nwc_url) - print(f"after urldecode: {nwc_url}") - # Split into pubkey and query params - parts = nwc_url.split('?') - pubkey = parts[0] - print(f"DEBUG: Extracted pubkey: {pubkey}") - # Validate pubkey (should be 64 hex characters) - if len(pubkey) != 64 or not all(c in '0123456789abcdef' for c in pubkey): - raise ValueError("Invalid NWC URL: pubkey must be 64 hex characters") - # Extract relay, secret, and lud16 from query params - relays = [] - lud16 = None - secret = None - if len(parts) > 1: - print(f"DEBUG: Query parameters found: {parts[1]}") - params = parts[1].split('&') - for param in params: - if param.startswith('relay='): - relay = param[6:] - print(f"DEBUG: Extracted relay: {relay}") - relays.append(relay) - elif param.startswith('secret='): - secret = param[7:] - print(f"DEBUG: Extracted secret: {secret}") - elif param.startswith('lud16='): - lud16 = param[6:] - print(f"DEBUG: Extracted lud16: {lud16}") - else: - print(f"DEBUG: No query parameters found") - if not pubkey or not len(relays) > 0 or not secret: - raise ValueError("Invalid NWC URL: missing required fields (pubkey, relay, or secret)") - # Validate secret (should be 64 hex characters) - if len(secret) != 64 or not all(c in '0123456789abcdef' for c in secret): - raise ValueError("Invalid NWC URL: secret must be 64 hex characters") - print(f"DEBUG: Parsed NWC data - Relay: {relays}, Pubkey: {pubkey}, Secret: {secret}, lud16: {lud16}") - return relays, pubkey, secret, lud16 - except Exception as e: - raise RuntimeError(f"Exception parsing NWC URL {nwc_url}: {e}") - - - # From wallet.py: # Public variables - # These values could be loading from a cache.json file at __init__ last_known_balance = 0 - payment_list = None - static_receive_code = None + event_list = None # Variables keep_running = True # Callbacks: - balance_updated_cb = None - payments_updated_cb = None - static_receive_code_updated_cb = None + events_updated_cb = None error_cb = None - - def __str__(self): - if isinstance(self, LNBitsWallet): - return "LNBitsWallet" - elif isinstance(self, NWCWallet): - return "NWCWallet" - - def handle_new_balance(self, new_balance, fetchPaymentsIfChanged=True): - if not self.keep_running or new_balance is None: - return - sats_added = new_balance - self.last_known_balance - if new_balance != self.last_known_balance: - print("Balance changed!") - self.last_known_balance = new_balance - print("Calling balance_updated_cb") - self.balance_updated_cb(sats_added) - if fetchPaymentsIfChanged: # Fetching *all* payments isn't necessary if balance was changed by a payment notification - print("Refreshing payments...") - self.fetch_payments() # if the balance changed, then re-list transactions - - def handle_new_payment(self, new_payment): + def handle_new_event(self, new_event): + """Handle a new event from the relay""" if not self.keep_running: return - print("handle_new_payment") - self.payment_list.add(new_payment) - self.payments_updated_cb() - - def handle_new_payments(self, new_payments): - if not self.keep_running: - return - print("handle_new_payments") - if self.payment_list != new_payments: - print("new list of payments") - self.payment_list = new_payments - self.payments_updated_cb() - - def handle_new_static_receive_code(self, new_static_receive_code): - print("handle_new_static_receive_code") - if not self.keep_running or not new_static_receive_code: - print("not self.keep_running or not new_static_receive_code") - return - if self.static_receive_code != new_static_receive_code: - print("it's really a new static_receive_code") - self.static_receive_code = new_static_receive_code - if self.static_receive_code_updated_cb: - self.static_receive_code_updated_cb() - else: - print(f"self.static_receive_code {self.static_receive_code } == new_static_receive_code {new_static_receive_code}") + print("handle_new_event") + self.event_list.append(new_event) + # Keep only the most recent EVENTS_TO_SHOW events + if len(self.event_list) > self.EVENTS_TO_SHOW: + self.event_list = self.event_list[-self.EVENTS_TO_SHOW:] + if self.events_updated_cb: + self.events_updated_cb() def handle_error(self, e): if self.error_cb: self.error_cb(e) - # Maybe also add callbacks for: - # - started (so the user can show the UI) - # - stopped (so the user can delete/free it) - # - error (so the user can show the error) - def start(self, balance_updated_cb, payments_updated_cb, static_receive_code_updated_cb = None, error_cb = None): + def start(self, events_updated_cb, error_cb=None): + """Start the event manager task""" self.keep_running = True - self.balance_updated_cb = balance_updated_cb - self.payments_updated_cb = payments_updated_cb - self.static_receive_code_updated_cb = static_receive_code_updated_cb + self.events_updated_cb = events_updated_cb self.error_cb = error_cb - TaskManager.create_task(self.async_wallet_manager_task()) + TaskManager.create_task(self.async_event_manager_task()) def stop(self): + """Stop the event manager task""" self.keep_running = False - # idea: do a "close connections" call here instead of waiting for polling sub-tasks to notice the change def is_running(self): return self.keep_running -