Skip to content

Commit 1ba4491

Browse files
wallet: add payment list
1 parent 08bf9bb commit 1ba4491

File tree

6 files changed

+246
-20
lines changed

6 files changed

+246
-20
lines changed

draft_code/queue_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import uasyncio
2+
from collections import deque
3+
4+
class AsyncQueue:
5+
def __init__(self, maxlen=10): # Set a default maximum length
6+
self._queue = deque((), maxlen, True) # Initialize deque with specified maxlen
7+
self._event = uasyncio.Event() # Event for signaling when items are added
8+
9+
async def get(self, timeout=None):
10+
"""Get an item from the queue, waiting if empty until an item is available or timeout expires."""
11+
while not self._queue:
12+
if timeout is not None:
13+
try:
14+
await uasyncio.wait_for(self._event.wait(), timeout) # Wait for item or timeout
15+
except uasyncio.TimeoutError:
16+
raise Empty("Queue is empty and timed out")
17+
else:
18+
await self._event.wait() # Wait indefinitely for an item
19+
self._event.clear() # Clear event after waking up
20+
return self._queue.popleft() # Return the item
21+
22+
async def put(self, item):
23+
"""Put an item in the queue and signal waiting coroutines."""
24+
self._queue.append(item) # This will now work with proper maxlen
25+
self._event.set() # Signal that an item is available
26+
27+
def qsize(self):
28+
"""Return the current size of the queue."""
29+
return len(self._queue)
30+
31+
def empty(self):
32+
"""Return True if the queue is empty."""
33+
return len(self._queue) == 0
34+
35+
class Empty(Exception):
36+
"""Exception raised when queue is empty and non-blocking or timeout occurs."""
37+
pass
38+
39+
40+
import uasyncio
41+
#from async_queue import AsyncQueue, Empty # Assuming the above code is in async_queue.py
42+
43+
async def producer(queue):
44+
for i in range(5):
45+
print(f"Producing {i}")
46+
await queue.put(i)
47+
await uasyncio.sleep(1) # Simulate some delay
48+
49+
async def consumer(queue):
50+
while True:
51+
try:
52+
item = await queue.get(timeout=2.0) # Wait up to 2 seconds
53+
print(f"Consumed {item}")
54+
except Empty:
55+
print("Consumer timed out waiting for item")
56+
break
57+
58+
async def main():
59+
queue = AsyncQueue()
60+
# Run producer and consumer concurrently in the event loop
61+
await uasyncio.gather(producer(queue), consumer(queue))
62+
63+
# Run the event loop
64+
uasyncio.run(main())

draft_code/test_aes_cbc.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ucryptolib
2+
import os
3+
4+
key = os.urandom(32)
5+
iv = bytes.fromhex("cafc34a94307c35f8c8f736831713467")
6+
#iv = bytes.fromhex("cafc34a94307c35f8c8f736831713468") # changing the IV doesn't change the output!
7+
#iv = os.urandom(16)
8+
data = b'{"method":"get_balance","params":{}}'
9+
pad_length = 16 - (len(data) % 16)
10+
padded_data = data + bytes([pad_length] * pad_length)
11+
print(f"Test padded_data: {padded_data.hex()}")
12+
13+
cipher = ucryptolib.aes(key, 1, iv)
14+
ciphertext = cipher.encrypt(padded_data)
15+
print(f"Test ciphertext: {ciphertext.hex()}")
16+
17+
cipher = ucryptolib.aes(key, 1, iv)
18+
decrypted = cipher.decrypt(ciphertext)
19+
print(f"Test decrypted: {decrypted.hex()}")
20+
21+
pad_length = decrypted[-1]
22+
if decrypted[-pad_length:] != bytes([pad_length] * pad_length):
23+
print(f"Test failed: invalid padding, got {decrypted[-pad_length:].hex()}")
24+
else:
25+
print(f"Test passed: valid padding")

