feat: update to lnbits 1.0.0 (#66)
--------- Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com> Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
This commit is contained in:
parent
3e006654ea
commit
c7623e4c5a
15 changed files with 1372 additions and 1278 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Pay Links",
|
"name": "Pay Links",
|
||||||
"short_description": "Make reusable LNURL pay links",
|
"short_description": "Make reusable LNURL pay links",
|
||||||
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
||||||
"min_lnbits_version": "0.12.4",
|
"min_lnbits_version": "1.0.0",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "arcbtc",
|
"name": "arcbtc",
|
||||||
|
|
|
||||||
139
crud.py
139
crud.py
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import insert_query, update_query, urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from .models import CreatePayLinkData, LnurlpSettings, PayLink
|
from .models import CreatePayLinkData, LnurlpSettings, PayLink
|
||||||
from .nostr.key import PrivateKey
|
from .nostr.key import PrivateKey
|
||||||
|
|
@ -10,22 +10,19 @@ db = Database("ext_lnurlp")
|
||||||
|
|
||||||
|
|
||||||
async def get_or_create_lnurlp_settings() -> LnurlpSettings:
|
async def get_or_create_lnurlp_settings() -> LnurlpSettings:
|
||||||
row = await db.fetchone("SELECT * FROM lnurlp.settings LIMIT 1")
|
settings = await db.fetchone(
|
||||||
if row:
|
"SELECT * FROM lnurlp.settings LIMIT 1", model=LnurlpSettings
|
||||||
return LnurlpSettings(**row)
|
)
|
||||||
|
if settings:
|
||||||
|
return settings
|
||||||
else:
|
else:
|
||||||
settings = LnurlpSettings(nostr_private_key=PrivateKey().hex())
|
settings = LnurlpSettings(nostr_private_key=PrivateKey().hex())
|
||||||
await db.execute(
|
await db.insert("lnurlp.settings", settings)
|
||||||
insert_query("lnurlp.settings", settings), (*settings.dict().values(),)
|
|
||||||
)
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings:
|
async def update_lnurlp_settings(settings: LnurlpSettings) -> LnurlpSettings:
|
||||||
await db.execute(
|
await db.update("lnurlp.settings", settings, "")
|
||||||
update_query("lnurlp.settings", settings, where=""),
|
|
||||||
(*settings.dict().values(),),
|
|
||||||
)
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,110 +31,74 @@ async def delete_lnurlp_settings() -> None:
|
||||||
|
|
||||||
|
|
||||||
async def get_pay_link_by_username(username: str) -> Optional[PayLink]:
|
async def get_pay_link_by_username(username: str) -> Optional[PayLink]:
|
||||||
row = await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,)
|
"SELECT * FROM lnurlp.pay_links WHERE username = :username",
|
||||||
|
{"username": username},
|
||||||
|
PayLink,
|
||||||
)
|
)
|
||||||
return PayLink.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def create_pay_link(data: CreatePayLinkData) -> PayLink:
|
async def create_pay_link(data: CreatePayLinkData) -> PayLink:
|
||||||
|
|
||||||
link_id = urlsafe_short_hash()[:6]
|
link_id = urlsafe_short_hash()[:6]
|
||||||
|
|
||||||
result = await db.execute(
|
assert data.wallet, "Wallet is required"
|
||||||
"""
|
|
||||||
INSERT INTO lnurlp.pay_links (
|
|
||||||
id,
|
|
||||||
wallet,
|
|
||||||
description,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
served_meta,
|
|
||||||
served_pr,
|
|
||||||
webhook_url,
|
|
||||||
webhook_headers,
|
|
||||||
webhook_body,
|
|
||||||
success_text,
|
|
||||||
success_url,
|
|
||||||
comment_chars,
|
|
||||||
currency,
|
|
||||||
fiat_base_multiplier,
|
|
||||||
username,
|
|
||||||
zaps
|
|
||||||
|
|
||||||
)
|
link = PayLink(
|
||||||
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
id=link_id,
|
||||||
""",
|
wallet=data.wallet,
|
||||||
(
|
description=data.description,
|
||||||
link_id,
|
min=data.min,
|
||||||
data.wallet,
|
max=data.max,
|
||||||
data.description,
|
served_meta=0,
|
||||||
data.min,
|
served_pr=0,
|
||||||
data.max,
|
username=data.username,
|
||||||
data.webhook_url,
|
zaps=data.zaps,
|
||||||
data.webhook_headers,
|
domain=None,
|
||||||
data.webhook_body,
|
webhook_url=data.webhook_url,
|
||||||
data.success_text,
|
webhook_headers=data.webhook_headers,
|
||||||
data.success_url,
|
webhook_body=data.webhook_body,
|
||||||
data.comment_chars,
|
success_text=data.success_text,
|
||||||
data.currency,
|
success_url=data.success_url,
|
||||||
data.fiat_base_multiplier,
|
currency=data.currency,
|
||||||
data.username,
|
comment_chars=data.comment_chars,
|
||||||
data.zaps,
|
fiat_base_multiplier=data.fiat_base_multiplier,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
assert result
|
|
||||||
|
|
||||||
link = await get_pay_link(link_id)
|
await db.insert("lnurlp.pay_links", link)
|
||||||
assert link, "Newly created link couldn't be retrieved"
|
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
|
||||||
async def get_address_data(username: str) -> Optional[PayLink]:
|
async def get_address_data(username: str) -> Optional[PayLink]:
|
||||||
row = await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,)
|
"SELECT * FROM lnurlp.pay_links WHERE username = :username",
|
||||||
|
{"username": username},
|
||||||
|
PayLink,
|
||||||
)
|
)
|
||||||
return PayLink.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_pay_link(link_id: str) -> Optional[PayLink]:
|
async def get_pay_link(link_id: str) -> Optional[PayLink]:
|
||||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
return await db.fetchone(
|
||||||
return PayLink.from_row(row) if row else None
|
"SELECT * FROM lnurlp.pay_links WHERE id = :id",
|
||||||
|
{"id": link_id},
|
||||||
|
PayLink,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
async 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]
|
||||||
|
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
|
||||||
q = ",".join(["?"] * len(wallet_ids))
|
return await db.fetchall(
|
||||||
rows = await db.fetchall(
|
f"SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q}) ORDER BY Id",
|
||||||
f"""
|
model=PayLink,
|
||||||
SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q})
|
|
||||||
ORDER BY Id
|
|
||||||
""",
|
|
||||||
(*wallet_ids,),
|
|
||||||
)
|
)
|
||||||
return [PayLink.from_row(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
async def update_pay_link(link: PayLink) -> PayLink:
|
||||||
|
await db.update("lnurlp.pay_links", link)
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
return link
|
||||||
await db.execute(
|
|
||||||
f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
|
|
||||||
)
|
|
||||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
|
||||||
return PayLink.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
|
||||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
|
||||||
await db.execute(
|
|
||||||
f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
|
|
||||||
)
|
|
||||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
|
||||||
return PayLink.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_pay_link(link_id: str) -> None:
|
async def delete_pay_link(link_id: str) -> None:
|
||||||
await db.execute("DELETE FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
await db.execute("DELETE FROM lnurlp.pay_links WHERE id = :id", {"id": link_id})
|
||||||
|
|
|
||||||
|
|
@ -174,3 +174,10 @@ async def m009_add_settings(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m010_add_pay_link_domain(db):
|
||||||
|
"""
|
||||||
|
Add domain to pay links
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN domain TEXT;")
|
||||||
|
|
|
||||||
12
models.py
12
models.py
|
|
@ -1,9 +1,7 @@
|
||||||
import json
|
import json
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Query, Request
|
||||||
from fastapi.param_functions import Query
|
|
||||||
from lnurl import encode as lnurl_encode
|
from lnurl import encode as lnurl_encode
|
||||||
from lnurl.types import LnurlPayMetadata
|
from lnurl.types import LnurlPayMetadata
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
@ -61,14 +59,6 @@ class PayLink(BaseModel):
|
||||||
max: float
|
max: float
|
||||||
fiat_base_multiplier: int
|
fiat_base_multiplier: int
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "PayLink":
|
|
||||||
data = dict(row)
|
|
||||||
if data["currency"] and data["fiat_base_multiplier"]:
|
|
||||||
data["min"] /= data["fiat_base_multiplier"]
|
|
||||||
data["max"] /= data["fiat_base_multiplier"]
|
|
||||||
return cls(**data)
|
|
||||||
|
|
||||||
def lnurl(self, req: Request) -> str:
|
def lnurl(self, req: Request) -> str:
|
||||||
url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id)
|
url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id)
|
||||||
url_str = str(url)
|
url_str = str(url)
|
||||||
|
|
|
||||||
2270
poetry.lock
generated
2270
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@ authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10 | ^3.9"
|
python = "^3.10 | ^3.9"
|
||||||
lnbits = "*"
|
lnbits = {version = "*", allow-prereleases = true}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^24.3.0"
|
black = "^24.3.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
||||||
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
const locationPath = [
|
const locationPath = [
|
||||||
window.location.protocol,
|
window.location.protocol,
|
||||||
'//',
|
'//',
|
||||||
|
|
@ -11,19 +9,17 @@ const locationPath = [
|
||||||
|
|
||||||
const mapPayLink = obj => {
|
const mapPayLink = obj => {
|
||||||
obj._data = _.clone(obj)
|
obj._data = _.clone(obj)
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = LNbits.utils.formatDate(obj.time)
|
||||||
new Date(obj.time * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||||
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
||||||
obj.pay_url = [locationPath, 'link/', obj.id].join('')
|
obj.pay_url = [locationPath, 'link/', obj.id].join('')
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
new Vue({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [window.windowMixin],
|
||||||
computed: {
|
computed: {
|
||||||
endpoint: function () {
|
endpoint: function () {
|
||||||
return `/lnurlp/api/v1/settings?usr=${this.g.user.id}`
|
return `/lnurlp/api/v1/settings?usr=${this.g.user.id}`
|
||||||
|
|
@ -240,7 +236,7 @@ new Vue({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.g.user.wallets.length) {
|
if (this.g.user.wallets?.length) {
|
||||||
var getPayLinks = this.getPayLinks
|
var getPayLinks = this.getPayLinks
|
||||||
getPayLinks()
|
getPayLinks()
|
||||||
this.checker = setInterval(() => {
|
this.checker = setInterval(() => {
|
||||||
|
|
|
||||||
35
tasks.py
35
tasks.py
|
|
@ -5,9 +5,8 @@ from threading import Thread
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from lnbits.core.crud import update_payment_extra
|
from lnbits.core.crud import get_payment, update_payment
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websocket import WebSocketApp
|
from websocket import WebSocketApp
|
||||||
|
|
@ -19,7 +18,7 @@ from .nostr.event import Event
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
invoice_queue = asyncio.Queue()
|
invoice_queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
register_invoice_listener(invoice_queue, "ext_lnurlp")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
payment = await invoice_queue.get()
|
payment = await invoice_queue.get()
|
||||||
|
|
@ -27,7 +26,8 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment):
|
async def on_invoice_paid(payment: Payment):
|
||||||
if payment.extra.get("tag") != "lnurlp":
|
|
||||||
|
if not payment.extra or payment.extra.get("tag") != "lnurlp":
|
||||||
return
|
return
|
||||||
|
|
||||||
if payment.extra.get("wh_status"):
|
if payment.extra.get("wh_status"):
|
||||||
|
|
@ -65,8 +65,10 @@ async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
|
||||||
"payment_hash": payment.payment_hash,
|
"payment_hash": payment.payment_hash,
|
||||||
"payment_request": payment.bolt11,
|
"payment_request": payment.bolt11,
|
||||||
"amount": payment.amount,
|
"amount": payment.amount,
|
||||||
"comment": payment.extra.get("comment"),
|
"comment": payment.extra.get("comment") if payment.extra else None,
|
||||||
"webhook_data": payment.extra.get("webhook_data") or "",
|
"webhook_data": (
|
||||||
|
payment.extra.get("webhook_data") if payment.extra else None
|
||||||
|
),
|
||||||
"lnurlp": pay_link.id,
|
"lnurlp": pay_link.id,
|
||||||
"body": (
|
"body": (
|
||||||
json.loads(pay_link.webhook_body)
|
json.loads(pay_link.webhook_body)
|
||||||
|
|
@ -80,7 +82,7 @@ async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
|
||||||
if pay_link.webhook_headers
|
if pay_link.webhook_headers
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
timeout=40,
|
timeout=6,
|
||||||
)
|
)
|
||||||
await mark_webhook_sent(
|
await mark_webhook_sent(
|
||||||
payment.payment_hash,
|
payment.payment_hash,
|
||||||
|
|
@ -99,20 +101,19 @@ async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
|
||||||
async def mark_webhook_sent(
|
async def mark_webhook_sent(
|
||||||
payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
|
payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
|
||||||
) -> None:
|
) -> None:
|
||||||
await update_payment_extra(
|
payment = await get_payment(payment_hash)
|
||||||
payment_hash,
|
extra = payment.extra or {}
|
||||||
{
|
extra["wh_status"] = status # keep for backwards compability
|
||||||
"wh_status": status, # keep for backwards compability
|
extra["wh_success"] = is_success
|
||||||
"wh_success": is_success,
|
extra["wh_message"] = reason_phrase
|
||||||
"wh_message": reason_phrase,
|
extra["wh_response"] = text
|
||||||
"wh_response": text,
|
payment.extra = extra
|
||||||
},
|
await update_payment(payment)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# NIP-57 - load the zap request
|
# NIP-57 - load the zap request
|
||||||
async def send_zap(payment: Payment):
|
async def send_zap(payment: Payment):
|
||||||
nostr = payment.extra.get("nostr")
|
nostr = payment.extra.get("nostr") if payment.extra else None
|
||||||
if not nostr:
|
if not nostr:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key:
|
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key:
|
||||||
{{ user.wallets[0].inkey }}"
|
<span v-text="g.user.wallets[0].inkey"></span> "
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
||||||
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
|
-H "X-Api-Key: <span v-text="g.user.wallets[0].inkey"></span>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
'{"description": <string>, "amount": <integer>, "max":
|
'{"description": <string>, "amount": <integer>, "max":
|
||||||
<integer>, "min": <integer>, "comment_chars":
|
<integer>, "min": <integer>, "comment_chars":
|
||||||
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||||
{{ user.wallets[0].adminkey }}"
|
<span v-text="g.user.wallets[0].adminkey"></span>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -103,8 +103,8 @@
|
||||||
<code
|
<code
|
||||||
>curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
>curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
||||||
-d '{"description": <string>, "amount": <integer>}' -H
|
-d '{"description": <string>, "amount": <integer>}' -H
|
||||||
"Content-type: application/json" -H "X-Api-Key: {{
|
"Content-type: application/json" -H "X-Api-Key:
|
||||||
user.wallets[0].adminkey }}"
|
<span v-text="g.user.wallets[0].adminkey"></span>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -129,8 +129,8 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.base_url
|
>curl -X DELETE {{ request.base_url
|
||||||
}}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key: {{
|
}}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key:
|
||||||
user.wallets[0].adminkey }}"
|
<span v-text="g.user.wallets[0].adminkey"></span>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,7 @@
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<lnbits-qrcode value="lightning:{{ lnurl }}"></lnbits-qrcode>
|
||||||
<qrcode
|
|
||||||
value="lightning:{{ lnurl }}"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
|
|
@ -44,11 +38,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %}
|
{% endblock %} {% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
window.app = Vue.createApp({
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin]
|
mixins: [window.windowMixin]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,10 @@
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
:data="payLinks"
|
:rows="payLinks"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:pagination.sync="payLinksTable.pagination"
|
v-model:pagination="payLinksTable.pagination"
|
||||||
>
|
>
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr class="text-left" :props="props">
|
<q-tr class="text-left" :props="props">
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
|
|
@ -53,6 +52,7 @@
|
||||||
type="a"
|
type="a"
|
||||||
:href="props.row.pay_url"
|
:href="props.row.pay_url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
class="q-ml-sm"
|
||||||
><q-tooltip>Shareable Page</q-tooltip></q-btn
|
><q-tooltip>Shareable Page</q-tooltip></q-btn
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -61,26 +61,33 @@
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="visibility"
|
icon="visibility"
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
class="q-ml-sm"
|
||||||
@click="openQrCodeDialog(props.row.id)"
|
@click="openQrCodeDialog(props.row.id)"
|
||||||
><q-tooltip>View Link</q-tooltip></q-btn
|
><q-tooltip>View Link</q-tooltip></q-btn
|
||||||
>
|
>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width>{{ props.row.description }}</q-td>
|
<q-td auto-width v-text="props.row.description"></q-td>
|
||||||
<q-td auto-width>
|
<q-td auto-width>
|
||||||
<span v-if="props.row.min == props.row.max">
|
<span
|
||||||
{{ props.row.min }}
|
v-if="props.row.min == props.row.max"
|
||||||
</span>
|
v-text="props.row.min"
|
||||||
<span v-else>{{ props.row.min }} - {{ props.row.max }}</span>
|
></span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
v-text="props.row.min + ' - ' + props.row.max"
|
||||||
|
></span>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>{{ props.row.currency || 'sat' }}</q-td>
|
<q-td v-text="props.row.currency || 'sat'"></q-td>
|
||||||
<q-td
|
<q-td
|
||||||
auto-width
|
auto-width
|
||||||
:class="(props.row.username) ? 'text-normal' : 'text-grey'"
|
:class="(props.row.username) ? 'text-normal' : 'text-grey'"
|
||||||
>{{ props.row.username || 'None' }}</q-td
|
v-text="props.row.username || 'None'"
|
||||||
>
|
></q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
<q-icon v-if="props.row.webhook_url" size="14px" name="http">
|
<q-icon v-if="props.row.webhook_url" size="14px" name="http">
|
||||||
<q-tooltip>Webhook to {{ props.row.webhook_url }}</q-tooltip>
|
<q-tooltip
|
||||||
|
>Webhook to <span v-text="props.row.webhook_url"></span
|
||||||
|
></q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.row.success_text || props.row.success_url"
|
v-if="props.row.success_text || props.row.success_url"
|
||||||
|
|
@ -88,9 +95,13 @@
|
||||||
name="call_to_action"
|
name="call_to_action"
|
||||||
>
|
>
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
On success, show message '{{ props.row.success_text }}'
|
On success, show message '<span
|
||||||
|
v-text="props.row.success_text"
|
||||||
|
></span
|
||||||
|
>'
|
||||||
<span v-if="props.row.success_url"
|
<span v-if="props.row.success_url"
|
||||||
>and URL '{{ props.row.success_url }}'</span
|
>and URL '<span v-text="props.row.success_url"></span
|
||||||
|
>'</span
|
||||||
>
|
>
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
|
|
@ -100,7 +111,8 @@
|
||||||
name="insert_comment"
|
name="insert_comment"
|
||||||
>
|
>
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
{{ props.row.comment_chars }}-char comment allowed
|
<span v-text="props.row.comment_chars"></span>-char comment
|
||||||
|
allowed
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
@ -127,7 +139,6 @@
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
</q-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -184,7 +195,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="margin-top: 10px">
|
<div class="col" style="margin-top: 10px">
|
||||||
<span class="label">
|
<span class="label">
|
||||||
@ {% raw %} {{ domain }} {% endraw %}
|
@ <span v-text="domain"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -357,34 +368,37 @@
|
||||||
|
|
||||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||||
{% raw %}
|
<lnbits-qrcode
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
:value="'lightning:' + qrCodeDialog.data.lnurl"
|
||||||
<qrcode
|
></lnbits-qrcode>
|
||||||
:value="'lightning:' + qrCodeDialog.data.lnurl"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
>
|
|
||||||
</qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
<p style="word-break: break-all">
|
<p style="word-break: break-all">
|
||||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
<strong>ID:</strong> <span v-text="qrCodeDialog.data.id"></span><br />
|
||||||
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }}<br />
|
<strong>Amount:</strong> <span v-text="qrCodeDialog.data.amount"></span
|
||||||
|
><br />
|
||||||
|
|
||||||
<span v-if="qrCodeDialog.data.currency"
|
<span v-if="qrCodeDialog.data.currency"
|
||||||
><strong>{{ qrCodeDialog.data.currency }} price:</strong> {{
|
><strong
|
||||||
fiatRates[qrCodeDialog.data.currency] ?
|
><span v-text="qrCodeDialog.data.currency"></span> price:</strong
|
||||||
fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}<br
|
>
|
||||||
|
<span
|
||||||
|
v-if="fiatRates[qrCodeDialog.data.currency]"
|
||||||
|
v-text="fiatRates[qrCodeDialog.data.currency] + 'sat'"
|
||||||
|
></span>
|
||||||
|
<span v-else>Loading...</span>
|
||||||
|
<br
|
||||||
/></span>
|
/></span>
|
||||||
<strong>Accepts comments:</strong> {{ qrCodeDialog.data.comments }}<br />
|
<strong>Accepts comments:</strong>
|
||||||
<strong>Dispatches webhook to:</strong> {{ qrCodeDialog.data.webhook
|
<span v-text="qrCodeDialog.data.comments"></span><br />
|
||||||
}}<br />
|
<strong>Dispatches webhook to:</strong>
|
||||||
<strong>On success:</strong> {{ qrCodeDialog.data.success }}<br />
|
<span v-text="qrCodeDialog.data.webhook"></span><br />
|
||||||
|
<strong>On success:</strong>
|
||||||
|
<span v-text="qrCodeDialog.data.success"></span><br />
|
||||||
<span v-if="qrCodeDialog.data.username">
|
<span v-if="qrCodeDialog.data.username">
|
||||||
<strong>Lightning Address: </strong>
|
<strong>Lightning Address: </strong>
|
||||||
{{ qrCodeDialog.data.username }}@{{ domain }}
|
<span v-text="qrCodeDialog.data.username+'@'+domain"></span>
|
||||||
<br />
|
<br />
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
{% endraw %}
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "print.html" %} {% block page %}
|
{% extends "print.html" %} {% block page %}
|
||||||
<div class="row justify-center">
|
<div class="row justify-center">
|
||||||
<div class="qr">
|
<div class="qr">
|
||||||
<qrcode value="lightning:{{ lnurl }}" :options="{width}"></qrcode>
|
<lnbits-qrcode value="lightning:{{ lnurl }}"></lnbits-qrcode>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block styles %}
|
{% endblock %} {% block styles %}
|
||||||
|
|
@ -12,14 +12,12 @@
|
||||||
</style>
|
</style>
|
||||||
{% endblock %} {% block scripts %}
|
{% endblock %} {% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
window.app = Vue.createApp({
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
created: function () {
|
created() {
|
||||||
window.print()
|
window.print()
|
||||||
},
|
},
|
||||||
data: function () {
|
data() {
|
||||||
return {width: window.innerWidth * 0.5}
|
return {width: window.innerWidth * 0.5}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
6
views.py
6
views.py
|
|
@ -1,7 +1,6 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
|
@ -17,13 +16,10 @@ def lnurlp_renderer():
|
||||||
return template_renderer(["lnurlp/templates"])
|
return template_renderer(["lnurlp/templates"])
|
||||||
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_generic_router.get("/", response_class=HTMLResponse)
|
@lnurlp_generic_router.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return lnurlp_renderer().TemplateResponse(
|
return lnurlp_renderer().TemplateResponse(
|
||||||
"lnurlp/index.html", {"request": request, "user": user.dict()}
|
"lnurlp/index.html", {"request": request, "user": user.json()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
20
views_api.py
20
views_api.py
|
|
@ -8,7 +8,6 @@ from lnbits.core.crud import get_user, get_wallet
|
||||||
from lnbits.core.models import WalletTypeInfo
|
from lnbits.core.models import WalletTypeInfo
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_admin,
|
check_admin,
|
||||||
get_key_type,
|
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
|
|
@ -41,13 +40,13 @@ async def api_list_currencies_available():
|
||||||
@lnurlp_api_router.get("/api/v1/links", status_code=HTTPStatus.OK)
|
@lnurlp_api_router.get("/api/v1/links", status_code=HTTPStatus.OK)
|
||||||
async def api_links(
|
async def api_links(
|
||||||
req: Request,
|
req: Request,
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
all_wallets: bool = Query(False),
|
all_wallets: bool = Query(False),
|
||||||
):
|
):
|
||||||
wallet_ids = [wallet.wallet.id]
|
wallet_ids = [key_info.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
user = await get_user(wallet.wallet.user)
|
user = await get_user(key_info.wallet.user)
|
||||||
wallet_ids = user.wallet_ids if user else []
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -189,7 +188,10 @@ async def api_link_create_or_update(
|
||||||
if data.username and data.username != link.username:
|
if data.username and data.username != link.username:
|
||||||
await check_username_exists(data.username)
|
await check_username_exists(data.username)
|
||||||
|
|
||||||
link = await update_pay_link(**data.dict(), link_id=link_id)
|
for k, v in data.dict().items():
|
||||||
|
setattr(link, k, v)
|
||||||
|
|
||||||
|
link = await update_pay_link(link)
|
||||||
else:
|
else:
|
||||||
if data.username:
|
if data.username:
|
||||||
await check_username_exists(data.username)
|
await check_username_exists(data.username)
|
||||||
|
|
@ -201,7 +203,9 @@ async def api_link_create_or_update(
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_api_router.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
@lnurlp_api_router.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_link_delete(link_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_link_delete(
|
||||||
|
link_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
link = await get_pay_link(link_id)
|
link = await get_pay_link(link_id)
|
||||||
|
|
||||||
if not link:
|
if not link:
|
||||||
|
|
@ -210,9 +214,9 @@ async def api_link_delete(link_id: str, wallet: WalletTypeInfo = Depends(get_key
|
||||||
)
|
)
|
||||||
|
|
||||||
# admins are allowed to delete paylinks beloging to regular users
|
# admins are allowed to delete paylinks beloging to regular users
|
||||||
user = await get_user(wallet.wallet.user)
|
user = await get_user(key_info.wallet.user)
|
||||||
admin_user = user.admin if user else False
|
admin_user = user.admin if user else False
|
||||||
if not admin_user and link.wallet != wallet.wallet.id:
|
if not admin_user and link.wallet != key_info.wallet.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ from pydantic import parse_obj_as
|
||||||
from .crud import (
|
from .crud import (
|
||||||
get_address_data,
|
get_address_data,
|
||||||
get_or_create_lnurlp_settings,
|
get_or_create_lnurlp_settings,
|
||||||
increment_pay_link,
|
get_pay_link,
|
||||||
|
update_pay_link,
|
||||||
)
|
)
|
||||||
|
|
||||||
lnurlp_lnurl_router = APIRouter()
|
lnurlp_lnurl_router = APIRouter()
|
||||||
|
|
@ -36,11 +37,13 @@ async def api_lnurl_callback(
|
||||||
amount: int = Query(...),
|
amount: int = Query(...),
|
||||||
webhook_data: str = Query(None),
|
webhook_data: str = Query(None),
|
||||||
):
|
):
|
||||||
link = await increment_pay_link(link_id, served_pr=1)
|
link = await get_pay_link(link_id)
|
||||||
if not link:
|
if not link:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
||||||
)
|
)
|
||||||
|
link.served_pr = 1
|
||||||
|
await update_pay_link(link)
|
||||||
mininum = link.min
|
mininum = link.min
|
||||||
maximum = link.max
|
maximum = link.max
|
||||||
|
|
||||||
|
|
@ -100,7 +103,7 @@ async def api_lnurl_callback(
|
||||||
# we take the zap request as the description instead of the metadata if present
|
# we take the zap request as the description instead of the metadata if present
|
||||||
unhashed_description = nostr.encode() if nostr else link.lnurlpay_metadata.encode()
|
unhashed_description = nostr.encode() if nostr else link.lnurlpay_metadata.encode()
|
||||||
|
|
||||||
_, payment_request = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=link.wallet,
|
wallet_id=link.wallet,
|
||||||
amount=int(amount / 1000),
|
amount=int(amount / 1000),
|
||||||
memo=link.description,
|
memo=link.description,
|
||||||
|
|
@ -120,7 +123,7 @@ async def api_lnurl_callback(
|
||||||
message = parse_obj_as(Max144Str, link.success_text)
|
message = parse_obj_as(Max144Str, link.success_text)
|
||||||
action = MessageAction(message=message)
|
action = MessageAction(message=message)
|
||||||
|
|
||||||
invoice = parse_obj_as(LightningInvoice, LightningInvoice(payment_request))
|
invoice = parse_obj_as(LightningInvoice, LightningInvoice(payment.bolt11))
|
||||||
resp = LnurlPayActionResponse(pr=invoice, successAction=action, routes=[])
|
resp = LnurlPayActionResponse(pr=invoice, successAction=action, routes=[])
|
||||||
return resp.dict()
|
return resp.dict()
|
||||||
|
|
||||||
|
|
@ -136,13 +139,15 @@ async def api_lnurl_callback(
|
||||||
name="lnurlp.api_lnurl_response",
|
name="lnurlp.api_lnurl_response",
|
||||||
)
|
)
|
||||||
async def api_lnurl_response(
|
async def api_lnurl_response(
|
||||||
request: Request, link_id, webhook_data: Optional[str] = Query(None)
|
request: Request, link_id: str, webhook_data: Optional[str] = Query(None)
|
||||||
):
|
):
|
||||||
link = await increment_pay_link(link_id, served_meta=1)
|
link = await get_pay_link(link_id)
|
||||||
if not link:
|
if not link:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
||||||
)
|
)
|
||||||
|
link.served_meta = 1
|
||||||
|
await update_pay_link(link)
|
||||||
|
|
||||||
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
|
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
|
||||||
url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)
|
url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue