From be7d36214a7b8fce63ee86379b30316cefec5bdf Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 15 Oct 2020 00:18:56 -0300 Subject: [PATCH] use payments/sse on the core wallet UI. still fallback to the invoice polling (now with a 5 seconds interval because less than that is too annoying). this fixes issues with /lnurlwallet invoices not getting paid in time, so we update the UI automatically when they do get paid. (see https://t.me/lnbits/7069) --- lnbits/core/static/js/wallet.js | 66 +++++++++++++++++++-------------- lnbits/core/views/api.py | 8 ++-- lnbits/decorators.py | 5 ++- lnbits/static/js/base.js | 13 +++++++ 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index fbcf81c3..c0ce8033 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -116,6 +116,7 @@ new Vue({ show: false, status: 'pending', paymentReq: null, + paymentHash: null, minMax: [0, 2100000000000000], lnurl: null, data: { @@ -225,6 +226,7 @@ new Vue({ this.receive.show = true this.receive.status = 'pending' this.receive.paymentReq = null + this.receive.paymentHash = null this.receive.data.amount = null this.receive.data.memo = null this.receive.paymentChecker = null @@ -241,17 +243,25 @@ new Vue({ this.parse.camera.show = false }, closeReceiveDialog: function () { - var checker = this.receive.paymentChecker setTimeout(() => { - clearInterval(checker) + clearInterval(this.receive.paymentChecker) }, 10000) }, closeParseDialog: function () { - var checker = this.parse.paymentChecker setTimeout(() => { - clearInterval(checker) + clearInterval(this.parse.paymentChecker) }, 10000) }, + onPaymentReceived: function (paymentHash) { + this.fetchPayments() + this.fetchBalance() + + if (this.receive.paymentHash === paymentHash) { + this.receive.show = false + this.receive.paymentHash = null + clearInterval(this.receive.paymentChecker) + } + }, createInvoice: function () { this.receive.status = 'loading' LNbits.api @@ -264,6 +274,7 @@ new Vue({ .then(response => { this.receive.status = 'success' this.receive.paymentReq = response.data.payment_request + this.receive.paymentHash = response.data.payment_hash if (response.data.lnurl_response !== null) { if (response.data.lnurl_response === false) { @@ -274,7 +285,7 @@ new Vue({ // failure this.$q.notify({ timeout: 5000, - type: 'negative', + type: 'warning', message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`, caption: response.data.lnurl_response }) @@ -283,7 +294,6 @@ new Vue({ // success this.$q.notify({ timeout: 5000, - type: 'positive', message: `Invoice sent to ${this.receive.lnurl.domain}!`, spinner: true }) @@ -291,17 +301,14 @@ new Vue({ } this.receive.paymentChecker = setInterval(() => { - LNbits.api - .getPayment(this.g.wallet, response.data.payment_hash) - .then(response => { - if (response.data.paid) { - this.fetchPayments() - this.fetchBalance() - this.receive.show = false - clearInterval(this.receive.paymentChecker) - } - }) - }, 2000) + let hash = response.data.payment_hash + + LNbits.api.getPayment(this.g.wallet, hash).then(response => { + if (response.data.paid) { + this.onPaymentReceived(hash) + } + }) + }, 5000) }) .catch(err => { LNbits.utils.notifyApiError(err) @@ -354,6 +361,7 @@ new Vue({ this.receive.show = true this.receive.status = 'pending' this.receive.paymentReq = null + this.receive.paymentHash = null this.receive.data.amount = data.maxWithdrawable / 1000 this.receive.data.memo = data.defaultDescription this.receive.minMax = [ @@ -475,7 +483,7 @@ new Vue({ message: `${response.data.success_action.url}`, caption: response.data.success_action.description, html: true, - type: 'info', + type: 'positive', timeout: 0, closeBtn: true }) @@ -483,7 +491,7 @@ new Vue({ case 'message': this.$q.notify({ message: response.data.success_action.message, - type: 'info', + type: 'positive', timeout: 0, closeBtn: true }) @@ -491,20 +499,18 @@ new Vue({ case 'aes': LNbits.api .getPayment(this.g.wallet, response.data.payment_hash) - .then( - ({data: payment}) => - console.log(payment) || - decryptLnurlPayAES( - response.data.success_action, - payment.preimage - ) + .then(({data: payment}) => + decryptLnurlPayAES( + response.data.success_action, + payment.preimage + ) ) .then(value => { this.$q.notify({ message: value, caption: response.data.success_action.description, html: true, - type: 'info', + type: 'positive', timeout: 0, closeBtn: true }) @@ -575,6 +581,7 @@ new Vue({ setTimeout(this.checkPendingPayments(), 1200) }, mounted: function () { + // show disclaimer if ( this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown') @@ -582,5 +589,10 @@ new Vue({ this.disclaimerDialog.show = true this.$q.localStorage.set('lnbits.disclaimerShown', true) } + + // listen to incoming payments + LNbits.events.onInvoicePaid(this.g.wallet, payment => + this.onPaymentReceived(payment.payment_hash) + ) } }) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 41e4865b..3ae78574 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -228,7 +228,7 @@ async def api_payment(payment_hash): @core_app.route("/api/v1/payments/sse", methods=["GET"]) -@api_check_wallet_key("invoice") +@api_check_wallet_key("invoice", accept_querystring=True) async def api_payments_sse(): g.db.close() this_wallet_id = g.wallet.id @@ -238,12 +238,12 @@ async def api_payments_sse(): print("adding sse listener", send_payment) sse_listeners.append(send_payment) - send_event, receive_event = trio.open_memory_channel(0) + send_event, event_to_send = trio.open_memory_channel(0) async def payment_received() -> None: async for payment in receive_payment: if payment.wallet_id == this_wallet_id: - await send_event.send(("payment", payment)) + await send_event.send(("payment-received", payment)) async def repeat_keepalive(): await trio.sleep(1) @@ -256,7 +256,7 @@ async def api_payments_sse(): async def send_events(): try: - async for typ, data in receive_event: + async for typ, data in event_to_send: message = [f"event: {typ}".encode("utf-8")] if data: diff --git a/lnbits/decorators.py b/lnbits/decorators.py index ac73e4e4..34d132e9 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -9,12 +9,13 @@ from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.settings import LNBITS_ALLOWED_USERS -def api_check_wallet_key(key_type: str = "invoice"): +def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False): def wrap(view): @wraps(view) async def wrapped_view(**kwargs): try: - g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type) + key_value = request.headers.get("X-Api-Key") or request.args["api-key"] + g.wallet = get_wallet_for_key(key_value, key_type) except KeyError: return ( jsonify({"message": "`X-Api-Key` header missing."}), diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 0cad91b9..f765c38b 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -63,6 +63,19 @@ window.LNbits = { ) } }, + events: { + onInvoicePaid: function (wallet, cb) { + if (!this.pis) { + this.pis = new EventSource( + '/api/v1/payments/sse?api-key=' + wallet.inkey + ) + } + + this.pis.addEventListener('payment-received', ev => + cb(JSON.parse(ev.data)) + ) + } + }, href: { createWallet: function (walletName, userId) { window.location.href =