You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
wallet: add payment list
This commit is contained in:
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user