wallet: add payment list

This commit is contained in:
Thomas Farstrike
2025-05-26 15:25:42 +02:00
parent 08bf9bb4e6
commit 1ba44918bd
6 changed files with 246 additions and 20 deletions
+64
View File
@@ -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())
+25
View File
@@ -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")
+46
View File
@@ -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")
@@ -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:
@@ -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
+11
View File
@@ -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