draft_code/test_aes_cbc_iv.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import ucryptolib
2+
import os
3+
import sys
4+
5+
print(f"MicroPython version: {sys.version}")
6+
print(f"Platform: {sys.platform}")
7+
8+
key = os.urandom(32)
9+
iv1 = bytes.fromhex("cafc34a94307c35f8c8f736831713467")
10+
iv2 = bytes.fromhex("cafc34a94307c35f8c8f736831713468")
11+
iv3 = os.urandom(16) # Random IV
12+
data = b'{"method":"get_balance","params":{}}'
13+
pad_length = 16 - (len(data) % 16)
14+
padded_data = data + bytes([pad_length] * pad_length)
15+
print(f"Test key: {key.hex()}")
16+
print(f"Test padded_data: {padded_data.hex()} (length: {len(padded_data)})")
17+
18+
mode_cbc = 2
19+
20+
# Test with IV1
21+
cipher1 = ucryptolib.aes(key, mode_cbc, iv1)
22+
ciphertext1 = cipher1.encrypt(padded_data)
23+
print(f"IV1: {iv1.hex()}, Ciphertext1: {ciphertext1.hex()}")
24+
25+
# Test with IV2
26+
cipher2 = ucryptolib.aes(key, mode_cbc, iv2)
27+
ciphertext2 = cipher2.encrypt(padded_data)
28+
print(f"IV2: {iv2.hex()}, Ciphertext2: {ciphertext2.hex()}")
29+
30+
# Test with IV3
31+
cipher3 = ucryptolib.aes(key, mode_cbc, iv3)
32+
ciphertext3 = cipher3.encrypt(padded_data)
33+
print(f"IV3: {iv3.hex()}, Ciphertext3: {ciphertext3.hex()}")
34+
35+
# Compare ciphertexts
36+
print(f"Ciphertext1 == Ciphertext2: {ciphertext1 == ciphertext2}")
37+
print(f"Ciphertext1 == Ciphertext3: {ciphertext1 == ciphertext3}")
38+
39+
# Verify decryption
40+
cipher_decrypt = ucryptolib.aes(key, 1, iv1)
41+
decrypted = cipher_decrypt.decrypt(ciphertext1)
42+
print(f"Decrypted with IV1: {decrypted.hex()}")
43+
if decrypted[-pad_length:] != bytes([pad_length] * pad_length):
44+
print(f"Test failed: invalid padding, got {decrypted[-pad_length:].hex()}")
45+
else:
46+
print(f"Test passed: valid padding")

internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
# widgets
1313
balance_label = None
14+
payments_label = None
1415

1516
wallet = None
1617

@@ -191,21 +192,25 @@ def settings_button_tap(event):
191192
mpos.ui.load_screen(settings_screen)
192193

193194
def build_main_ui():
194-
global main_screen, balance_label
195+
global main_screen, balance_label, payments_label
195196
main_screen = lv.obj()
196197
main_screen.set_style_pad_all(10, 0)
197198
balance_label = lv.label(main_screen)
198199
balance_label.align(lv.ALIGN.TOP_LEFT, 0, 0)
199-
balance_label.set_style_text_font(lv.font_montserrat_20, 0)
200-
balance_label.set_text('123456')
200+
balance_label.set_style_text_font(lv.font_montserrat_22, 0)
201+
balance_label.set_text(lv.SYMBOL.REFRESH)
201202
style_line = lv.style_t()
202203
style_line.init()
203-
style_line.set_line_width(4)
204+
style_line.set_line_width(2)
204205
style_line.set_line_color(lv.palette_main(lv.PALETTE.PINK))
205206
style_line.set_line_rounded(True)
206207
balance_line = lv.line(main_screen)
207208
balance_line.set_points([{'x':0,'y':35},{'x':300,'y':35}],2)
208209
balance_line.add_style(style_line, 0)
210+
payments_label = lv.label(main_screen)
211+
payments_label.align_to(balance_line,lv.ALIGN.OUT_BOTTOM_LEFT,0,10)
212+
payments_label.set_style_text_font(lv.font_montserrat_16, 0)
213+
payments_label.set_text(lv.SYMBOL.REFRESH)
209214
settings_button = lv.button(main_screen)
210215
settings_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0)
211216
snap_label = lv.label(settings_button)
@@ -215,10 +220,14 @@ def build_main_ui():
215220
mpos.ui.load_screen(main_screen)
216221

217222

218-
def redraw_balance_cb(timer):
223+
def redraw_balance_cb():
219224
global balance_label
220-
if balance_label.get_text() != str(wallet.last_known_balance):
221-
balance_label.set_text(str(wallet.last_known_balance))
225+
balance_label.set_text(str(wallet.last_known_balance))
226+
227+
def redraw_payments_cb():
228+
global payments_label
229+
print("redrawing payments")
230+
payments_label.set_text(wallet.payment_list_string())
222231

223232
def janitor_cb(timer):
224233
global wallet, config
@@ -239,7 +248,7 @@ def janitor_cb(timer):
239248
else:
240249
print(f"No or unsupported wallet type configured: '{wallet_type}'")
241250
if wallet:
242-
wallet.start(lambda : balance_label.set_text(str(wallet.last_known_balance)))
251+
wallet.start(redraw_balance_cb, redraw_payments_cb)
243252
else:
244253
print("ERROR: could not start any wallet!") # maybe call the error callback to show the error to the user
245254
elif lv.screen_active() != main_screen and lv.screen_active() != settings_screen:

internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/wallet.py

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212

1313
import mpos.apps
1414
import mpos.time
15+
import mpos.util
1516

1617
class Wallet:
1718

1819
# These values could be loading from a cache.json file at __init__
19-
last_known_balance = 0
20+
last_known_balance = -1
2021
#last_known_balance_timestamp = 0
22+
payment_list = []
2123

2224
def __init__(self):
2325
self.keep_running = True
@@ -28,16 +30,33 @@ def __str__(self):
2830
elif isinstance(self, NWCWallet):
2931
return "NWCWallet"
3032

33+
def are_payment_lists_equal(self, list1, list2):
34+
if len(list1) != len(list2):
35+
return False
36+
return all(p1 == p2 for p1, p2 in zip(list1, list2))
37+
38+
def handle_new_payments(self, new_payments):
39+
print("handle_new_payments")
40+
if not self.are_payment_lists_equal(self.payment_list, new_payments):
41+
print("new list of payments")
42+
self.payment_list = new_payments
43+
self.payments_updated_cb()
44+
45+
def payment_list_string(self):
46+
return "\n".join(f"{payment.amount_sats} sats: {payment.comment}" for payment in self.payment_list)
47+
3148
# Need callbacks for:
3249
# - started (so the user can show the UI)
3350
# - stopped (so the user can delete/free it)
3451
# - error (so the user can show the error)
3552
# - balance
3653
# - transactions
37-
def start(self, balance_updated_cb):
54+
def start(self, balance_updated_cb, payments_updated_cb):
3855
self.keep_running = True
56+
self.balance_updated_cb = balance_updated_cb
57+
self.payments_updated_cb = payments_updated_cb
3958
_thread.stack_size(mpos.apps.good_stack_size())
40-
_thread.start_new_thread(self.wallet_manager_thread, (balance_updated_cb,))
59+
_thread.start_new_thread(self.wallet_manager_thread, ())
4160

4261
def stop(self):
4362
self.keep_running = False
@@ -52,20 +71,23 @@ def __init__(self, lnbits_url, lnbits_readkey):
5271
self.lnbits_url = lnbits_url
5372
self.lnbits_readkey = lnbits_readkey
5473

55-
def wallet_manager_thread(self, balance_updated_cb):
74+
def wallet_manager_thread(self):
5675
print("wallet_manager_thread")
5776
while self.keep_running:
5877
try:
59-
self.last_known_balance = fetch_balance()
60-
balance_updated_cb()
61-
# TODO: if the balance changed, then re-list transactions
78+
new_balance = self.fetch_balance()
79+
if new_balance != self.last_known_balance:
80+
self.last_known_balance = new_balance
81+
self.balance_updated_cb()
82+
new_payments = self.fetch_payments() # if the balance changed, then re-list transactions
83+
self.handle_new_payments(new_payments)
6284
except Exception as e:
63-
print(f"WARNING: fetch_balance got exception {e}, ignorning.")
85+
print(f"WARNING: wallet_manager_thread got exception {e}, ignorning.")
6486
print("Sleeping a while before re-fetching balance...")
6587
time.sleep(60)
6688
print("wallet_manager_thread stopping")
6789

