use {"tag": ext} for extension-related payments.

This commit is contained in:
fiatjaf 2020-09-02 12:44:54 -03:00
parent 4447a48724
commit 197af922d0
14 changed files with 100 additions and 39 deletions

View File

@ -1,3 +1,4 @@
import json
import datetime import datetime
from uuid import uuid4 from uuid import uuid4
from typing import List, Optional, Dict from typing import List, Optional, Dict
@ -245,7 +246,18 @@ def create_payment(
amount, pending, memo, fee, extra) amount, pending, memo, fee, extra)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(wallet_id, checking_id, payment_request, payment_hash, preimage, amount, int(pending), memo, fee, extra), (
wallet_id,
checking_id,
payment_request,
payment_hash,
preimage,
amount,
int(pending),
memo,
fee,
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
),
) )
new_payment = get_wallet_payment(wallet_id, payment_hash) new_payment = get_wallet_payment(wallet_id, payment_hash)

View File

@ -83,6 +83,26 @@ def m002_add_fields_to_apipayments(db):
db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT") db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT") db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT")
import json
rows = db.fetchall("SELECT * FROM apipayments")
for row in rows:
if not row["memo"] or not row["memo"].startswith("#"):
continue
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = "#" + ext + " "
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
db.execute(
"""
UPDATE apipayments SET extra = ?, memo = ?
WHERE checking_id = ? AND memo = ?
""",
(json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]),
)
break
def migrate(): def migrate():
with open_db() as db: with open_db() as db:

View File

@ -1,4 +1,4 @@
/* globals Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _ */ /* globals decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader) Vue.use(VueQrcodeReader)
@ -123,6 +123,7 @@ new Vue({
mixins: [windowMixin], mixins: [windowMixin],
data: function() { data: function() {
return { return {
user: LNbits.map.user(window.user),
receive: { receive: {
show: false, show: false,
status: 'pending', status: 'pending',
@ -146,7 +147,12 @@ new Vue({
payments: [], payments: [],
paymentsTable: { paymentsTable: {
columns: [ columns: [
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, {
name: 'memo',
align: 'left',
label: 'Memo',
field: 'memo'
},
{ {
name: 'date', name: 'date',
align: 'left', align: 'left',
@ -179,7 +185,7 @@ new Vue({
computed: { computed: {
filteredPayments: function() { filteredPayments: function() {
var q = this.paymentsTable.filter var q = this.paymentsTable.filter
if (!q || q == '') return this.payments if (!q || q === '') return this.payments
return LNbits.utils.search(this.payments, q) return LNbits.utils.search(this.payments, q)
}, },
@ -316,11 +322,11 @@ new Vue({
_.each(invoice.data.tags, function(tag) { _.each(invoice.data.tags, function(tag) {
if (_.isObject(tag) && _.has(tag, 'description')) { if (_.isObject(tag) && _.has(tag, 'description')) {
if (tag.description == 'payment_hash') { if (tag.description === 'payment_hash') {
cleanInvoice.hash = tag.value cleanInvoice.hash = tag.value
} else if (tag.description == 'description') { } else if (tag.description === 'description') {
cleanInvoice.description = tag.value cleanInvoice.description = tag.value
} else if (tag.description == 'expiry') { } else if (tag.description === 'expiry') {
var expireDate = new Date( var expireDate = new Date(
(invoice.data.time_stamp + tag.value) * 1000 (invoice.data.time_stamp + tag.value) * 1000
) )

View File

@ -8,7 +8,7 @@
{% endblock %} {% block scripts %} {{ window_vars(user, wallet) }} {% endblock %} {% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> <script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js', {% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %} 'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js', {% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js', 'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
@ -76,7 +76,7 @@
clearable clearable
v-model="paymentsTable.filter" v-model="paymentsTable.filter"
debounce="300" debounce="300"
placeholder="Search by memo, amount" placeholder="Search by tag, memo, amount"
class="q-mb-md" class="q-mb-md"
> >
</q-input> </q-input>
@ -84,7 +84,7 @@
dense dense
flat flat
:data="filteredPayments" :data="filteredPayments"
row-key="checking_id" row-key="payment_hash"
:columns="paymentsTable.columns" :columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination" :pagination.sync="paymentsTable.pagination"
> >
@ -111,6 +111,11 @@
</q-icon> </q-icon>
</q-td> </q-td>
<q-td key="memo" :props="props"> <q-td key="memo" :props="props">
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a class="inherit" :href="['/', props.row.tag, '?usr=', user.id].join('')">
#{{ props.row.tag }}
</a>
</q-badge>
{{ props.row.memo }} {{ props.row.memo }}
</q-td> </q-td>
<q-td auto-width key="date" :props="props"> <q-td auto-width key="date" :props="props">

View File

@ -34,7 +34,9 @@ def api_amilkit(amilk_id):
except LnurlException: except LnurlException:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
payment_hash, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo) payment_hash, payment_request = create_invoice(
wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo, extra={"tag": "amilk"}
)
r = requests.get( r = requests.get(
withdraw_res.callback.base, withdraw_res.callback.base,

View File

@ -109,10 +109,10 @@ def api_tickets():
def api_ticket_make_ticket(event_id, sats): def api_ticket_make_ticket(event_id, sats):
event = get_event(event_id) event = get_event(event_id)
if not event: if not event:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
try: try:
payment_hash, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}" wallet_id=event.wallet, amount=int(sats), memo=f"{event_id}", extra={"tag": "events"}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
@ -120,7 +120,7 @@ def api_ticket_make_ticket(event_id, sats):
ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data) ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data)
if not ticket: if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK

View File

@ -106,15 +106,15 @@
computed: { computed: {
amountWords() { amountWords() {
var regex = /\s+/gi var regex = /\s+/gi
var char = this.formDialog.data.text var nwords = this.formDialog.data.text
.trim() .trim()
.replace(regex, ' ') .replace(regex, ' ')
.split(' ').length .split(' ').length
this.formDialog.data.sats = char * parseInt('{{ form_costpword }}') var sats = nwords * parseInt('{{ form_costpword }}')
if (this.formDialog.data.sats == parseInt('{{ form_costpword }}')) { if (sats === parseInt('{{ form_costpword }}')) {
return '0 Sats to pay' return '0 Sats to pay'
} else { } else {
return this.formDialog.data.sats + ' Sats to pay' return sats + ' Sats to pay'
} }
} }
}, },
@ -125,7 +125,6 @@
this.formDialog.data.name = '' this.formDialog.data.name = ''
this.formDialog.data.email = '' this.formDialog.data.email = ''
this.formDialog.data.text = '' this.formDialog.data.text = ''
this.formDialog.data.sats = 0
}, },
closeReceiveDialog: function () { closeReceiveDialog: function () {
@ -139,15 +138,12 @@
var self = this var self = this
axios axios
.post( .post(
'/lnticket/api/v1/tickets/' + '/lnticket/api/v1/tickets/{{ form_id }}',
'{{ form_id }}/' +
self.formDialog.data.sats,
{ {
form: '{{ form_id }}', form: '{{ form_id }}',
name: self.formDialog.data.name, name: self.formDialog.data.name,
email: self.formDialog.data.email, email: self.formDialog.data.email,
ltext: self.formDialog.data.text, ltext: self.formDialog.data.text,
sats: self.formDialog.data.sats
} }
) )
.then(function (response) { .then(function (response) {
@ -175,7 +171,6 @@
self.formDialog.data.name = '' self.formDialog.data.name = ''
self.formDialog.data.email = '' self.formDialog.data.email = ''
self.formDialog.data.text = '' self.formDialog.data.text = ''
self.formDialog.data.sats = 0
self.$q.notify({ self.$q.notify({
type: 'positive', type: 'positive',

View File

@ -1,3 +1,4 @@
import re
from flask import g, jsonify, request from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
@ -48,7 +49,6 @@ def api_forms():
def api_form_create(form_id=None): def api_form_create(form_id=None):
if form_id: if form_id:
form = get_form(form_id) form = get_form(form_id)
print(g.data)
if not form: if not form:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
@ -92,29 +92,32 @@ def api_tickets():
return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<form_id>/<sats>", methods=["POST"]) @lnticket_ext.route("/api/v1/tickets/<form_id>", methods=["POST"])
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"form": {"type": "string", "empty": False, "required": True}, "form": {"type": "string", "empty": False, "required": True},
"name": {"type": "string", "empty": False, "required": True}, "name": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True}, "email": {"type": "string", "empty": True, "required": True},
"ltext": {"type": "string", "empty": False, "required": True}, "ltext": {"type": "string", "empty": False, "required": True},
"sats": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_ticket_make_ticket(form_id, sats): def api_ticket_make_ticket(form_id):
event = get_form(form_id) form = get_form(form_id)
if not form:
if not event:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
try: try:
nwords = len(re.split(r"\s+", g.data["ltext"]))
sats = nwords * form.costpword
payment_hash, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}" wallet_id=form.wallet,
amount=sats,
memo=f"ticket with {nwords} words on {form_id}",
extra={"tag": "lnticket"},
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, **g.data) ticket = create_ticket(payment_hash=payment_hash, wallet=form.wallet, sats=sats, **g.data)
if not ticket: if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND

View File

@ -123,6 +123,7 @@ def api_lnurl_callback(link_id):
amount=link.amount, amount=link.amount,
memo=link.description, memo=link.description,
description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(), description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(),
extra={"tag": "lnurlp"},
) )
resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[]) resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[])

View File

@ -64,7 +64,7 @@ def api_paywall_create_invoice(paywall_id):
try: try:
amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
payment_hash, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}" wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={'tag': 'paywall'}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR

View File

@ -60,7 +60,7 @@ def api_tpos_create_invoice(tpos_id):
try: try:
payment_hash, payment_request = create_invoice( payment_hash, payment_request = create_invoice(
wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}" wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"{tpos.name}", extra={"tag": "tpos"}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR

View File

@ -62,5 +62,5 @@ class WithdrawLink(NamedTuple):
k1=self.k1, k1=self.k1,
min_withdrawable=self.min_withdrawable * 1000, min_withdrawable=self.min_withdrawable * 1000,
max_withdrawable=self.max_withdrawable * 1000, max_withdrawable=self.max_withdrawable * 1000,
default_description="#withdraw LNbits LNURL", default_description="LNbits voucher",
) )

View File

@ -182,7 +182,12 @@ def api_lnurl_callback(unique_hash):
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK
try: try:
pay_invoice(wallet_id=link.wallet, payment_request=payment_request, max_sat=link.max_withdrawable) pay_invoice(
wallet_id=link.wallet,
payment_request=payment_request,
max_sat=link.max_withdrawable,
extra={"tag": "withdraw"},
)
changes = { changes = {
"open_time": link.wait_time + now, "open_time": link.wait_time + now,

View File

@ -94,7 +94,18 @@ var LNbits = {
}, },
payment: function(data) { payment: function(data) {
var obj = _.object( var obj = _.object(
['checking_id', 'pending', 'amount', 'fee', 'memo', 'time'], [
'checking_id',
'pending',
'amount',
'fee',
'memo',
'time',
'bolt11',
'preimage',
'payment_hash',
'extra'
],
data data
) )
obj.date = Quasar.utils.date.formatDate( obj.date = Quasar.utils.date.formatDate(
@ -103,6 +114,7 @@ var LNbits = {
) )
obj.msat = obj.amount obj.msat = obj.amount
obj.sat = obj.msat / 1000 obj.sat = obj.msat / 1000
obj.tag = obj.extra.tag
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat) obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
obj.isIn = obj.amount > 0 obj.isIn = obj.amount > 0
obj.isOut = obj.amount < 0 obj.isOut = obj.amount < 0