diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 21f058dd2..661d2e24a 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -1,4 +1,4 @@ -/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */ +/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart, decryptLnurlPayAES */ Vue.component(VueQrcode.name, VueQrcode) Vue.use(VueQrcodeReader) @@ -248,7 +248,7 @@ new Vue({ var checker = this.parse.paymentChecker setTimeout(() => { clearInterval(checker) - }, 1000) + }, 10000) }, createInvoice: function () { this.receive.status = 'loading' @@ -469,7 +469,7 @@ new Vue({ switch (response.data.success_action.tag) { case 'url': this.$q.notify({ - message: `${response.data.success_action.url}`, + message: `${response.data.success_action.url}`, caption: response.data.success_action.description, html: true, type: 'info', @@ -486,6 +486,26 @@ new Vue({ }) break 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(value => { + this.$q.notify({ + message: value, + caption: response.data.success_action.description, + html: true, + type: 'info', + timeout: 0, + closeBtn: true + }) + }) break } } diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 1e292dc33..90526d747 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -211,14 +211,14 @@ async def api_payment(payment_hash): if not payment: return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND elif not payment.pending: - return jsonify({"paid": True}), HTTPStatus.OK + return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK try: payment.check_pending() except Exception: return jsonify({"paid": False}), HTTPStatus.OK - return jsonify({"paid": not payment.pending}), HTTPStatus.OK + return jsonify({"paid": not payment.pending, "preimage": payment.preimage}), HTTPStatus.OK @core_app.route("/api/v1/payments/sse", methods=["GET"]) diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 9f1bb283f..c1bdbb9ff 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -1,10 +1,8 @@ -/* globals moment, Vue, EventHub, axios, Quasar, _ */ +/* globals crypto, moment, Vue, axios, Quasar, _ */ -var LOCALE = 'en' - -var EventHub = new Vue() - -var LNbits = { +window.LOCALE = 'en' +window.EventHub = new Vue() +window.LNbits = { api: { request: function (method, url, apiKey, data) { return axios({ @@ -106,7 +104,7 @@ var LNbits = { ) obj.msat = obj.balance obj.sat = Math.round(obj.balance / 1000) - obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat) + obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat) obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('') return obj }, @@ -134,7 +132,7 @@ var LNbits = { obj.msat = obj.amount obj.sat = obj.msat / 1000 obj.tag = obj.extra.tag - obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat) + obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat) obj.isIn = obj.amount > 0 obj.isOut = obj.amount < 0 obj.isPaid = obj.pending === 0 @@ -157,13 +155,13 @@ var LNbits = { }) }, formatCurrency: function (value, currency) { - return new Intl.NumberFormat(LOCALE, { + return new Intl.NumberFormat(window.LOCALE, { style: 'currency', currency: currency }).format(value) }, formatSat: function (value) { - return new Intl.NumberFormat(LOCALE).format(value) + return new Intl.NumberFormat(window.LOCALE).format(value) }, notifyApiError: function (error) { var types = { @@ -246,7 +244,7 @@ var LNbits = { } } -var windowMixin = { +window.windowMixin = { data: function () { return { g: { @@ -276,17 +274,17 @@ var windowMixin = { created: function () { this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode')) if (window.user) { - this.g.user = Object.freeze(LNbits.map.user(window.user)) + this.g.user = Object.freeze(window.LNbits.map.user(window.user)) } if (window.wallet) { - this.g.wallet = Object.freeze(LNbits.map.wallet(window.wallet)) + this.g.wallet = Object.freeze(window.LNbits.map.wallet(window.wallet)) } if (window.extensions) { var user = this.g.user this.g.extensions = Object.freeze( window.extensions .map(function (data) { - return LNbits.map.extension(data) + return window.LNbits.map.extension(data) }) .map(function (obj) { if (user) { @@ -303,3 +301,27 @@ var windowMixin = { } } } + +window.decryptLnurlPayAES = function (success_action, preimage) { + let keyb = new Uint8Array( + preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)) + ) + + return crypto.subtle + .importKey('raw', keyb, {name: 'AES-CBC', length: 256}, false, ['decrypt']) + .then(key => { + let ivb = Uint8Array.from(window.atob(success_action.iv), c => + c.charCodeAt(0) + ) + let ciphertextb = Uint8Array.from( + window.atob(success_action.ciphertext), + c => c.charCodeAt(0) + ) + + return crypto.subtle.decrypt({name: 'AES-CBC', iv: ivb}, key, ciphertextb) + }) + .then(valueb => { + let decoder = new TextDecoder('utf-8') + return decoder.decode(valueb) + }) +} diff --git a/lnbits/static/js/components.js b/lnbits/static/js/components.js index 3d8c55474..0d37f520f 100644 --- a/lnbits/static/js/components.js +++ b/lnbits/static/js/components.js @@ -1,4 +1,4 @@ -/* global Vue, moment, LNbits, EventHub */ +/* global Vue, moment, LNbits, EventHub, decryptLnurlPayAES */ Vue.component('lnbits-fsat', { props: { @@ -199,10 +199,64 @@ Vue.component('lnbits-payment-details', {
Payment hash:
{{ payment.payment_hash }}
-
+
Payment proof:
{{ payment.preimage }}
+
+
Success action:
+
+ +
+
- ` + `, + computed: { + hasPreimage() { + return ( + this.payment.preimage && + this.payment.preimage !== + '0000000000000000000000000000000000000000000000000000000000000000' + ) + }, + hasSuccessAction() { + return ( + this.hasPreimage && + this.payment.extra && + this.payment.extra.success_action + ) + } + } +}) + +Vue.component('lnbits-lnurlpay-success-action', { + props: ['payment', 'success_action'], + data() { + return { + decryptedValue: this.success_action.ciphertext + } + }, + template: ` +
+

{{ success_action.message || success_action.description }}

+ + {{ decryptedValue }} + +

+ {{ success_action.url }} +

+
+ `, + mounted: function () { + if (this.success_action.tag !== 'aes') return null + + decryptLnurlPayAES(this.success_action, this.payment.preimage).then( + value => { + this.decryptedValue = value + } + ) + } }) diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 8b7daf5ea..65629f754 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -34,7 +34,7 @@ class LNPayWallet(Wallet): f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}", 0 ) - return StatusResponse(None, data["balance"] / 1000) + return StatusResponse(None, data["balance"] * 1000) def create_invoice( self, diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index bce53cd41..d9f3c4f35 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -29,6 +29,8 @@ class SparkWallet(Wallet): params = args elif kwargs: params = kwargs + else: + params = {} r = httpx.post(self.url + "/rpc", headers={"X-Access": self.token}, json={"method": key, "params": params}) try: