@@ -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