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)
This commit is contained in:
fiatjaf
2020-10-15 00:18:56 -03:00
parent f02ec67f35
commit be7d36214a
4 changed files with 59 additions and 33 deletions

View File

@@ -116,6 +116,7 @@ new Vue({
show: false, show: false,
status: 'pending', status: 'pending',
paymentReq: null, paymentReq: null,
paymentHash: null,
minMax: [0, 2100000000000000], minMax: [0, 2100000000000000],
lnurl: null, lnurl: null,
data: { data: {
@@ -225,6 +226,7 @@ new Vue({
this.receive.show = true this.receive.show = true
this.receive.status = 'pending' this.receive.status = 'pending'
this.receive.paymentReq = null this.receive.paymentReq = null
this.receive.paymentHash = null
this.receive.data.amount = null this.receive.data.amount = null
this.receive.data.memo = null this.receive.data.memo = null
this.receive.paymentChecker = null this.receive.paymentChecker = null
@@ -241,17 +243,25 @@ new Vue({
this.parse.camera.show = false this.parse.camera.show = false
}, },
closeReceiveDialog: function () { closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
setTimeout(() => { setTimeout(() => {
clearInterval(checker) clearInterval(this.receive.paymentChecker)
}, 10000) }, 10000)
}, },
closeParseDialog: function () { closeParseDialog: function () {
var checker = this.parse.paymentChecker
setTimeout(() => { setTimeout(() => {
clearInterval(checker) clearInterval(this.parse.paymentChecker)
}, 10000) }, 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 () { createInvoice: function () {
this.receive.status = 'loading' this.receive.status = 'loading'
LNbits.api LNbits.api
@@ -264,6 +274,7 @@ new Vue({
.then(response => { .then(response => {
this.receive.status = 'success' this.receive.status = 'success'
this.receive.paymentReq = response.data.payment_request 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 !== null) {
if (response.data.lnurl_response === false) { if (response.data.lnurl_response === false) {
@@ -274,7 +285,7 @@ new Vue({
// failure // failure
this.$q.notify({ this.$q.notify({
timeout: 5000, timeout: 5000,
type: 'negative', type: 'warning',
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`, message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
caption: response.data.lnurl_response caption: response.data.lnurl_response
}) })
@@ -283,7 +294,6 @@ new Vue({
// success // success
this.$q.notify({ this.$q.notify({
timeout: 5000, timeout: 5000,
type: 'positive',
message: `Invoice sent to ${this.receive.lnurl.domain}!`, message: `Invoice sent to ${this.receive.lnurl.domain}!`,
spinner: true spinner: true
}) })
@@ -291,17 +301,14 @@ new Vue({
} }
this.receive.paymentChecker = setInterval(() => { this.receive.paymentChecker = setInterval(() => {
LNbits.api let hash = response.data.payment_hash
.getPayment(this.g.wallet, response.data.payment_hash)
.then(response => { LNbits.api.getPayment(this.g.wallet, hash).then(response => {
if (response.data.paid) { if (response.data.paid) {
this.fetchPayments() this.onPaymentReceived(hash)
this.fetchBalance() }
this.receive.show = false })
clearInterval(this.receive.paymentChecker) }, 5000)
}
})
}, 2000)
}) })
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
@@ -354,6 +361,7 @@ new Vue({
this.receive.show = true this.receive.show = true
this.receive.status = 'pending' this.receive.status = 'pending'
this.receive.paymentReq = null this.receive.paymentReq = null
this.receive.paymentHash = null
this.receive.data.amount = data.maxWithdrawable / 1000 this.receive.data.amount = data.maxWithdrawable / 1000
this.receive.data.memo = data.defaultDescription this.receive.data.memo = data.defaultDescription
this.receive.minMax = [ this.receive.minMax = [
@@ -475,7 +483,7 @@ new Vue({
message: `<a target="_blank" style="color: inherit" href="${response.data.success_action.url}">${response.data.success_action.url}</a>`, message: `<a target="_blank" style="color: inherit" href="${response.data.success_action.url}">${response.data.success_action.url}</a>`,
caption: response.data.success_action.description, caption: response.data.success_action.description,
html: true, html: true,
type: 'info', type: 'positive',
timeout: 0, timeout: 0,
closeBtn: true closeBtn: true
}) })
@@ -483,7 +491,7 @@ new Vue({
case 'message': case 'message':
this.$q.notify({ this.$q.notify({
message: response.data.success_action.message, message: response.data.success_action.message,
type: 'info', type: 'positive',
timeout: 0, timeout: 0,
closeBtn: true closeBtn: true
}) })
@@ -491,20 +499,18 @@ new Vue({
case 'aes': case 'aes':
LNbits.api LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash) .getPayment(this.g.wallet, response.data.payment_hash)
.then( .then(({data: payment}) =>
({data: payment}) => decryptLnurlPayAES(
console.log(payment) || response.data.success_action,
decryptLnurlPayAES( payment.preimage
response.data.success_action, )
payment.preimage
)
) )
.then(value => { .then(value => {
this.$q.notify({ this.$q.notify({
message: value, message: value,
caption: response.data.success_action.description, caption: response.data.success_action.description,
html: true, html: true,
type: 'info', type: 'positive',
timeout: 0, timeout: 0,
closeBtn: true closeBtn: true
}) })
@@ -575,6 +581,7 @@ new Vue({
setTimeout(this.checkPendingPayments(), 1200) setTimeout(this.checkPendingPayments(), 1200)
}, },
mounted: function () { mounted: function () {
// show disclaimer
if ( if (
this.$refs.disclaimer && this.$refs.disclaimer &&
!this.$q.localStorage.getItem('lnbits.disclaimerShown') !this.$q.localStorage.getItem('lnbits.disclaimerShown')
@@ -582,5 +589,10 @@ new Vue({
this.disclaimerDialog.show = true this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true) this.$q.localStorage.set('lnbits.disclaimerShown', true)
} }
// listen to incoming payments
LNbits.events.onInvoicePaid(this.g.wallet, payment =>
this.onPaymentReceived(payment.payment_hash)
)
} }
}) })

View File

@@ -228,7 +228,7 @@ async def api_payment(payment_hash):
@core_app.route("/api/v1/payments/sse", methods=["GET"]) @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(): async def api_payments_sse():
g.db.close() g.db.close()
this_wallet_id = g.wallet.id this_wallet_id = g.wallet.id
@@ -238,12 +238,12 @@ async def api_payments_sse():
print("adding sse listener", send_payment) print("adding sse listener", send_payment)
sse_listeners.append(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 def payment_received() -> None:
async for payment in receive_payment: async for payment in receive_payment:
if payment.wallet_id == this_wallet_id: if payment.wallet_id == this_wallet_id:
await send_event.send(("payment", payment)) await send_event.send(("payment-received", payment))
async def repeat_keepalive(): async def repeat_keepalive():
await trio.sleep(1) await trio.sleep(1)
@@ -256,7 +256,7 @@ async def api_payments_sse():
async def send_events(): async def send_events():
try: try:
async for typ, data in receive_event: async for typ, data in event_to_send:
message = [f"event: {typ}".encode("utf-8")] message = [f"event: {typ}".encode("utf-8")]
if data: if data:

View File

@@ -9,12 +9,13 @@ from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.settings import LNBITS_ALLOWED_USERS 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): def wrap(view):
@wraps(view) @wraps(view)
async def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
try: 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: except KeyError:
return ( return (
jsonify({"message": "`X-Api-Key` header missing."}), jsonify({"message": "`X-Api-Key` header missing."}),

View File

@@ -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: { href: {
createWallet: function (walletName, userId) { createWallet: function (walletName, userId) {
window.location.href = window.location.href =