Browse Source

send/create/scan buttons for clear LNURL support.

atmext
fiatjaf 4 years ago
parent
commit
7a5159f293
  1. 50
      lnbits/core/static/js/wallet.js
  2. 134
      lnbits/core/templates/core/wallet.html
  3. 40
      lnbits/core/views/api.py
  4. 4
      lnbits/extensions/lnurlp/templates/lnurlp/display.html

50
lnbits/core/static/js/wallet.js

@ -1,4 +1,4 @@
/* globals moment, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */ /* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader) Vue.use(VueQrcodeReader)
@ -132,8 +132,9 @@ new Vue({
send: { send: {
show: false, show: false,
invoice: null, invoice: null,
lnurl: {},
data: { data: {
bolt11: '' request: ''
} }
}, },
theCamera: { theCamera: {
@ -206,12 +207,6 @@ new Vue({
} }
}, },
methods: { methods: {
// closeCamera: function () {
// this.sendCamera.show = false
// },
// showCamera: function () {
// this.sendCamera.show = true
// },
closeCamera: function () { closeCamera: function () {
this.theCamera.show = false this.theCamera.show = false
}, },
@ -240,8 +235,9 @@ new Vue({
this.send = { this.send = {
show: true, show: true,
invoice: null, invoice: null,
lnurl: {},
data: { data: {
bolt11: '' request: ''
}, },
paymentChecker: null paymentChecker: null
} }
@ -253,7 +249,6 @@ new Vue({
}, 10000) }, 10000)
}, },
closeSendDialog: function () { closeSendDialog: function () {
// this.sendCamera.show = false
var checker = this.send.paymentChecker var checker = this.send.paymentChecker
setTimeout(function () { setTimeout(function () {
clearInterval(checker) clearInterval(checker)
@ -290,29 +285,32 @@ new Vue({
}) })
}, },
decodeQR: function (res) { decodeQR: function (res) {
if (res.substring(0, 4) == 'lnurl') { this.send.data.request = res
console.log(res) this.decodeRequest()
var self = this this.sendCamera.show = false
},
decodeRequest: function () {
if (this.send.data.request.startsWith('lightning:')) {
this.send.data.request = this.send.data.request.slice(10)
}
if (this.send.data.request.startsWith('lnurl:')) {
this.send.data.request = this.send.data.request.slice(6)
}
if (this.send.data.request.toLowerCase().startsWith('lnurl1')) {
LNbits.api LNbits.api
.request('GET', '/lnurlscan/' + res, this.g.user.wallets[0].adminkey) .request(
'GET',
'/api/v1/lnurlscan/' + this.send.data.request,
this.g.user.wallets[0].adminkey
)
.then(function (response) { .then(function (response) {
console.log(response.data) this.send.lnurl[response.kind] = Object.freeze(response)
}) })
.catch(function (error) { .catch(function (error) {
clearInterval(self.checker)
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
} else { return
this.send.data.bolt11 = res
this.decodeInvoice()
this.theCamera.show = false
}
},
decodeInvoice: function () {
if (this.send.data.bolt11.startsWith('lightning:')) {
this.send.data.bolt11 = this.send.data.bolt11.slice(10)
} }
let invoice let invoice

134
lnbits/core/templates/core/wallet.html

@ -17,7 +17,7 @@
color="deep-purple" color="deep-purple"
class="full-width" class="full-width"
@click="showSendDialog" @click="showSendDialog"
>Send</q-btn >Paste Request</q-btn
> >
</div> </div>
<div class="col"> <div class="col">
@ -26,7 +26,7 @@
color="deep-purple" color="deep-purple"
class="full-width" class="full-width"
@click="showReceiveDialog" @click="showReceiveDialog"
>Receive</q-btn >Create Invoice</q-btn
> >
</div> </div>
<div class="col"> <div class="col">
@ -141,7 +141,9 @@
<div v-if="props.row.isIn && props.row.pending"> <div v-if="props.row.isIn && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon> <q-icon name="settings_ethernet" color="grey"></q-icon>
Invoice waiting to be paid Invoice waiting to be paid
<lnbits-payment-details :payment="props.row"></lnbits-payment-details> <lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
<div v-if="props.row.bolt11" class="text-center q-mb-lg"> <div v-if="props.row.bolt11" class="text-center q-mb-lg">
<a :href="'lightning:' + props.row.bolt11"> <a :href="'lightning:' + props.row.bolt11">
<q-responsive :ratio="1" class="q-mx-xl"> <q-responsive :ratio="1" class="q-mx-xl">
@ -172,7 +174,9 @@
:color="'green'" :color="'green'"
></q-icon> ></q-icon>
Payment Received Payment Received
<lnbits-payment-details :payment="props.row"></lnbits-payment-details> <lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div> </div>
<div v-else-if="props.row.isPaid && props.row.isOut"> <div v-else-if="props.row.isPaid && props.row.isOut">
<q-icon <q-icon
@ -181,12 +185,16 @@
:color="'pink'" :color="'pink'"
></q-icon> ></q-icon>
Payment Sent Payment Sent
<lnbits-payment-details :payment="props.row"></lnbits-payment-details> <lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div> </div>
<div v-else-if="props.row.isOut && props.row.pending"> <div v-else-if="props.row.isOut && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon> <q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending Outgoing payment pending
<lnbits-payment-details :payment="props.row"></lnbits-payment-details> <lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div> </div>
</div> </div>
</q-card> </q-card>
@ -199,47 +207,46 @@
</div> </div>
</div> </div>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<q-btn flat color="grey" @click="exportCSV" class="float-right" <q-btn flat color="grey" @click="exportCSV" class="float-right"
>Renew keys</q-btn >Renew keys</q-btn
> >
<h6 class="text-subtitle1 q-mt-none q-mb-sm">LNbits wallet</h6> <h6 class="text-subtitle1 q-mt-none q-mb-sm">LNbits wallet</h6>
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br /> <strong>Wallet name: </strong><em>{{ wallet.name }}</em><br />
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br /> <strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br />
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br /> <strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br />
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em> <strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "core/_api_docs.html" %}
<q-separator></q-separator> <q-separator></q-separator>
<q-list> <q-expansion-item
{% include "core/_api_docs.html" %} group="extras"
<q-separator></q-separator> icon="remove_circle"
<q-expansion-item label="Delete wallet"
group="extras" >
icon="remove_circle" <q-card>
label="Delete wallet" <q-card-section>
> <p>
<q-card> This whole wallet will be deleted, the funds will be
<q-card-section> <strong>UNRECOVERABLE</strong>.
<p> </p>
This whole wallet will be deleted, the funds will be <q-btn
<strong>UNRECOVERABLE</strong>. unelevated
</p> color="red-10"
<q-btn @click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
unelevated >Delete wallet</q-btn
color="red-10" >
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')" </q-card-section>
>Delete wallet</q-btn </q-card>
> </q-expansion-item>
</q-card-section> </q-list>
</q-card> </q-card-section>
</q-expansion-item> </q-card>
</q-list>
</q-card-section>
</q-card>
</div>
</div> </div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog"> <q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
@ -304,25 +311,25 @@
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="!send.invoice"> <div v-if="!send.invoice">
<q-form <q-form
v-if="!sendCamera.show" v-if="!theCamera.show"
@submit="decodeInvoice" @submit="decodeInvoice"
class="q-gutter-md" class="q-gutter-md"
> >
<q-input <q-input
filled filled
dense dense
v-model.trim="send.data.bolt11" v-model.trim="send.data.request"
type="textarea" type="textarea"
label="Paste an invoice *" label="Paste an invoice, payment request or lnurl code *"
> >
</q-input> </q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
unelevated unelevated
color="deep-purple" color="deep-purple"
:disable="send.data.bolt11 == ''" :disable="send.data.request == ''"
type="submit" type="submit"
>Read invoice</q-btn >Read</q-btn
> >
<q-btn v-close-popup flat color="grey" class="q-ml-auto" <q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn >Cancel</q-btn
@ -343,6 +350,29 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="send.lnurl.withdraw">
{% raw %}
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
<q-separator class="q-my-sm"></q-separator>
<p style="word-break: break-all">
<strong>Description:</strong> {{ send.invoice.description }}<br />
<strong>Payment hash:</strong> {{ send.invoice.hash }}<br />
<strong>Expire date:</strong> {{ send.invoice.expireDate }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="deep-purple" @click="payInvoice"
>Send satoshis</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</div>
<div v-else> <div v-else>
{% raw %} {% raw %}
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6> <h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
@ -383,7 +413,7 @@
</q-dialog> </q-dialog>
<q-dialog v-model="paymentsChart.show" position="top"> <q-dialog v-model="paymentsChart.show" position="top">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset;"> <q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section> <q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas> <canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section> </q-card-section>

40
lnbits/core/views/api.py

@ -1,9 +1,15 @@
<<<<<<< HEAD
import trio # type: ignore import trio # type: ignore
import json import json
import traceback import traceback
from quart import g, jsonify, request, make_response from quart import g, jsonify, request, make_response
=======
import lnurl
from quart import g, jsonify, request
>>>>>>> da8fd9a... send/create buttons wip.
from http import HTTPStatus from http import HTTPStatus
from binascii import unhexlify from binascii import unhexlify
from urllib.parse import urlparse
from lnbits import bolt11 from lnbits import bolt11
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
@ -131,6 +137,7 @@ async def api_payment(payment_hash):
return jsonify({"paid": not payment.pending}), HTTPStatus.OK return jsonify({"paid": not payment.pending}), HTTPStatus.OK
<<<<<<< HEAD
@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")
async def api_payments_sse(): async def api_payments_sse():
@ -183,3 +190,36 @@ async def api_payments_sse():
) )
response.timeout = None response.timeout = None
return response return response
=======
return jsonify({"paid": False}), HTTPStatus.OK
@core_app.route("/api/v1/lnurlscan/<code>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_lnurlscan(code: str):
try:
url = lnurl.Lnurl(code)
except ValueError:
return jsonify({"error": "invalid lnurl"}), HTTPStatus.BAD_REQUEST
domain = urlparse(url.url).netloc
if url.is_login:
return jsonify({"domain": domain, "kind": "auth", "error": "unsupported"})
data: lnurl.LnurlResponseModel = lnurl.get(url.url)
if not data.ok:
return jsonify({"domain": domain, "error": "failed to get parameters"})
if type(data) is lnurl.LnurlChannelResponse:
return jsonify({"domain": domain, "kind": "channel", "error": "unsupported"})
params = data.dict()
if type(data) is lnurl.LnurlWithdrawResponse:
params.update(kind="withdraw", fixed=data.min_withdrawable == data.max_withdrawable)
if type(data) is lnurl.LnurlPayResponse:
params.update(kind="pay", fixed=data.min_sendable == data.max_sendable)
params.update(domain=domain)
return jsonify(params)
>>>>>>> da8fd9a... send/create buttons wip.

4
lnbits/extensions/lnurlp/templates/lnurlp/display.html

@ -26,9 +26,7 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-pay link</h6> <h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-pay link</h6>
<p class="q-my-none"> <p class="q-my-none">Use an LNURL compatible bitcoin wallet to pay.</p>
Use an LNURL compatible bitcoin wallet to pay.
</p>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>

Loading…
Cancel
Save