68-
def fetch_balance():
90+
def fetch_balance(self):
6991
walleturl = self.lnbits_url + "/api/v1/wallet"
7092
headers = {
7193
"X-Api-Key": self.lnbits_readkey,
@@ -87,6 +109,39 @@ def fetch_balance():
87109
print(f"Could not parse reponse text '{response_text}' as JSON: {e}")
88110
raise e
89111

112+
def fetch_payments(self):
113+
paymentsurl = self.lnbits_url + "/api/v1/payments?limit=6"
114+
headers = {
115+
"X-Api-Key": self.lnbits_readkey,
116+
}
117+
try:
118+
response = requests.get(paymentsurl, timeout=10, headers=headers)
119+
except Exception as e:
120+
print("fetch_payments: get request failed:", e)
121+
if response and response.status_code == 200:
122+
response_text = response.text
123+
print(f"Got response text: {response_text}")
124+
response.close()
125+
try:
126+
payments_reply = json.loads(response_text)
127+
print(f"Got payments: {payments_reply}")
128+
new_payments = []
129+
for payment in payments_reply:
130+
print(f"Got payment: {payment}")
131+
amount = payment["amount"]
132+
amount = round(amount / 1000)
133+
comment = payment["memo"]
134+
extra = payment["extra"]
135+
if extra:
136+
extracomment = extra["comment"]
137+
if extracomment:
138+
comment = extracomment
139+
payment = Payment(amount, comment)
140+
new_payments.append(payment)
141+
return new_payments
142+
except Exception as e:
143+
print(f"Could not parse reponse text '{response_text}' as JSON: {e}")
144+
raise e
90145

91146
class NWCWallet(Wallet):
92147

@@ -95,7 +150,7 @@ def __init__(self, nwc_url):
95150
self.nwc_url = nwc_url
96151
self.connected = False
97152

98-
def wallet_manager_thread(self, balance_updated_cb):
153+
def wallet_manager_thread(self):
99154
self.relay, self.wallet_pubkey, self.secret, self.lud16 = self.parse_nwc_url(self.nwc_url)
100155
self.private_key = PrivateKey(bytes.fromhex(self.secret))
101156
self.relay_manager = RelayManager()
@@ -167,7 +222,7 @@ def wallet_manager_thread(self, balance_updated_cb):
167222
self.last_known_balance = round(int(response["result"]["balance"]) / 1000)
168223
print(f"Got balance: {self.last_known_balance}")
169224
# TODO: if balance changed, then update list of transactions
170-
balance_updated_cb()
225+
self.balance_updated_cb()
171226
elif response["result"]["transactions"]:
172227
print("TODO: Response contains transactions!")
173228
else:
@@ -197,7 +252,9 @@ def parse_nwc_url(self, nwc_url):
197252
print(f"DEBUG: No recognized prefix found in URL")
198253
raise ValueError("Invalid NWC URL: missing 'nostr+walletconnect://' or 'nwc:' prefix")
199254
print(f"DEBUG: URL after prefix removal: {nwc_url}")
200-
# TODO: urldecode because the relay might have %3A%2F%2F etc
255+
# urldecode because the relay might have %3A%2F%2F etc
256+
nwc_url = mpos.util.urldecode(nwc_url)
257+
print(f"after urldecode: {nwc_url}")
201258
# Split into pubkey and query params
202259
parts = nwc_url.split('?')
203260
pubkey = parts[0]
@@ -234,3 +291,17 @@ def parse_nwc_url(self, nwc_url):
234291
except Exception as e:
235292
print(f"DEBUG: Error parsing NWC URL: {e}")
236293

294+
295+
class Payment:
296+
297+
def __init__(self, amount_sats, comment):
298+
self.amount_sats = amount_sats
299+
self.comment = comment
300+
301+
def __str__(self):
302+
return f"Payment(amount_sats={self.amount_sats}, comment='{self.comment}')"
303+
304+
def __eq__(self, other):
305+
if not isinstance(other, Payment):
306+
return False
307+
return self.amount_sats == other.amount_sats and self.comment == other.comment
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
def urldecode(s):
2+
result = ""
3+
i = 0
4+
while i < len(s):
5+
if s[i] == '%':
6+
result += chr(int(s[i+1:i+3], 16))
7+
i += 3
8+
else:
9+
result += s[i]
10+
i += 1
11+
return result

0 commit comments

Comments
 (0)