Skip to content

Commit a3739e2

Browse files
Piggy wallet: add instant payments for LNBits and NWC (untested)
1 parent b1cd17c commit a3739e2

File tree

1 file changed

+97
-30
lines changed
  • internal_filesystem/apps/com.lightningpiggy.displaywallet/assets

1 file changed

+97
-30
lines changed

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

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ def __str__(self):
7474
elif isinstance(self, NWCWallet):
7575
return "NWCWallet"
7676

77-
def handle_new_balance(self, new_balance):
77+
def handle_new_balance(self, new_balance, fetchPaymentsIfChanged=True):
7878
if new_balance != self.last_known_balance:
7979
print("Balance changed!")
8080
self.last_known_balance = new_balance
8181
print("Calling balance_updated_cb")
8282
self.balance_updated_cb()
83-
# Refreshing isn't strictly necessary if it was changed by a payment notification
84-
print("Refreshing payments...")
85-
self.fetch_payments() # if the balance changed, then re-list transactions
83+
if fetchPaymentsIfChanged: # Fetching *all* payments isn't necessary if balance was changed by a payment notification
84+
print("Refreshing payments...")
85+
self.fetch_payments() # if the balance changed, then re-list transactions
8686

8787
def handle_new_payments(self, new_payments):
8888
print("handle_new_payments")
@@ -117,16 +117,71 @@ def __init__(self, lnbits_url, lnbits_readkey):
117117
self.lnbits_url = lnbits_url
118118
self.lnbits_readkey = lnbits_readkey
119119

120+
121+
def parseLNBitsPayment(self, transaction):
122+
amount = transaction["amount"]
123+
amount = round(amount / 1000)
124+
comment = transaction["memo"]
125+
epoch_time = transaction["time"]
126+
extra = transaction["extra"]
127+
if extra:
128+
extracomment = extra["comment"]
129+
if extracomment:
130+
if extracomment.get(0): # some LNBits 0.x versions return a list instead of a string here...
131+
comment = extracomment.get(0)
132+
else:
133+
comment = extracomment
134+
return Payment(epoch_time, amount, comment)
135+
136+
# Example data: {"wallet_balance": 4936, "payment": {"checking_id": "037c14...56b3", "pending": false, "amount": 1000000, "fee": 0, "memo": "zap2oink", "time": 1711226003, "bolt11": "lnbc10u1pjl70y....qq9renr", "preimage": "0000...000", "payment_hash": "037c1438b20ef4729b1d3dc252c2809dc2a2a2e641c7fb99fe4324e182f356b3", "expiry": 1711226603.0, "extra": {"tag": "lnurlp", "link": "TkjgaB", "extra": "1000000", "comment": ["yes"], "lnaddress": "oink@demo.lnpiggy.com"}, "wallet_id": "c9168...8de4", "webhook": null, "webhook_status": null}}
137+
def on_message(self, class_obj, message: str):
138+
print(f"relay.py _on_message received: {message}")
139+
try:
140+
payment_notification = json.loads(message)
141+
new_balance = payment_notification.get("wallet_balance")
142+
if new_balance:
143+
self.handle_new_balance(new_balance, False) # handle new balance BUT don't trigger a full fetch_payments
144+
transaction = payment_notification.get("payment")
145+
new_payments = UniqueSortedList()
146+
print(f"Got transaction: {transaction}")
147+
paymentObj = parseLNBitsPayment(transaction)
148+
new_payments.add(paymentObj)
149+
self.handle_new_payments(new_payments)
150+
except Exception as e:
151+
print(f"websocket on_message got exception: {e}")
152+
153+
def websocket_thread(self):
154+
print("Opening websocket for payment notifications...")
155+
wsurl = self.lnbits_url + "/api/v1/ws/" + self.lnbits_readkey
156+
self.ws = WebSocketApp(
157+
wsurl,
158+
on_message=self.on_message,
159+
) # maybe add other callbacks to reconnect when disconnected etc.
160+
self.ws.run_forever(
161+
sslopt=ssl_options,
162+
http_proxy_host=None if proxy is None else proxy.get("host"),
163+
http_proxy_port=None if proxy is None else proxy.get("port"),
164+
proxy_type=None if proxy is None else proxy.get("type"),
165+
ping_interval=5
166+
)
167+
168+
120169
def wallet_manager_thread(self):
121170
print("wallet_manager_thread")
171+
websocket_running = False
122172
while self.keep_running:
123173
try:
124-
new_balance = self.fetch_balance()
174+
new_balance = self.fetch_balance() # TODO: only do this every 60 seconds, but loop the main thread more frequently
125175
except Exception as e:
126176
print(f"WARNING: wallet_manager_thread got exception {e}, ignorning.")
177+
if not websocket_running: # after
178+
websocket_running = True
179+
_thread.stack_size(mpos.apps.good_stack_size())
180+
_thread.start_new_thread(self.websocket_thread, ())
127181
print("Sleeping a while before re-fetching balance...")
128182
time.sleep(60)
129183
print("wallet_manager_thread stopping")
184+
self.ws.close()
130185

