lnurlp: accept comments, USD prices, min/max ranges.
This commit is contained in:
parent
2552fd8fc9
commit
2863653261
10 changed files with 495 additions and 322 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
|
import json
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from lnbits import bolt11
|
|
||||||
from lnbits.db import open_ext_db
|
from lnbits.db import open_ext_db
|
||||||
|
from lnbits.core.models import Payment
|
||||||
|
from quart import g
|
||||||
|
|
||||||
from .models import PayLink
|
from .models import PayLink
|
||||||
|
|
||||||
|
|
@ -10,7 +12,10 @@ def create_pay_link(
|
||||||
*,
|
*,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
description: str,
|
description: str,
|
||||||
amount: int,
|
min: int,
|
||||||
|
max: int,
|
||||||
|
comment_chars: int = 0,
|
||||||
|
currency: Optional[str] = None,
|
||||||
webhook_url: Optional[str] = None,
|
webhook_url: Optional[str] = None,
|
||||||
success_text: Optional[str] = None,
|
success_text: Optional[str] = None,
|
||||||
success_url: Optional[str] = None,
|
success_url: Optional[str] = None,
|
||||||
|
|
@ -21,16 +26,29 @@ def create_pay_link(
|
||||||
INSERT INTO pay_links (
|
INSERT INTO pay_links (
|
||||||
wallet,
|
wallet,
|
||||||
description,
|
description,
|
||||||
amount,
|
min,
|
||||||
|
max,
|
||||||
served_meta,
|
served_meta,
|
||||||
served_pr,
|
served_pr,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
success_text,
|
success_text,
|
||||||
success_url
|
success_url,
|
||||||
|
comment_chars,
|
||||||
|
currency
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, 0, 0, ?, ?, ?)
|
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(wallet_id, description, amount, webhook_url, success_text, success_url),
|
(
|
||||||
|
wallet_id,
|
||||||
|
description,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
webhook_url,
|
||||||
|
success_text,
|
||||||
|
success_url,
|
||||||
|
comment_chars,
|
||||||
|
currency,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
link_id = db.cursor.lastrowid
|
link_id = db.cursor.lastrowid
|
||||||
return get_pay_link(link_id)
|
return get_pay_link(link_id)
|
||||||
|
|
@ -43,22 +61,6 @@ def get_pay_link(link_id: int) -> Optional[PayLink]:
|
||||||
return PayLink.from_row(row) if row else None
|
return PayLink.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def get_pay_link_by_invoice(payment_hash: str) -> Optional[PayLink]:
|
|
||||||
# this excludes invoices with webhooks that have been sent already
|
|
||||||
|
|
||||||
with open_ext_db("lnurlp") as db:
|
|
||||||
row = db.fetchone(
|
|
||||||
"""
|
|
||||||
SELECT pay_links.* FROM pay_links
|
|
||||||
INNER JOIN invoices ON invoices.pay_link = pay_links.id
|
|
||||||
WHERE payment_hash = ? AND webhook_sent IS NULL
|
|
||||||
""",
|
|
||||||
(payment_hash,),
|
|
||||||
)
|
|
||||||
|
|
||||||
return PayLink.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
||||||
if isinstance(wallet_ids, str):
|
if isinstance(wallet_ids, str):
|
||||||
wallet_ids = [wallet_ids]
|
wallet_ids = [wallet_ids]
|
||||||
|
|
@ -101,25 +103,12 @@ def delete_pay_link(link_id: int) -> None:
|
||||||
db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
|
db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
|
||||||
|
|
||||||
|
|
||||||
def save_link_invoice(link_id: int, payment_request: str) -> None:
|
def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
inv = bolt11.decode(payment_request)
|
payment.extra["wh_status"] = status
|
||||||
|
g.db.execute(
|
||||||
with open_ext_db("lnurlp") as db:
|
"""
|
||||||
db.execute(
|
UPDATE apipayments SET extra = ?
|
||||||
"""
|
WHERE hash = ?
|
||||||
INSERT INTO invoices (pay_link, payment_hash, expiry)
|
""",
|
||||||
VALUES (?, ?, ?)
|
(json.dumps(payment.extra), payment.payment_hash),
|
||||||
""",
|
)
|
||||||
(link_id, inv.payment_hash, inv.expiry),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mark_webhook_sent(payment_hash: str, status: int) -> None:
|
|
||||||
with open_ext_db("lnurlp") as db:
|
|
||||||
db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE invoices SET webhook_sent = ?
|
|
||||||
WHERE payment_hash = ?
|
|
||||||
""",
|
|
||||||
(status, payment_hash),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
48
lnbits/extensions/lnurlp/helpers.py
Normal file
48
lnbits/extensions/lnurlp/helpers.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import trio # type: ignore
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fiat_rate(currency: str):
|
||||||
|
assert currency == "USD", "Only USD is supported as fiat currency."
|
||||||
|
return await get_usd_rate()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_usd_rate():
|
||||||
|
"""
|
||||||
|
Returns an average satoshi price from multiple sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
satoshi_prices = [None, None, None]
|
||||||
|
|
||||||
|
async def fetch_price(index, url, getter):
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(url)
|
||||||
|
r.raise_for_status()
|
||||||
|
satoshi_price = int(100_000_000 / float(getter(r.json())))
|
||||||
|
satoshi_prices[index] = satoshi_price
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
nursery.start_soon(
|
||||||
|
fetch_price,
|
||||||
|
0,
|
||||||
|
"https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD",
|
||||||
|
lambda d: d["result"]["XXBTCZUSD"]["c"][0],
|
||||||
|
)
|
||||||
|
nursery.start_soon(
|
||||||
|
fetch_price,
|
||||||
|
1,
|
||||||
|
"https://www.bitstamp.net/api/v2/ticker/btcusd",
|
||||||
|
lambda d: d["last"],
|
||||||
|
)
|
||||||
|
nursery.start_soon(
|
||||||
|
fetch_price,
|
||||||
|
2,
|
||||||
|
"https://api.coincap.io/v2/rates/bitcoin",
|
||||||
|
lambda d: d["data"]["rateUsd"],
|
||||||
|
)
|
||||||
|
|
||||||
|
satoshi_prices = [x for x in satoshi_prices if x]
|
||||||
|
return sum(satoshi_prices) / len(satoshi_prices)
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import math
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from quart import jsonify, url_for
|
from quart import jsonify, url_for, request
|
||||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse
|
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
|
|
||||||
from . import lnurlp_ext
|
from . import lnurlp_ext
|
||||||
from .crud import increment_pay_link, save_link_invoice
|
from .crud import increment_pay_link
|
||||||
|
from .helpers import get_fiat_rate
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
|
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
|
||||||
|
|
@ -15,16 +17,19 @@ async def api_lnurl_response(link_id):
|
||||||
if not link:
|
if not link:
|
||||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||||
|
|
||||||
url = url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True)
|
rate = await get_fiat_rate(link.currency) if link.currency else 1
|
||||||
|
|
||||||
resp = LnurlPayResponse(
|
resp = LnurlPayResponse(
|
||||||
callback=url,
|
callback=url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True),
|
||||||
min_sendable=link.amount * 1000,
|
min_sendable=math.ceil(link.min * rate) * 1000,
|
||||||
max_sendable=link.amount * 1000,
|
max_sendable=round(link.max * rate) * 1000,
|
||||||
metadata=link.lnurlpay_metadata,
|
metadata=link.lnurlpay_metadata,
|
||||||
)
|
)
|
||||||
|
params = resp.dict()
|
||||||
|
|
||||||
return jsonify(resp.dict()), HTTPStatus.OK
|
if link.comment_chars > 0:
|
||||||
|
params["commentAllowed"] = link.comment_chars
|
||||||
|
|
||||||
|
return jsonify(params), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
|
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
|
||||||
|
|
@ -33,16 +38,44 @@ async def api_lnurl_callback(link_id):
|
||||||
if not link:
|
if not link:
|
||||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||||
|
|
||||||
|
min, max = link.min, link.max
|
||||||
|
rate = await get_fiat_rate(link.currency) if link.currency else 1
|
||||||
|
if link.currency:
|
||||||
|
# allow some fluctuation (as the fiat price may have changed between the calls)
|
||||||
|
min = rate * 995 * link.min
|
||||||
|
max = rate * 1010 * link.max
|
||||||
|
|
||||||
|
amount_received = int(request.args.get("amount"))
|
||||||
|
if amount_received < min:
|
||||||
|
return (
|
||||||
|
jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is smaller than minimum {min}.").dict()),
|
||||||
|
HTTPStatus.OK,
|
||||||
|
)
|
||||||
|
elif amount_received > max:
|
||||||
|
return (
|
||||||
|
jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is greater than maximum {max}.").dict()),
|
||||||
|
HTTPStatus.OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
comment = request.args.get("comment")
|
||||||
|
if len(comment or "") > link.comment_chars:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
LnurlErrorResponse(
|
||||||
|
reason=f"Got a comment with {len(comment)} characters, but can only accept {link.comment_chars}"
|
||||||
|
).dict()
|
||||||
|
),
|
||||||
|
HTTPStatus.OK,
|
||||||
|
)
|
||||||
|
|
||||||
payment_hash, payment_request = create_invoice(
|
payment_hash, payment_request = create_invoice(
|
||||||
wallet_id=link.wallet,
|
wallet_id=link.wallet,
|
||||||
amount=link.amount,
|
amount=int(amount_received / 1000),
|
||||||
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"},
|
extra={"tag": "lnurlp", "link": link.id, "comment": comment},
|
||||||
)
|
)
|
||||||
|
|
||||||
save_link_invoice(link_id, payment_request)
|
|
||||||
|
|
||||||
resp = LnurlPayActionResponse(
|
resp = LnurlPayActionResponse(
|
||||||
pr=payment_request,
|
pr=payment_request,
|
||||||
success_action=link.success_action(payment_hash),
|
success_action=link.success_action(payment_hash),
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,16 @@ def m002_webhooks_and_success_actions(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def m003_min_max_comment_fiat(db):
|
||||||
|
"""
|
||||||
|
Support for min/max amounts, comments and fiat prices that get
|
||||||
|
converted automatically to satoshis based on some API.
|
||||||
|
"""
|
||||||
|
db.execute("ALTER TABLE pay_links ADD COLUMN currency TEXT;") # null = satoshis
|
||||||
|
db.execute("ALTER TABLE pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;")
|
||||||
|
db.execute("ALTER TABLE pay_links RENAME COLUMN amount TO min;")
|
||||||
|
db.execute("ALTER TABLE pay_links ADD COLUMN max INTEGER;")
|
||||||
|
db.execute("UPDATE pay_links SET max = min;")
|
||||||
|
db.execute("DROP TABLE invoices")
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@ class PayLink(NamedTuple):
|
||||||
id: int
|
id: int
|
||||||
wallet: str
|
wallet: str
|
||||||
description: str
|
description: str
|
||||||
amount: int
|
min: int
|
||||||
served_meta: int
|
served_meta: int
|
||||||
served_pr: int
|
served_pr: int
|
||||||
webhook_url: str
|
webhook_url: str
|
||||||
success_text: str
|
success_text: str
|
||||||
success_url: str
|
success_url: str
|
||||||
|
currency: str
|
||||||
|
comment_chars: int
|
||||||
|
max: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "PayLink":
|
def from_row(cls, row: Row) -> "PayLink":
|
||||||
|
|
|
||||||
207
lnbits/extensions/lnurlp/static/js/index.js
Normal file
207
lnbits/extensions/lnurlp/static/js/index.js
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
||||||
|
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
var locationPath = [
|
||||||
|
window.location.protocol,
|
||||||
|
'//',
|
||||||
|
window.location.host,
|
||||||
|
window.location.pathname
|
||||||
|
].join('')
|
||||||
|
|
||||||
|
var mapPayLink = obj => {
|
||||||
|
obj._data = _.clone(obj)
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||||
|
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
||||||
|
obj.pay_url = [locationPath, obj.id].join('')
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fiatRates: {},
|
||||||
|
checker: null,
|
||||||
|
payLinks: [],
|
||||||
|
payLinksTable: {
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
fixedAmount: true,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
qrCodeDialog: {
|
||||||
|
show: false,
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getPayLinks() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/lnurlp/api/v1/links?all_wallets',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
this.payLinks = response.data.map(mapPayLink)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
clearInterval(this.checker)
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeFormDialog() {},
|
||||||
|
openQrCodeDialog(linkId) {
|
||||||
|
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||||
|
if (link.currency) this.updateFiatRate(link.currency)
|
||||||
|
|
||||||
|
this.qrCodeDialog.data = {
|
||||||
|
id: link.id,
|
||||||
|
amount:
|
||||||
|
(link.min === link.max ? link.min : `${link.min} - ${link.max}`) +
|
||||||
|
' ' +
|
||||||
|
(link.currency || 'sat'),
|
||||||
|
currency: link.currency,
|
||||||
|
comments: link.comment_chars
|
||||||
|
? `${link.comment_chars} characters`
|
||||||
|
: 'no',
|
||||||
|
webhook: link.webhook_url || 'nowhere',
|
||||||
|
success:
|
||||||
|
link.success_text || link.success_url
|
||||||
|
? 'Display message "' +
|
||||||
|
link.success_text +
|
||||||
|
'"' +
|
||||||
|
(link.success_url ? ' and URL "' + link.success_url + '"' : '')
|
||||||
|
: 'do nothing',
|
||||||
|
lnurl: link.lnurl,
|
||||||
|
pay_url: link.pay_url,
|
||||||
|
print_url: link.print_url
|
||||||
|
}
|
||||||
|
this.qrCodeDialog.show = true
|
||||||
|
},
|
||||||
|
openUpdateDialog(linkId) {
|
||||||
|
const link = _.findWhere(this.payLinks, {id: linkId})
|
||||||
|
if (link.currency) this.updateFiatRate(link.currency)
|
||||||
|
|
||||||
|
this.formDialog.data = _.clone(link._data)
|
||||||
|
this.formDialog.show = true
|
||||||
|
this.formDialog.fixedAmount =
|
||||||
|
this.formDialog.data.min === this.formDialog.data.max
|
||||||
|
},
|
||||||
|
sendFormData() {
|
||||||
|
const wallet = _.findWhere(this.g.user.wallets, {
|
||||||
|
id: this.formDialog.data.wallet
|
||||||
|
})
|
||||||
|
var data = _.omit(this.formDialog.data, 'wallet')
|
||||||
|
|
||||||
|
if (this.formDialog.fixedAmount) data.max = data.min
|
||||||
|
if (data.currency === 'satoshis') data.currency = null
|
||||||
|
if (isNaN(parseInt(data.comment_chars))) data.comment_chars = 0
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
this.updatePayLink(wallet, data)
|
||||||
|
} else {
|
||||||
|
this.createPayLink(wallet, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatePayLink(wallet, data) {
|
||||||
|
let values = _.omit(
|
||||||
|
_.pick(
|
||||||
|
data,
|
||||||
|
'description',
|
||||||
|
'min',
|
||||||
|
'max',
|
||||||
|
'webhook_url',
|
||||||
|
'success_text',
|
||||||
|
'success_url',
|
||||||
|
'comment_chars',
|
||||||
|
'currency'
|
||||||
|
),
|
||||||
|
(value, key) =>
|
||||||
|
(key === 'webhook_url' ||
|
||||||
|
key === 'success_text' ||
|
||||||
|
key === 'success_url') &&
|
||||||
|
(value === null || value === '')
|
||||||
|
)
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
'/lnurlp/api/v1/links/' + data.id,
|
||||||
|
wallet.adminkey,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
|
||||||
|
this.payLinks.push(mapPayLink(response.data))
|
||||||
|
this.formDialog.show = false
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createPayLink(wallet, data) {
|
||||||
|
LNbits.api
|
||||||
|
.request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
|
||||||
|
.then(response => {
|
||||||
|
this.payLinks.push(mapPayLink(response.data))
|
||||||
|
this.formDialog.show = false
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deletePayLink: linkId => {
|
||||||
|
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||||
|
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||||
|
.onOk(() => {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/lnurlp/api/v1/links/' + linkId,
|
||||||
|
_.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
this.payLinks = _.reject(this.payLinks, obj => obj.id === linkId)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateFiatRate(currency) {
|
||||||
|
LNbits.api
|
||||||
|
.request('GET', '/lnurlp/api/v1/rate/' + currency, null)
|
||||||
|
.then(response => {
|
||||||
|
let rates = _.clone(this.fiatRates)
|
||||||
|
rates[currency] = response.data.rate
|
||||||
|
this.fiatRates = rates
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.g.user.wallets.length) {
|
||||||
|
var getPayLinks = this.getPayLinks
|
||||||
|
getPayLinks()
|
||||||
|
this.checker = setInterval(() => {
|
||||||
|
getPayLinks()
|
||||||
|
}, 20000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -4,7 +4,7 @@ import httpx
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.tasks import run_on_pseudo_request, register_invoice_listener
|
from lnbits.tasks import run_on_pseudo_request, register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_pay_link_by_invoice, mark_webhook_sent
|
from .crud import mark_webhook_sent, get_pay_link
|
||||||
|
|
||||||
|
|
||||||
async def register_listeners():
|
async def register_listeners():
|
||||||
|
|
@ -19,25 +19,29 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
islnurlp = "lnurlp" == payment.extra.get("tag")
|
if "lnurlp" != payment.extra.get("tag"):
|
||||||
if islnurlp:
|
# not an lnurlp invoice
|
||||||
pay_link = get_pay_link_by_invoice(payment.payment_hash)
|
return
|
||||||
if not pay_link:
|
|
||||||
# no pay_link or this webhook has already been sent
|
if payment.extra.get("wh_status"):
|
||||||
return
|
# this webhook has already been sent
|
||||||
if pay_link.webhook_url:
|
return
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
pay_link = get_pay_link(payment.extra.get("link", -1))
|
||||||
r = await client.post(
|
if pay_link and pay_link.webhook_url:
|
||||||
pay_link.webhook_url,
|
async with httpx.AsyncClient() as client:
|
||||||
json={
|
try:
|
||||||
"payment_hash": payment.payment_hash,
|
r = await client.post(
|
||||||
"payment_request": payment.bolt11,
|
pay_link.webhook_url,
|
||||||
"amount": payment.amount,
|
json={
|
||||||
"lnurlp": pay_link.id,
|
"payment_hash": payment.payment_hash,
|
||||||
},
|
"payment_request": payment.bolt11,
|
||||||
timeout=40,
|
"amount": payment.amount,
|
||||||
)
|
"comment": payment.extra.get("comment"),
|
||||||
mark_webhook_sent(payment.payment_hash, r.status_code)
|
"lnurlp": pay_link.id,
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
},
|
||||||
mark_webhook_sent(payment.payment_hash, -1)
|
timeout=40,
|
||||||
|
)
|
||||||
|
mark_webhook_sent(payment, r.status_code)
|
||||||
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
|
mark_webhook_sent(payment, -1)
|
||||||
|
|
|
||||||
|
|
@ -16,25 +16,22 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h5 class="text-subtitle1 q-my-none">Pay links</h5>
|
<h5 class="text-subtitle1 q-my-none">Pay links</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
:data="payLinks"
|
:data="payLinks"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:columns="payLinksTable.columns"
|
|
||||||
:pagination.sync="payLinksTable.pagination"
|
:pagination.sync="payLinksTable.pagination"
|
||||||
>
|
>
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th auto-width>Description</q-th>
|
||||||
{{ col.label }}
|
<q-th auto-width>Amount</q-th>
|
||||||
</q-th>
|
<q-th auto-width>Currency</q-th>
|
||||||
|
<q-th auto-width></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -60,8 +57,39 @@
|
||||||
@click="openQrCodeDialog(props.row.id)"
|
@click="openQrCodeDialog(props.row.id)"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td auto-width>{{ props.row.description }}</q-td>
|
||||||
{{ col.value }}
|
<q-td auto-width>
|
||||||
|
<span v-if="props.row.min == props.row.max">
|
||||||
|
{{ props.row.min }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ props.row.min }} - {{ props.row.max }}</span>
|
||||||
|
</q-td>
|
||||||
|
<q-td>{{ props.row.currency || 'sat' }}</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-icon v-if="props.row.webhook_url" size="14px" name="http">
|
||||||
|
<q-tooltip>Webhook to {{ props.row.webhook_url}}</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
<q-icon
|
||||||
|
v-if="props.row.success_text || props.row.success_url"
|
||||||
|
size="14px"
|
||||||
|
name="call_to_action"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
On success, show message '{{ props.row.success_text }}'
|
||||||
|
<span v-if="props.row.success_url"
|
||||||
|
>and URL '{{ props.row.success_url }}'</span
|
||||||
|
>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
<q-icon
|
||||||
|
v-if="props.row.comment_chars > 0"
|
||||||
|
size="14px"
|
||||||
|
name="insert_comment"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
{{ props.row.comment_chars }}-char comment allowed
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -124,12 +152,52 @@
|
||||||
type="text"
|
type="text"
|
||||||
label="Item description *"
|
label="Item description *"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="formDialog.data.min"
|
||||||
|
type="number"
|
||||||
|
:step="formDialog.data.currency && formDialog.data.currency !== 'satoshis' ? '0.01' : '1'"
|
||||||
|
:label="formDialog.fixedAmount ? 'Amount *' : 'Min *'"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="!formDialog.fixedAmount"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="formDialog.data.max"
|
||||||
|
type="number"
|
||||||
|
:step="formDialog.data.currency && formDialog.data.currency !== 'satoshis' ? '0.01' : '1'"
|
||||||
|
label="Max *"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox
|
||||||
|
dense
|
||||||
|
v-model="formDialog.fixedAmount"
|
||||||
|
label="Fixed amount"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
:options='["satoshis", "USD"]'
|
||||||
|
v-model="formDialog.data.currency"
|
||||||
|
:display-value="formDialog.data.currency || 'satoshis'"
|
||||||
|
label="Currency"
|
||||||
|
:hint="'Amounts will be converted at use-time to satoshis. ' + (formDialog.data.currency && fiatRates[formDialog.data.currency] ? `Currently 1 ${formDialog.data.currency} = ${fiatRates[formDialog.data.currency]} sat` : '')"
|
||||||
|
@input="updateFiatRate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.number="formDialog.data.amount"
|
v-model.number="formDialog.data.comment_chars"
|
||||||
type="number"
|
type="number"
|
||||||
label="Amount (sat) *"
|
label="Comment maximum characters"
|
||||||
|
hint="Tell wallets to prompt users for a comment that will be sent along with the payment. LNURLp will store the comment and send it in the webhook."
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
|
|
@ -171,8 +239,8 @@
|
||||||
formDialog.data.wallet == null ||
|
formDialog.data.wallet == null ||
|
||||||
formDialog.data.description == null ||
|
formDialog.data.description == null ||
|
||||||
(
|
(
|
||||||
formDialog.data.amount == null ||
|
formDialog.data.min == null ||
|
||||||
formDialog.data.amount < 1
|
formDialog.data.min <= 0
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -198,11 +266,16 @@
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<p style="word-break: break-all">
|
<p style="word-break: break-all">
|
||||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
||||||
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }} sat<br />
|
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }}<br />
|
||||||
<strong>Webhook:</strong> {{ qrCodeDialog.data.webhook_url }}<br />
|
<span v-if="qrCodeDialog.data.currency"
|
||||||
<strong>Success Message:</strong> {{ qrCodeDialog.data.success_text
|
><strong>{{ qrCodeDialog.data.currency }} price:</strong> {{
|
||||||
|
fiatRates[qrCodeDialog.data.currency] ?
|
||||||
|
fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}<br
|
||||||
|
/></span>
|
||||||
|
<strong>Accepts comments:</strong> {{ qrCodeDialog.data.comments }}<br />
|
||||||
|
<strong>Dispatches webhook to:</strong> {{ qrCodeDialog.data.webhook
|
||||||
}}<br />
|
}}<br />
|
||||||
<strong>Success URL:</strong> {{ qrCodeDialog.data.success_url }}<br />
|
<strong>On success:</strong> {{ qrCodeDialog.data.success }}<br />
|
||||||
</p>
|
</p>
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
|
|
@ -220,7 +293,6 @@
|
||||||
>Shareable link</q-btn
|
>Shareable link</q-btn
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!qrCodeDialog.data.is_unique"
|
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
icon="print"
|
icon="print"
|
||||||
|
|
@ -234,223 +306,5 @@
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script src="/lnurlp/static/js/index.js"></script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
var locationPath = [
|
|
||||||
window.location.protocol,
|
|
||||||
'//',
|
|
||||||
window.location.host,
|
|
||||||
window.location.pathname
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
var mapPayLink = function (obj) {
|
|
||||||
obj._data = _.clone(obj)
|
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
|
||||||
new Date(obj.time * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
|
||||||
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
|
||||||
obj.pay_url = [locationPath, obj.id].join('')
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
checker: null,
|
|
||||||
payLinks: [],
|
|
||||||
payLinksTable: {
|
|
||||||
columns: [
|
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Description',
|
|
||||||
field: 'description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount',
|
|
||||||
align: 'right',
|
|
||||||
label: 'Amount (sat)',
|
|
||||||
field: 'amount'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'webhook_url',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Webhook URL',
|
|
||||||
field: 'webhook_url'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'success_action',
|
|
||||||
align: 'center',
|
|
||||||
label: '',
|
|
||||||
format: (_, row) =>
|
|
||||||
row.success_text || row.success_url ? '💬' : ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formDialog: {
|
|
||||||
show: false,
|
|
||||||
secondMultiplier: 'seconds',
|
|
||||||
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
|
|
||||||
data: {
|
|
||||||
is_unique: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
qrCodeDialog: {
|
|
||||||
show: false,
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getPayLinks: function () {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/lnurlp/api/v1/links?all_wallets',
|
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.payLinks = response.data.map(function (obj) {
|
|
||||||
return mapPayLink(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
clearInterval(self.checker)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeFormDialog: function () {
|
|
||||||
this.formDialog.data = {
|
|
||||||
is_unique: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openQrCodeDialog: function (linkId) {
|
|
||||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
|
||||||
this.qrCodeDialog.data = _.clone(link)
|
|
||||||
this.qrCodeDialog.show = true
|
|
||||||
},
|
|
||||||
openUpdateDialog: function (linkId) {
|
|
||||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
|
||||||
this.formDialog.data = _.clone(link._data)
|
|
||||||
this.formDialog.show = true
|
|
||||||
},
|
|
||||||
sendFormData: function () {
|
|
||||||
var wallet = _.findWhere(this.g.user.wallets, {
|
|
||||||
id: this.formDialog.data.wallet
|
|
||||||
})
|
|
||||||
var data = _.omit(this.formDialog.data, 'wallet')
|
|
||||||
|
|
||||||
data.wait_time =
|
|
||||||
data.wait_time *
|
|
||||||
{
|
|
||||||
seconds: 1,
|
|
||||||
minutes: 60,
|
|
||||||
hours: 3600
|
|
||||||
}[this.formDialog.secondMultiplier]
|
|
||||||
|
|
||||||
if (data.id) {
|
|
||||||
this.updatePayLink(wallet, data)
|
|
||||||
} else {
|
|
||||||
this.createPayLink(wallet, data)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updatePayLink: function (wallet, data) {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
let values = _.omit(
|
|
||||||
_.pick(
|
|
||||||
data,
|
|
||||||
'description',
|
|
||||||
'amount',
|
|
||||||
'webhook_url',
|
|
||||||
'success_text',
|
|
||||||
'success_url'
|
|
||||||
),
|
|
||||||
(value, key) =>
|
|
||||||
(key === 'webhook_url' ||
|
|
||||||
key === 'success_text' ||
|
|
||||||
key === 'success_url') &&
|
|
||||||
(value === null || value === '')
|
|
||||||
)
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'PUT',
|
|
||||||
'/lnurlp/api/v1/links/' + data.id,
|
|
||||||
wallet.adminkey,
|
|
||||||
values
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.payLinks = _.reject(self.payLinks, function (obj) {
|
|
||||||
return obj.id === data.id
|
|
||||||
})
|
|
||||||
self.payLinks.push(mapPayLink(response.data))
|
|
||||||
self.formDialog.show = false
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createPayLink: function (wallet, data) {
|
|
||||||
var self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
|
|
||||||
.then(function (response) {
|
|
||||||
self.payLinks.push(mapPayLink(response.data))
|
|
||||||
self.formDialog.show = false
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deletePayLink: function (linkId) {
|
|
||||||
var self = this
|
|
||||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
|
||||||
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
|
||||||
.onOk(function () {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
'/lnurlp/api/v1/links/' + linkId,
|
|
||||||
_.findWhere(self.g.user.wallets, {id: link.wallet}).adminkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.payLinks = _.reject(self.payLinks, function (obj) {
|
|
||||||
return obj.id === linkId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
exportCSV: function () {
|
|
||||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: function () {
|
|
||||||
if (this.g.user.wallets.length) {
|
|
||||||
var getPayLinks = this.getPayLinks
|
|
||||||
getPayLinks()
|
|
||||||
this.checker = setInterval(function () {
|
|
||||||
getPayLinks()
|
|
||||||
}, 20000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from quart import g, jsonify, request
|
from quart import g, jsonify, request
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
|
|
||||||
from lnbits.extensions.lnurlp import lnurlp_ext
|
from lnbits.extensions.lnurlp import lnurlp_ext # type: ignore
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_pay_link,
|
create_pay_link,
|
||||||
get_pay_link,
|
get_pay_link,
|
||||||
|
|
@ -13,6 +13,7 @@ from .crud import (
|
||||||
update_pay_link,
|
update_pay_link,
|
||||||
delete_pay_link,
|
delete_pay_link,
|
||||||
)
|
)
|
||||||
|
from .helpers import get_fiat_rate
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.route("/api/v1/links", methods=["GET"])
|
@lnurlp_ext.route("/api/v1/links", methods=["GET"])
|
||||||
|
|
@ -55,13 +56,24 @@ async def api_link_retrieve(link_id):
|
||||||
@api_validate_post_request(
|
@api_validate_post_request(
|
||||||
schema={
|
schema={
|
||||||
"description": {"type": "string", "empty": False, "required": True},
|
"description": {"type": "string", "empty": False, "required": True},
|
||||||
"amount": {"type": "integer", "min": 1, "required": True},
|
"min": {"type": "number", "min": 0.01, "required": True},
|
||||||
|
"max": {"type": "number", "min": 0.01, "required": True},
|
||||||
|
"currency": {"type": "string", "allowed": ["USD"], "nullable": True, "required": False},
|
||||||
|
"comment_chars": {"type": "integer", "required": True, "min": 0, "max": 800},
|
||||||
"webhook_url": {"type": "string", "required": False},
|
"webhook_url": {"type": "string", "required": False},
|
||||||
"success_text": {"type": "string", "required": False},
|
"success_text": {"type": "string", "required": False},
|
||||||
"success_url": {"type": "string", "required": False},
|
"success_url": {"type": "string", "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_link_create_or_update(link_id=None):
|
async def api_link_create_or_update(link_id=None):
|
||||||
|
if g.data["min"] > g.data["max"]:
|
||||||
|
return jsonify({"message": "Min is greater than max."}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
if g.data.get("currency") == None and (
|
||||||
|
round(g.data["min"]) != g.data["min"] or round(g.data["max"]) != g.data["max"]
|
||||||
|
):
|
||||||
|
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
if link_id:
|
if link_id:
|
||||||
link = get_pay_link(link_id)
|
link = get_pay_link(link_id)
|
||||||
|
|
||||||
|
|
@ -92,3 +104,13 @@ async def api_link_delete(link_id):
|
||||||
delete_pay_link(link_id)
|
delete_pay_link(link_id)
|
||||||
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
@lnurlp_ext.route("/api/v1/rate/<currency>", methods=["GET"])
|
||||||
|
async def api_check_fiat_rate(currency):
|
||||||
|
try:
|
||||||
|
rate = await get_fiat_rate(currency)
|
||||||
|
except AssertionError:
|
||||||
|
rate = None
|
||||||
|
|
||||||
|
return jsonify({"rate": rate}), HTTPStatus.OK
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block scripts %} {{ window_vars(user) }}
|
%} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script type="text/javascript" src="/withdraw/static/js/index.js"></script>
|
<script src="/withdraw/static/js/index.js"></script>
|
||||||
{% endblock %} {% block page %}
|
{% endblock %} {% block page %}
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue