Work on Nostr client

This commit is contained in:
Thomas Farstrike
2026-01-21 17:32:59 +01:00
parent faa46fbb18
commit e3bf36f9b1
2 changed files with 177 additions and 446 deletions
@@ -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)
@@ -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