131186
def fetch_balance(self):
132187
walleturl = self.lnbits_url + "/api/v1/wallet"
@@ -170,17 +225,8 @@ def fetch_payments(self):
170225
new_payments = UniqueSortedList()
171226
for transaction in payments_reply:
172227
#print(f"Got transaction: {transaction}")
173-
amount = transaction["amount"]
174-
amount = round(amount / 1000)
175-
comment = transaction["memo"]
176-
epoch_time = transaction["time"]
177-
extra = transaction["extra"]
178-
if extra:
179-
extracomment = extra["comment"]
180-
if extracomment:
181-
comment = extracomment
182-
transaction = Payment(epoch_time, amount, comment)
183-
new_payments.add(transaction)
228+
paymentObj = parseLNBitsPayment(transaction)
229+
new_payments.add(paymentObj)
184230
self.handle_new_payments(new_payments)
185231
except Exception as e:
186232
print(f"Could not parse reponse text '{response_text}' as JSON: {e}")
@@ -193,6 +239,21 @@ def __init__(self, nwc_url):
193239
self.nwc_url = nwc_url
194240
self.connected = False
195241

242+
def getCommentFromTransaction(self, transaction):
243+
comment = ""
244+
try:
245+
comment = transaction["description"]
246+
json_comment = json.loads(comment)
247+
for field in json_comment:
248+
if field[0] == "text/plain":
249+
comment = field[1]
250+
break
251+
else:
252+
print("text/plain field is missing from JSON description")
253+
except Exception as e:
254+
print(f"Info: could not parse comment as JSON, using as-is: {e}")
255+
return comment
256+
196257
def wallet_manager_thread(self):
197258
self.relay, self.wallet_pubkey, self.secret, self.lud16 = self.parse_nwc_url(self.nwc_url)
198259
self.private_key = PrivateKey(bytes.fromhex(self.secret))
@@ -217,7 +278,7 @@ def wallet_manager_thread(self):
217278
self.subscription_id = "micropython_nwc_" + str(round(time.time()))
218279
print(f"DEBUG: Setting up subscription with ID: {self.subscription_id}")
219280
self.filters = Filters([Filter(
220-
kinds=[23195], # NWC replies
281+
kinds=[23195, 23196], # NWC reponses and notifications
221282
authors=[self.wallet_pubkey],
222283
pubkey_refs=[self.private_key.public_key.hex()]
223284
)])
@@ -258,22 +319,28 @@ def wallet_manager_thread(self):
258319
for transaction in result["transactions"]:
259320
amount = transaction["amount"]
260321
amount = round(amount / 1000)
261-
comment = transaction["description"]
262-
try:
263-
json_comment = json.loads(comment)
264-
for field in json_comment:
265-
if field[0] == "text/plain":
266-
comment = field[1]
267-
break
268-
else:
269-
print("text/plain field is missing from JSON description")
270-
except Exception as e:
271-
print(f"Could not parse comment as JSON: {e}")
272-
pass # NWC description can also be just a regular sting, not json
322+
comment = self.getCommentFromTransaction(transaction)
273323
epoch_time = transaction["created_at"]
274324
payment = Payment(epoch_time, amount, comment)
275325
new_payments.add(payment)
276326
self.handle_new_payments(new_payments)
327+
elif result.get("notification"): # it's a notification
328+
notification = result.get("notification")
329+
amount = notification["amount"]
330+
amount = round(amount / 1000)
331+
type = notification["type"]
332+
if type == "outgoing":
333+
amount = -amount
334+
elif type != "incoming":
335+
print(f"WARNING: invalid notification type {type}, ignoring.")
336+
continue
337+
self.handle_new_balance(new_balance, False)
338+
epoch_time = transaction["created_at"]
339+
comment = self.getCommentFromTransaction(notification)
340+
payment = Payment(epoch_time, amount, comment)
341+
new_payments = UniqueSortedList()
342+
new_payments.add(payment)
343+
self.handle_new_payments(new_payments)
277344
else:
278345
print("Unsupported response, ignoring.")
279346
else:
@@ -383,7 +450,7 @@ def __init__(self, epoch_time, amount_sats, comment):
383450

384451
def __str__(self):
385452
sattext = "sats"
386-
if self.amount_sats is 1:
453+
if self.amount_sats == 1:
387454
sattext = "sat"
388455
return f"{self.amount_sats} {sattext}: {self.comment}"
389456

0 commit comments

Comments
 (0)