diff --git a/draft_code/queue_test.py b/draft_code/queue_test.py new file mode 100644 index 00000000..6d0515f2 --- /dev/null +++ b/draft_code/queue_test.py @@ -0,0 +1,64 @@ +import uasyncio +from collections import deque + +class AsyncQueue: + def __init__(self, maxlen=10): # Set a default maximum length + self._queue = deque((), maxlen, True) # Initialize deque with specified maxlen + self._event = uasyncio.Event() # Event for signaling when items are added + + async def get(self, timeout=None): + """Get an item from the queue, waiting if empty until an item is available or timeout expires.""" + while not self._queue: + if timeout is not None: + try: + await uasyncio.wait_for(self._event.wait(), timeout) # Wait for item or timeout + except uasyncio.TimeoutError: + raise Empty("Queue is empty and timed out") + else: + await self._event.wait() # Wait indefinitely for an item + self._event.clear() # Clear event after waking up + return self._queue.popleft() # Return the item + + async def put(self, item): + """Put an item in the queue and signal waiting coroutines.""" + self._queue.append(item) # This will now work with proper maxlen + self._event.set() # Signal that an item is available + + def qsize(self): + """Return the current size of the queue.""" + return len(self._queue) + + def empty(self): + """Return True if the queue is empty.""" + return len(self._queue) == 0 + +class Empty(Exception): + """Exception raised when queue is empty and non-blocking or timeout occurs.""" + pass + + +import uasyncio +#from async_queue import AsyncQueue, Empty # Assuming the above code is in async_queue.py + +async def producer(queue): + for i in range(5): + print(f"Producing {i}") + await queue.put(i) + await uasyncio.sleep(1) # Simulate some delay + +async def consumer(queue): + while True: + try: + item = await queue.get(timeout=2.0) # Wait up to 2 seconds + print(f"Consumed {item}") + except Empty: + print("Consumer timed out waiting for item") + break + +async def main(): + queue = AsyncQueue() + # Run producer and consumer concurrently in the event loop + await uasyncio.gather(producer(queue), consumer(queue)) + +# Run the event loop +uasyncio.run(main()) diff --git a/draft_code/test_aes_cbc.py b/draft_code/test_aes_cbc.py new file mode 100644 index 00000000..05307e24 --- /dev/null +++ b/draft_code/test_aes_cbc.py @@ -0,0 +1,25 @@ +import ucryptolib +import os + +key = os.urandom(32) +iv = bytes.fromhex("cafc34a94307c35f8c8f736831713467") +#iv = bytes.fromhex("cafc34a94307c35f8c8f736831713468") # changing the IV doesn't change the output! +#iv = os.urandom(16) +data = b'{"method":"get_balance","params":{}}' +pad_length = 16 - (len(data) % 16) +padded_data = data + bytes([pad_length] * pad_length) +print(f"Test padded_data: {padded_data.hex()}") + +cipher = ucryptolib.aes(key, 1, iv) +ciphertext = cipher.encrypt(padded_data) +print(f"Test ciphertext: {ciphertext.hex()}") + +cipher = ucryptolib.aes(key, 1, iv) +decrypted = cipher.decrypt(ciphertext) +print(f"Test decrypted: {decrypted.hex()}") + +pad_length = decrypted[-1] +if decrypted[-pad_length:] != bytes([pad_length] * pad_length): + print(f"Test failed: invalid padding, got {decrypted[-pad_length:].hex()}") +else: + print(f"Test passed: valid padding") diff --git a/draft_code/test_aes_cbc_iv.py b/draft_code/test_aes_cbc_iv.py new file mode 100644 index 00000000..aec4403e --- /dev/null +++ b/draft_code/test_aes_cbc_iv.py @@ -0,0 +1,46 @@ +import ucryptolib +import os +import sys + +print(f"MicroPython version: {sys.version}") +print(f"Platform: {sys.platform}") + +key = os.urandom(32) +iv1 = bytes.fromhex("cafc34a94307c35f8c8f736831713467") +iv2 = bytes.fromhex("cafc34a94307c35f8c8f736831713468") +iv3 = os.urandom(16) # Random IV +data = b'{"method":"get_balance","params":{}}' +pad_length = 16 - (len(data) % 16) +padded_data = data + bytes([pad_length] * pad_length) +print(f"Test key: {key.hex()}") +print(f"Test padded_data: {padded_data.hex()} (length: {len(padded_data)})") + +mode_cbc = 2 + +# Test with IV1 +cipher1 = ucryptolib.aes(key, mode_cbc, iv1) +ciphertext1 = cipher1.encrypt(padded_data) +print(f"IV1: {iv1.hex()}, Ciphertext1: {ciphertext1.hex()}") + +# Test with IV2 +cipher2 = ucryptolib.aes(key, mode_cbc, iv2) +ciphertext2 = cipher2.encrypt(padded_data) +print(f"IV2: {iv2.hex()}, Ciphertext2: {ciphertext2.hex()}") + +# Test with IV3 +cipher3 = ucryptolib.aes(key, mode_cbc, iv3) +ciphertext3 = cipher3.encrypt(padded_data) +print(f"IV3: {iv3.hex()}, Ciphertext3: {ciphertext3.hex()}") + +# Compare ciphertexts +print(f"Ciphertext1 == Ciphertext2: {ciphertext1 == ciphertext2}") +print(f"Ciphertext1 == Ciphertext3: {ciphertext1 == ciphertext3}") + +# Verify decryption +cipher_decrypt = ucryptolib.aes(key, 1, iv1) +decrypted = cipher_decrypt.decrypt(ciphertext1) +print(f"Decrypted with IV1: {decrypted.hex()}") +if decrypted[-pad_length:] != bytes([pad_length] * pad_length): + print(f"Test failed: invalid padding, got {decrypted[-pad_length:].hex()}") +else: + print(f"Test passed: valid padding") diff --git a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py index 926e73a1..f08a66ca 100644 --- a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py +++ b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py @@ -11,6 +11,7 @@ settings_screen = None # widgets balance_label = None +payments_label = None wallet = None @@ -191,21 +192,25 @@ def settings_button_tap(event): mpos.ui.load_screen(settings_screen) def build_main_ui(): - global main_screen, balance_label + global main_screen, balance_label, payments_label main_screen = lv.obj() main_screen.set_style_pad_all(10, 0) balance_label = lv.label(main_screen) balance_label.align(lv.ALIGN.TOP_LEFT, 0, 0) - balance_label.set_style_text_font(lv.font_montserrat_20, 0) - balance_label.set_text('123456') + balance_label.set_style_text_font(lv.font_montserrat_22, 0) + balance_label.set_text(lv.SYMBOL.REFRESH) style_line = lv.style_t() style_line.init() - style_line.set_line_width(4) + style_line.set_line_width(2) style_line.set_line_color(lv.palette_main(lv.PALETTE.PINK)) style_line.set_line_rounded(True) balance_line = lv.line(main_screen) balance_line.set_points([{'x':0,'y':35},{'x':300,'y':35}],2) balance_line.add_style(style_line, 0) + payments_label = lv.label(main_screen) + payments_label.align_to(balance_line,lv.ALIGN.OUT_BOTTOM_LEFT,0,10) + payments_label.set_style_text_font(lv.font_montserrat_16, 0) + payments_label.set_text(lv.SYMBOL.REFRESH) settings_button = lv.button(main_screen) settings_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) snap_label = lv.label(settings_button) @@ -215,10 +220,14 @@ def build_main_ui(): mpos.ui.load_screen(main_screen) -def redraw_balance_cb(timer): +def redraw_balance_cb(): global balance_label - if balance_label.get_text() != str(wallet.last_known_balance): - balance_label.set_text(str(wallet.last_known_balance)) + balance_label.set_text(str(wallet.last_known_balance)) + +def redraw_payments_cb(): + global payments_label + print("redrawing payments") + payments_label.set_text(wallet.payment_list_string()) def janitor_cb(timer): global wallet, config @@ -239,7 +248,7 @@ def janitor_cb(timer): else: print(f"No or unsupported wallet type configured: '{wallet_type}'") if wallet: - wallet.start(lambda : balance_label.set_text(str(wallet.last_known_balance))) + wallet.start(redraw_balance_cb, redraw_payments_cb) else: print("ERROR: could not start any wallet!") # maybe call the error callback to show the error to the user elif lv.screen_active() != main_screen and lv.screen_active() != settings_screen: diff --git a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/wallet.py b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/wallet.py index e2303dd0..170d387e 100644 --- a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/wallet.py +++ b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/wallet.py @@ -12,12 +12,14 @@ from nostr.key import PrivateKey import mpos.apps import mpos.time +import mpos.util class Wallet: # These values could be loading from a cache.json file at __init__ - last_known_balance = 0 + last_known_balance = -1 #last_known_balance_timestamp = 0 + payment_list = [] def __init__(self): self.keep_running = True @@ -28,16 +30,33 @@ class Wallet: elif isinstance(self, NWCWallet): return "NWCWallet" + def are_payment_lists_equal(self, list1, list2): + if len(list1) != len(list2): + return False + return all(p1 == p2 for p1, p2 in zip(list1, list2)) + + def handle_new_payments(self, new_payments): + print("handle_new_payments") + if not self.are_payment_lists_equal(self.payment_list, new_payments): + print("new list of payments") + self.payment_list = new_payments + self.payments_updated_cb() + + def payment_list_string(self): + return "\n".join(f"{payment.amount_sats} sats: {payment.comment}" for payment in self.payment_list) + # Need 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) # - balance # - transactions - def start(self, balance_updated_cb): + def start(self, balance_updated_cb, payments_updated_cb): self.keep_running = True + self.balance_updated_cb = balance_updated_cb + self.payments_updated_cb = payments_updated_cb _thread.stack_size(mpos.apps.good_stack_size()) - _thread.start_new_thread(self.wallet_manager_thread, (balance_updated_cb,)) + _thread.start_new_thread(self.wallet_manager_thread, ()) def stop(self): self.keep_running = False @@ -52,20 +71,23 @@ class LNBitsWallet(Wallet): self.lnbits_url = lnbits_url self.lnbits_readkey = lnbits_readkey - def wallet_manager_thread(self, balance_updated_cb): + def wallet_manager_thread(self): print("wallet_manager_thread") while self.keep_running: try: - self.last_known_balance = fetch_balance() - balance_updated_cb() - # TODO: if the balance changed, then re-list transactions + new_balance = self.fetch_balance() + if new_balance != self.last_known_balance: + self.last_known_balance = new_balance + self.balance_updated_cb() + new_payments = self.fetch_payments() # if the balance changed, then re-list transactions + self.handle_new_payments(new_payments) except Exception as e: - print(f"WARNING: fetch_balance got exception {e}, ignorning.") + print(f"WARNING: wallet_manager_thread got exception {e}, ignorning.") print("Sleeping a while before re-fetching balance...") time.sleep(60) print("wallet_manager_thread stopping") - def fetch_balance(): + def fetch_balance(self): walleturl = self.lnbits_url + "/api/v1/wallet" headers = { "X-Api-Key": self.lnbits_readkey, @@ -87,6 +109,39 @@ class LNBitsWallet(Wallet): print(f"Could not parse reponse text '{response_text}' as JSON: {e}") raise e + def fetch_payments(self): + paymentsurl = self.lnbits_url + "/api/v1/payments?limit=6" + headers = { + "X-Api-Key": self.lnbits_readkey, + } + try: + response = requests.get(paymentsurl, timeout=10, headers=headers) + except Exception as e: + print("fetch_payments: get request failed:", e) + if response and response.status_code == 200: + response_text = response.text + print(f"Got response text: {response_text}") + response.close() + try: + payments_reply = json.loads(response_text) + print(f"Got payments: {payments_reply}") + new_payments = [] + for payment in payments_reply: + print(f"Got payment: {payment}") + amount = payment["amount"] + amount = round(amount / 1000) + comment = payment["memo"] + extra = payment["extra"] + if extra: + extracomment = extra["comment"] + if extracomment: + comment = extracomment + payment = Payment(amount, comment) + new_payments.append(payment) + return new_payments + except Exception as e: + print(f"Could not parse reponse text '{response_text}' as JSON: {e}") + raise e class NWCWallet(Wallet): @@ -95,7 +150,7 @@ class NWCWallet(Wallet): self.nwc_url = nwc_url self.connected = False - def wallet_manager_thread(self, balance_updated_cb): + def wallet_manager_thread(self): self.relay, self.wallet_pubkey, self.secret, self.lud16 = self.parse_nwc_url(self.nwc_url) self.private_key = PrivateKey(bytes.fromhex(self.secret)) self.relay_manager = RelayManager() @@ -167,7 +222,7 @@ class NWCWallet(Wallet): self.last_known_balance = round(int(response["result"]["balance"]) / 1000) print(f"Got balance: {self.last_known_balance}") # TODO: if balance changed, then update list of transactions - balance_updated_cb() + self.balance_updated_cb() elif response["result"]["transactions"]: print("TODO: Response contains transactions!") else: @@ -197,7 +252,9 @@ class NWCWallet(Wallet): 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}") - # TODO: urldecode because the relay might have %3A%2F%2F etc + # urldecode because the relay might have %3A%2F%2F etc + nwc_url = mpos.util.urldecode(nwc_url) + print(f"after urldecode: {nwc_url}") # Split into pubkey and query params parts = nwc_url.split('?') pubkey = parts[0] @@ -234,3 +291,17 @@ class NWCWallet(Wallet): except Exception as e: print(f"DEBUG: Error parsing NWC URL: {e}") + +class Payment: + + def __init__(self, amount_sats, comment): + self.amount_sats = amount_sats + self.comment = comment + + def __str__(self): + return f"Payment(amount_sats={self.amount_sats}, comment='{self.comment}')" + + def __eq__(self, other): + if not isinstance(other, Payment): + return False + return self.amount_sats == other.amount_sats and self.comment == other.comment diff --git a/internal_filesystem/lib/mpos/util.py b/internal_filesystem/lib/mpos/util.py new file mode 100644 index 00000000..8fb8c03f --- /dev/null +++ b/internal_filesystem/lib/mpos/util.py @@ -0,0 +1,11 @@ +def urldecode(s): + result = "" + i = 0 + while i < len(s): + if s[i] == '%': + result += chr(int(s[i+1:i+3], 16)) + i += 3 + else: + result += s[i] + i += 1 + return result