mirror of
https://github.com/lnbits/lnbits.git
synced 2025-06-03 03:31:47 +02:00
Removed lnurlp (#1511)
* Removed lnurlp
* comment out extension checks for lnurlp
---------
Co-authored-by: dni ⚡ <office@dnilabs.com>
This commit is contained in:
parent
b5be96836c
commit
1026b242ba
@ -1,27 +0,0 @@
|
|||||||
# LNURLp
|
|
||||||
|
|
||||||
## Create a static QR code people can use to pay over Lightning Network
|
|
||||||
|
|
||||||
LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL-pay is a link that wallets use to fetch an invoice from a server on-demand. The link or QR code is fixed, but each time it is read by a compatible wallet a new invoice is issued by the service and sent to the wallet.
|
|
||||||
|
|
||||||
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Create an LNURLp (New Pay link)\
|
|
||||||

|
|
||||||
|
|
||||||
- select your wallets
|
|
||||||
- make a small description
|
|
||||||
- enter amount
|
|
||||||
- if _Fixed amount_ is unchecked you'll have the option to configure a Max and Min amount
|
|
||||||
- you can set the currency to something different than sats. For example if you choose EUR, the satoshi amount will be calculated when a user scans the LNURLp
|
|
||||||
- You can ask the user to send a comment that will be sent along with the payment (for example a comment to a blog post)
|
|
||||||
- Webhook URL allows to call an URL when the LNURLp is paid
|
|
||||||
- Success mesage, will send a message back to the user after a successful payment, for example a thank you note
|
|
||||||
- Success URL, will send back a clickable link to the user. Access to some hidden content, or a download link
|
|
||||||
|
|
||||||
2. Use the shareable link or view the LNURLp you just created\
|
|
||||||

|
|
||||||
- you can now open your LNURLp and copy the LNURL, get the shareable link or print it\
|
|
||||||

|
|
@ -1,38 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
|
||||||
|
|
||||||
db = Database("ext_lnurlp")
|
|
||||||
|
|
||||||
lnurlp_static_files = [
|
|
||||||
{
|
|
||||||
"path": "/lnurlp/static",
|
|
||||||
"app": StaticFiles(packages=[("lnbits", "extensions/lnurlp/static")]),
|
|
||||||
"name": "lnurlp_static",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
scheduled_tasks: List[asyncio.Task] = []
|
|
||||||
|
|
||||||
lnurlp_ext: APIRouter = APIRouter(prefix="/lnurlp", tags=["lnurlp"])
|
|
||||||
|
|
||||||
|
|
||||||
def lnurlp_renderer():
|
|
||||||
return template_renderer(["lnbits/extensions/lnurlp/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
from .lnurl import * # noqa: F401,F403
|
|
||||||
from .tasks import wait_for_paid_invoices
|
|
||||||
from .views import * # noqa: F401,F403
|
|
||||||
from .views_api import * # noqa: F401,F403
|
|
||||||
|
|
||||||
|
|
||||||
def lnurlp_start():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
task = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
|
||||||
scheduled_tasks.append(task)
|
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "LNURLp",
|
|
||||||
"short_description": "Make reusable LNURL pay links",
|
|
||||||
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
|
||||||
"contributors": [
|
|
||||||
"arcbtc",
|
|
||||||
"eillarra",
|
|
||||||
"fiatjaf"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import CreatePayLinkData, PayLink
|
|
||||||
|
|
||||||
|
|
||||||
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|
||||||
link_id = urlsafe_short_hash()[:6]
|
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
link_id,
|
|
||||||
wallet_id,
|
|
||||||
data.description,
|
|
||||||
data.min,
|
|
||||||
data.max,
|
|
||||||
data.webhook_url,
|
|
||||||
data.webhook_headers,
|
|
||||||
data.webhook_body,
|
|
||||||
data.success_text,
|
|
||||||
data.success_url,
|
|
||||||
data.comment_chars,
|
|
||||||
data.currency,
|
|
||||||
data.fiat_base_multiplier,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
assert result
|
|
||||||
|
|
||||||
link = await get_pay_link(link_id)
|
|
||||||
assert link, "Newly created link couldn't be retrieved"
|
|
||||||
return link
|
|
||||||
|
|
||||||
|
|
||||||
async def get_pay_link(link_id: str) -> Optional[PayLink]:
|
|
||||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
|
||||||
return PayLink.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
|
||||||
if isinstance(wallet_ids, str):
|
|
||||||
wallet_ids = [wallet_ids]
|
|
||||||
|
|
||||||
q = ",".join(["?"] * len(wallet_ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"""
|
|
||||||
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: int, **kwargs) -> Optional[PayLink]:
|
|
||||||
q = ", ".join([f"{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 increment_pay_link(link_id: int, **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: int) -> None:
|
|
||||||
await db.execute("DELETE FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
|
@ -1,106 +0,0 @@
|
|||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Request
|
|
||||||
from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
|
||||||
from lnbits.utils.exchange_rates import get_fiat_rate_satoshis
|
|
||||||
|
|
||||||
from . import lnurlp_ext
|
|
||||||
from .crud import increment_pay_link
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get(
|
|
||||||
"/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL)
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
name="lnurlp.api_lnurl_response.deprecated",
|
|
||||||
)
|
|
||||||
@lnurlp_ext.get(
|
|
||||||
"/{link_id}",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
name="lnurlp.api_lnurl_response",
|
|
||||||
)
|
|
||||||
async def api_lnurl_response(request: Request, link_id):
|
|
||||||
link = await increment_pay_link(link_id, served_meta=1)
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
|
|
||||||
|
|
||||||
resp = LnurlPayResponse(
|
|
||||||
callback=request.url_for("lnurlp.api_lnurl_callback", link_id=link.id),
|
|
||||||
min_sendable=round(link.min * rate) * 1000,
|
|
||||||
max_sendable=round(link.max * rate) * 1000,
|
|
||||||
metadata=link.lnurlpay_metadata,
|
|
||||||
)
|
|
||||||
params = resp.dict()
|
|
||||||
|
|
||||||
if link.comment_chars > 0:
|
|
||||||
params["commentAllowed"] = link.comment_chars
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get(
|
|
||||||
"/api/v1/lnurl/cb/{link_id}",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
name="lnurlp.api_lnurl_callback",
|
|
||||||
)
|
|
||||||
async def api_lnurl_callback(request: Request, link_id):
|
|
||||||
link = await increment_pay_link(link_id, served_pr=1)
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
|
||||||
)
|
|
||||||
min, max = link.min, link.max
|
|
||||||
rate = await get_fiat_rate_satoshis(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
|
|
||||||
else:
|
|
||||||
min = link.min * 1000
|
|
||||||
max = link.max * 1000
|
|
||||||
|
|
||||||
amount_received = int(request.query_params.get("amount") or 0)
|
|
||||||
if amount_received < min:
|
|
||||||
return LnurlErrorResponse(
|
|
||||||
reason=f"Amount {amount_received} is smaller than minimum {min}."
|
|
||||||
).dict()
|
|
||||||
|
|
||||||
elif amount_received > max:
|
|
||||||
return LnurlErrorResponse(
|
|
||||||
reason=f"Amount {amount_received} is greater than maximum {max}."
|
|
||||||
).dict()
|
|
||||||
|
|
||||||
comment = request.query_params.get("comment")
|
|
||||||
if len(comment or "") > link.comment_chars:
|
|
||||||
return LnurlErrorResponse(
|
|
||||||
reason=f"Got a comment with {len(comment)} characters, but can only accept {link.comment_chars}"
|
|
||||||
).dict()
|
|
||||||
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
|
||||||
wallet_id=link.wallet,
|
|
||||||
amount=int(amount_received / 1000),
|
|
||||||
memo=link.description,
|
|
||||||
unhashed_description=link.lnurlpay_metadata.encode(),
|
|
||||||
extra={
|
|
||||||
"tag": "lnurlp",
|
|
||||||
"link": link.id,
|
|
||||||
"comment": comment,
|
|
||||||
"extra": request.query_params.get("amount"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
success_action = link.success_action(payment_hash)
|
|
||||||
if success_action:
|
|
||||||
resp = LnurlPayActionResponse(
|
|
||||||
pr=payment_request, success_action=success_action, routes=[]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
resp = LnurlPayActionResponse(pr=payment_request, routes=[])
|
|
||||||
|
|
||||||
return resp.dict()
|
|
@ -1,148 +0,0 @@
|
|||||||
async def m001_initial(db):
|
|
||||||
"""
|
|
||||||
Initial pay table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE lnurlp.pay_links (
|
|
||||||
id {db.serial_primary_key},
|
|
||||||
wallet TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
amount {db.big_int} NOT NULL,
|
|
||||||
served_meta INTEGER NOT NULL,
|
|
||||||
served_pr INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_webhooks_and_success_actions(db):
|
|
||||||
"""
|
|
||||||
Webhooks and success actions.
|
|
||||||
"""
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_url TEXT;")
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_text TEXT;")
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_url TEXT;")
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE lnurlp.invoices (
|
|
||||||
pay_link INTEGER NOT NULL REFERENCES {db.references_schema}pay_links (id),
|
|
||||||
payment_hash TEXT NOT NULL,
|
|
||||||
webhook_sent INT, -- null means not sent, otherwise store status
|
|
||||||
expiry INT
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN currency TEXT;"
|
|
||||||
) # null = satoshis
|
|
||||||
await db.execute(
|
|
||||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;"
|
|
||||||
)
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links RENAME COLUMN amount TO min;")
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN max INTEGER;")
|
|
||||||
await db.execute("UPDATE lnurlp.pay_links SET max = min;")
|
|
||||||
await db.execute("DROP TABLE lnurlp.invoices")
|
|
||||||
|
|
||||||
|
|
||||||
async def m004_fiat_base_multiplier(db):
|
|
||||||
"""
|
|
||||||
Store the multiplier for fiat prices. We store the price in cents and
|
|
||||||
remember to multiply by 100 when we use it to convert to Dollars.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m005_webhook_headers_and_body(db):
|
|
||||||
"""
|
|
||||||
Add headers and body to webhooks
|
|
||||||
"""
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
|
|
||||||
|
|
||||||
|
|
||||||
async def m006_redux(db):
|
|
||||||
"""
|
|
||||||
Migrate ID column type to string for UUIDs and migrate existing data
|
|
||||||
"""
|
|
||||||
# we can simply change the column type for postgres
|
|
||||||
if db.type != "SQLITE":
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ALTER COLUMN id TYPE TEXT;")
|
|
||||||
else:
|
|
||||||
# but we have to do this for sqlite
|
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE lnurlp.pay_links (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
wallet TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
min {db.big_int} NOT NULL,
|
|
||||||
max {db.big_int},
|
|
||||||
currency TEXT,
|
|
||||||
fiat_base_multiplier INTEGER DEFAULT 1,
|
|
||||||
served_meta INTEGER NOT NULL,
|
|
||||||
served_pr INTEGER NOT NULL,
|
|
||||||
webhook_url TEXT,
|
|
||||||
success_text TEXT,
|
|
||||||
success_url TEXT,
|
|
||||||
comment_chars INTEGER DEFAULT 0,
|
|
||||||
webhook_headers TEXT,
|
|
||||||
webhook_body TEXT
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
for row in [
|
|
||||||
list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
|
|
||||||
]:
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO lnurlp.pay_links (
|
|
||||||
id,
|
|
||||||
wallet,
|
|
||||||
description,
|
|
||||||
min,
|
|
||||||
served_meta,
|
|
||||||
served_pr,
|
|
||||||
webhook_url,
|
|
||||||
success_text,
|
|
||||||
success_url,
|
|
||||||
currency,
|
|
||||||
comment_chars,
|
|
||||||
max,
|
|
||||||
fiat_base_multiplier,
|
|
||||||
webhook_headers,
|
|
||||||
webhook_body
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
row[0],
|
|
||||||
row[1],
|
|
||||||
row[2],
|
|
||||||
row[3],
|
|
||||||
row[4],
|
|
||||||
row[5],
|
|
||||||
row[6],
|
|
||||||
row[7],
|
|
||||||
row[8],
|
|
||||||
row[9],
|
|
||||||
row[10],
|
|
||||||
row[11],
|
|
||||||
row[12],
|
|
||||||
row[13],
|
|
||||||
row[14],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute("DROP TABLE lnurlp.pay_links_old")
|
|
@ -1,75 +0,0 @@
|
|||||||
import json
|
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import Dict, Optional
|
|
||||||
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
|
||||||
from lnurl.types import LnurlPayMetadata
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from starlette.requests import Request
|
|
||||||
|
|
||||||
from lnbits.lnurl import encode as lnurl_encode
|
|
||||||
|
|
||||||
|
|
||||||
class CreatePayLinkData(BaseModel):
|
|
||||||
description: str
|
|
||||||
min: float = Query(1, ge=0.01)
|
|
||||||
max: float = Query(1, ge=0.01)
|
|
||||||
currency: str = Query(None)
|
|
||||||
comment_chars: int = Query(0, ge=0, lt=800)
|
|
||||||
webhook_url: str = Query(None)
|
|
||||||
webhook_headers: str = Query(None)
|
|
||||||
webhook_body: str = Query(None)
|
|
||||||
success_text: str = Query(None)
|
|
||||||
success_url: str = Query(None)
|
|
||||||
fiat_base_multiplier: int = Query(100, ge=1)
|
|
||||||
|
|
||||||
|
|
||||||
class PayLink(BaseModel):
|
|
||||||
id: str
|
|
||||||
wallet: str
|
|
||||||
description: str
|
|
||||||
min: float
|
|
||||||
served_meta: int
|
|
||||||
served_pr: int
|
|
||||||
webhook_url: Optional[str]
|
|
||||||
webhook_headers: Optional[str]
|
|
||||||
webhook_body: Optional[str]
|
|
||||||
success_text: Optional[str]
|
|
||||||
success_url: Optional[str]
|
|
||||||
currency: Optional[str]
|
|
||||||
comment_chars: int
|
|
||||||
max: float
|
|
||||||
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:
|
|
||||||
url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id)
|
|
||||||
return lnurl_encode(url)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
|
||||||
return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
|
|
||||||
|
|
||||||
def success_action(self, payment_hash: str) -> Optional[Dict]:
|
|
||||||
if self.success_url:
|
|
||||||
url: ParseResult = urlparse(self.success_url)
|
|
||||||
# qs = parse_qs(url.query)
|
|
||||||
# setattr(qs, "payment_hash", payment_hash)
|
|
||||||
# url = url._replace(query=urlencode(qs, doseq=True))
|
|
||||||
return {
|
|
||||||
"tag": "url",
|
|
||||||
"description": self.success_text or "~",
|
|
||||||
"url": urlunparse(url),
|
|
||||||
}
|
|
||||||
elif self.success_text:
|
|
||||||
return {"tag": "message", "message": self.success_text}
|
|
||||||
else:
|
|
||||||
return None
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@ -1,264 +0,0 @@
|
|||||||
/* 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, 'link/', obj.id].join('')
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currencies: [],
|
|
||||||
fiatRates: {},
|
|
||||||
checker: null,
|
|
||||||
payLinks: [],
|
|
||||||
payLinksTable: {
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nfcTagWriting: false,
|
|
||||||
formDialog: {
|
|
||||||
show: false,
|
|
||||||
fixedAmount: true,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
qrCodeDialog: {
|
|
||||||
show: false,
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getPayLinks() {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/lnurlp/api/v1/links?all_wallets=true',
|
|
||||||
this.g.user.wallets[0].inkey
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
this.payLinks = response.data.map(mapPayLink)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
clearInterval(this.checker)
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeFormDialog() {
|
|
||||||
this.resetFormData()
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetFormData() {
|
|
||||||
this.formDialog = {
|
|
||||||
show: false,
|
|
||||||
fixedAmount: true,
|
|
||||||
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
|
|
||||||
this.resetFormData()
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createPayLink(wallet, data) {
|
|
||||||
LNbits.api
|
|
||||||
.request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
|
|
||||||
.then(response => {
|
|
||||||
this.getPayLinks()
|
|
||||||
this.formDialog.show = false
|
|
||||||
this.resetFormData()
|
|
||||||
})
|
|
||||||
.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)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
writeNfcTag: async function (lnurl) {
|
|
||||||
try {
|
|
||||||
if (typeof NDEFReader == 'undefined') {
|
|
||||||
throw {
|
|
||||||
toString: function () {
|
|
||||||
return 'NFC not supported on this device or browser.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ndef = new NDEFReader()
|
|
||||||
|
|
||||||
this.nfcTagWriting = true
|
|
||||||
this.$q.notify({
|
|
||||||
message: 'Tap your NFC tag to write the LNURL-pay link to it.'
|
|
||||||
})
|
|
||||||
|
|
||||||
await ndef.write({
|
|
||||||
records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}]
|
|
||||||
})
|
|
||||||
|
|
||||||
this.nfcTagWriting = false
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'NFC tag written successfully.'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
this.nfcTagWriting = false
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'negative',
|
|
||||||
message: error
|
|
||||||
? error.toString()
|
|
||||||
: 'An unexpected error has occurred.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if (this.g.user.wallets.length) {
|
|
||||||
var getPayLinks = this.getPayLinks
|
|
||||||
getPayLinks()
|
|
||||||
this.checker = setInterval(() => {
|
|
||||||
getPayLinks()
|
|
||||||
}, 20000)
|
|
||||||
}
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/lnurlp/api/v1/currencies')
|
|
||||||
.then(response => {
|
|
||||||
this.currencies = ['satoshis', ...response.data]
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,79 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.crud import update_payment_extra
|
|
||||||
from lnbits.core.models import Payment
|
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
|
||||||
|
|
||||||
from .crud import get_pay_link
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
|
||||||
invoice_queue = asyncio.Queue()
|
|
||||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
|
||||||
|
|
||||||
while True:
|
|
||||||
payment = await invoice_queue.get()
|
|
||||||
await on_invoice_paid(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment):
|
|
||||||
if payment.extra.get("tag") != "lnurlp":
|
|
||||||
return
|
|
||||||
|
|
||||||
if payment.extra.get("wh_status"):
|
|
||||||
# this webhook has already been sent
|
|
||||||
return
|
|
||||||
|
|
||||||
pay_link = await get_pay_link(payment.extra.get("link", -1))
|
|
||||||
if pay_link and pay_link.webhook_url:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
r: httpx.Response = await client.post(
|
|
||||||
pay_link.webhook_url,
|
|
||||||
json={
|
|
||||||
"payment_hash": payment.payment_hash,
|
|
||||||
"payment_request": payment.bolt11,
|
|
||||||
"amount": payment.amount,
|
|
||||||
"comment": payment.extra.get("comment"),
|
|
||||||
"lnurlp": pay_link.id,
|
|
||||||
"body": json.loads(pay_link.webhook_body)
|
|
||||||
if pay_link.webhook_body
|
|
||||||
else "",
|
|
||||||
},
|
|
||||||
headers=json.loads(pay_link.webhook_headers)
|
|
||||||
if pay_link.webhook_headers
|
|
||||||
else None,
|
|
||||||
timeout=40,
|
|
||||||
)
|
|
||||||
await mark_webhook_sent(
|
|
||||||
payment.payment_hash,
|
|
||||||
r.status_code,
|
|
||||||
r.is_success,
|
|
||||||
r.reason_phrase,
|
|
||||||
r.text,
|
|
||||||
)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error(ex)
|
|
||||||
await mark_webhook_sent(
|
|
||||||
payment.payment_hash, -1, False, "Unexpected Error", str(ex)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(
|
|
||||||
payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
await update_payment_extra(
|
|
||||||
payment_hash,
|
|
||||||
{
|
|
||||||
"wh_status": status, # keep for backwards compability
|
|
||||||
"wh_success": is_success,
|
|
||||||
"wh_message": reason_phrase,
|
|
||||||
"wh_response": text,
|
|
||||||
},
|
|
||||||
)
|
|
@ -1,138 +0,0 @@
|
|||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="API info"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlp"></q-btn>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="List pay links">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code><span class="text-blue">GET</span> /lnurlp/api/v1/links</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>[<pay_link_object>, ...]</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key:
|
|
||||||
{{ user.wallets[0].inkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Get a pay link">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span>
|
|
||||||
/lnurlp/api/v1/links/<pay_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 201 CREATED (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{"lnurl": <string>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
|
||||||
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Create a pay link"
|
|
||||||
>
|
|
||||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlp"></q-btn>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code><span class="text-green">POST</span> /lnurlp/api/v1/links</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<code
|
|
||||||
>{"description": <string> "amount": <integer> "max":
|
|
||||||
<integer> "min": <integer> "comment_chars":
|
|
||||||
<integer>}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 201 CREATED (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{"lnurl": <string>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.base_url }}lnurlp/api/v1/links -d
|
|
||||||
'{"description": <string>, "amount": <integer>, "max":
|
|
||||||
<integer>, "min": <integer>, "comment_chars":
|
|
||||||
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
|
||||||
{{ user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Update a pay link"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-green">PUT</span>
|
|
||||||
/lnurlp/api/v1/links/<pay_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<code>{"description": <string>, "amount": <integer>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{"lnurl": <string>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
|
||||||
-d '{"description": <string>, "amount": <integer>}' -H
|
|
||||||
"Content-type: application/json" -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Delete a pay link"
|
|
||||||
class="q-pb-md"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-pink">DELETE</span>
|
|
||||||
/lnurlp/api/v1/links/<pay_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
|
||||||
<code></code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X DELETE {{ request.base_url
|
|
||||||
}}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-expansion-item>
|
|
@ -1,31 +0,0 @@
|
|||||||
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<p>
|
|
||||||
<b>WARNING: LNURL must be used over https or TOR</b><br />
|
|
||||||
LNURL is a range of lightning-network standards that allow us to use
|
|
||||||
lightning-network differently. An LNURL-pay is a link that wallets use
|
|
||||||
to fetch an invoice from a server on-demand. The link or QR code is
|
|
||||||
fixed, but each time it is read by a compatible wallet a new QR code is
|
|
||||||
issued by the service. It can be used to activate machines without them
|
|
||||||
having to maintain an electronic screen to generate and show invoices
|
|
||||||
locally, or to sell any predefined good or service automatically.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Exploring LNURL and finding use cases, is really helping inform
|
|
||||||
lightning protocol development, rather than the protocol dictating how
|
|
||||||
lightning-network should be engaged with.
|
|
||||||
</p>
|
|
||||||
<small
|
|
||||||
>Check
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
href="https://github.com/fiatjaf/awesome-lnurl"
|
|
||||||
target="_blank"
|
|
||||||
>Awesome LNURL</a
|
|
||||||
>
|
|
||||||
for further information.</small
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
@ -1,54 +0,0 @@
|
|||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
|
||||||
<q-card class="q-pa-lg">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div class="text-center">
|
|
||||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
|
||||||
<qrcode
|
|
||||||
value="lightning:{{ lnurl }}"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
|
|
||||||
>Copy LNURL</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
icon="nfc"
|
|
||||||
@click="writeNfcTag(' {{ lnurl }} ')"
|
|
||||||
:disable="nfcTagWriting"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-pay link</h6>
|
|
||||||
<p class="q-my-none">Use an LNURL compatible bitcoin wallet to pay.</p>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-list> {% include "lnurlp/_lnurl.html" %} </q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin]
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,345 +0,0 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
||||||
%} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
|
||||||
>New pay link</q-btn
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">Pay links</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="payLinks"
|
|
||||||
row-key="id"
|
|
||||||
:pagination.sync="payLinksTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th auto-width>Description</q-th>
|
|
||||||
<q-th auto-width>Amount</q-th>
|
|
||||||
<q-th auto-width>Currency</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="launch"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
type="a"
|
|
||||||
:href="props.row.pay_url"
|
|
||||||
target="_blank"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="visibility"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
@click="openQrCodeDialog(props.row.id)"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>{{ props.row.description }}</q-td>
|
|
||||||
<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 auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="openUpdateDialog(props.row.id)"
|
|
||||||
icon="edit"
|
|
||||||
color="light-blue"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deletePayLink(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-subtitle1 q-my-none">
|
|
||||||
{{SITE_TITLE}} LNURL-pay extension
|
|
||||||
</h6>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-list>
|
|
||||||
{% include "lnurlp/_api_docs.html" %}
|
|
||||||
<q-separator></q-separator>
|
|
||||||
{% include "lnurlp/_lnurl.html" %}
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="formDialog.show" @hide="closeFormDialog">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="formDialog.data.wallet"
|
|
||||||
:options="g.user.walletOptions"
|
|
||||||
label="Wallet *"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.description"
|
|
||||||
type="text"
|
|
||||||
label="Item description *"
|
|
||||||
>
|
|
||||||
</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="currencies"
|
|
||||||
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
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.number="formDialog.data.comment_chars"
|
|
||||||
type="number"
|
|
||||||
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
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="formDialog.data.webhook_url"
|
|
||||||
type="text"
|
|
||||||
label="Webhook URL (optional)"
|
|
||||||
hint="A URL to be called whenever this link receives a payment."
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-if="formDialog.data.webhook_url"
|
|
||||||
v-model="formDialog.data.webhook_headers"
|
|
||||||
type="text"
|
|
||||||
label="Webhook headers (optional)"
|
|
||||||
hint="Custom data as JSON string, send headers along with the webhook."
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-if="formDialog.data.webhook_url"
|
|
||||||
v-model="formDialog.data.webhook_body"
|
|
||||||
type="text"
|
|
||||||
label="Webhook custom data (optional)"
|
|
||||||
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="formDialog.data.success_text"
|
|
||||||
type="text"
|
|
||||||
label="Success message (optional)"
|
|
||||||
hint="Will be shown to the user in his wallet after a successful payment."
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="formDialog.data.success_url"
|
|
||||||
type="text"
|
|
||||||
label="Success URL (optional)"
|
|
||||||
hint="Will be shown as a clickable link to the user in his wallet after a successful payment, appended by the payment_hash as a query string."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-if="formDialog.data.id"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
>Update pay link</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="
|
|
||||||
formDialog.data.wallet == null ||
|
|
||||||
formDialog.data.description == null ||
|
|
||||||
(
|
|
||||||
formDialog.data.min == null ||
|
|
||||||
formDialog.data.min <= 0
|
|
||||||
)
|
|
||||||
"
|
|
||||||
type="submit"
|
|
||||||
>Create pay link</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
|
||||||
{% raw %}
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
|
||||||
<qrcode
|
|
||||||
:value="'lightning:' + qrCodeDialog.data.lnurl"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
<p style="word-break: break-all">
|
|
||||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
|
||||||
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }}<br />
|
|
||||||
<span v-if="qrCodeDialog.data.currency"
|
|
||||||
><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 />
|
|
||||||
<strong>On success:</strong> {{ qrCodeDialog.data.success }}<br />
|
|
||||||
</p>
|
|
||||||
{% endraw %}
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
|
|
||||||
class="q-ml-sm"
|
|
||||||
>Copy LNURL</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
icon="link"
|
|
||||||
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
|
|
||||||
><q-tooltip>Copy sharable link</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
icon="nfc"
|
|
||||||
@click="writeNfcTag(qrCodeDialog.data.lnurl)"
|
|
||||||
:disable="nfcTagWriting"
|
|
||||||
><q-tooltip>Write to NFC</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
icon="print"
|
|
||||||
type="a"
|
|
||||||
:href="qrCodeDialog.data.print_url"
|
|
||||||
target="_blank"
|
|
||||||
><q-tooltip>Print</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
|
||||||
<script src="/lnurlp/static/js/index.js"></script>
|
|
||||||
{% endblock %}
|
|
@ -1,27 +0,0 @@
|
|||||||
{% extends "print.html" %} {% block page %}
|
|
||||||
<div class="row justify-center">
|
|
||||||
<div class="qr">
|
|
||||||
<qrcode value="lightning:{{ lnurl }}" :options="{width}"></qrcode>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block styles %}
|
|
||||||
<style>
|
|
||||||
.qr {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
created: function () {
|
|
||||||
window.print()
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {width: window.innerWidth * 0.5}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,43 +0,0 @@
|
|||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
from lnbits.decorators import check_user_exists
|
|
||||||
|
|
||||||
from . import lnurlp_ext, lnurlp_renderer
|
|
||||||
from .crud import get_pay_link
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/", response_class=HTMLResponse)
|
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
return lnurlp_renderer().TemplateResponse(
|
|
||||||
"lnurlp/index.html", {"request": request, "user": user.dict()}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/link/{link_id}", response_class=HTMLResponse)
|
|
||||||
async def display(request: Request, link_id):
|
|
||||||
link = await get_pay_link(link_id)
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
|
||||||
)
|
|
||||||
ctx = {"request": request, "lnurl": link.lnurl(req=request)}
|
|
||||||
return lnurlp_renderer().TemplateResponse("lnurlp/display.html", ctx)
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/print/{link_id}", response_class=HTMLResponse)
|
|
||||||
async def print_qr(request: Request, link_id):
|
|
||||||
link = await get_pay_link(link_id)
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
|
||||||
)
|
|
||||||
ctx = {"request": request, "lnurl": link.lnurl(req=request)}
|
|
||||||
return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", ctx)
|
|
@ -1,180 +0,0 @@
|
|||||||
import json
|
|
||||||
from asyncio.log import logger
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, Query, Request
|
|
||||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
|
||||||
from lnbits.decorators import WalletTypeInfo, check_admin, get_key_type
|
|
||||||
from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
|
|
||||||
|
|
||||||
from . import lnurlp_ext, scheduled_tasks
|
|
||||||
from .crud import (
|
|
||||||
create_pay_link,
|
|
||||||
delete_pay_link,
|
|
||||||
get_pay_link,
|
|
||||||
get_pay_links,
|
|
||||||
update_pay_link,
|
|
||||||
)
|
|
||||||
from .models import CreatePayLinkData
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/api/v1/currencies")
|
|
||||||
async def api_list_currencies_available():
|
|
||||||
return list(currencies.keys())
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
|
|
||||||
async def api_links(
|
|
||||||
req: Request,
|
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
|
||||||
all_wallets: bool = Query(False),
|
|
||||||
):
|
|
||||||
wallet_ids = [wallet.wallet.id]
|
|
||||||
|
|
||||||
if all_wallets:
|
|
||||||
user = await get_user(wallet.wallet.user)
|
|
||||||
wallet_ids = user.wallet_ids if user else []
|
|
||||||
|
|
||||||
try:
|
|
||||||
return [
|
|
||||||
{**link.dict(), "lnurl": link.lnurl(req)}
|
|
||||||
for link in await get_pay_links(wallet_ids)
|
|
||||||
]
|
|
||||||
|
|
||||||
except LnurlInvalidUrl:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.UPGRADE_REQUIRED,
|
|
||||||
detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_link_retrieve(
|
|
||||||
r: Request, link_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
):
|
|
||||||
link = await get_pay_link(link_id)
|
|
||||||
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
if link.wallet != wallet.wallet.id:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
return {**link.dict(), **{"lnurl": link.lnurl(r)}}
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
|
|
||||||
@lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_link_create_or_update(
|
|
||||||
data: CreatePayLinkData,
|
|
||||||
request: Request,
|
|
||||||
link_id=None,
|
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
|
||||||
):
|
|
||||||
|
|
||||||
if data.min > data.max:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.currency is None and (
|
|
||||||
round(data.min) != data.min or round(data.max) != data.max or data.min < 1
|
|
||||||
):
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.webhook_headers:
|
|
||||||
try:
|
|
||||||
json.loads(data.webhook_headers)
|
|
||||||
except ValueError:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Invalid JSON in webhook_headers.",
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.webhook_body:
|
|
||||||
try:
|
|
||||||
json.loads(data.webhook_body)
|
|
||||||
except ValueError:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Invalid JSON in webhook_body.",
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# database only allows int4 entries for min and max. For fiat currencies,
|
|
||||||
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
|
||||||
if data.currency and data.fiat_base_multiplier:
|
|
||||||
data.min *= data.fiat_base_multiplier
|
|
||||||
data.max *= data.fiat_base_multiplier
|
|
||||||
|
|
||||||
if data.success_url is not None and not data.success_url.startswith("https://"):
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Success URL must be secure https://...",
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_id:
|
|
||||||
link = await get_pay_link(link_id)
|
|
||||||
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
if link.wallet != wallet.wallet.id:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
link = await update_pay_link(**data.dict(), link_id=link_id)
|
|
||||||
else:
|
|
||||||
link = await create_pay_link(data, wallet_id=wallet.wallet.id)
|
|
||||||
assert link
|
|
||||||
return {**link.dict(), "lnurl": link.lnurl(request)}
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
|
||||||
link = await get_pay_link(link_id)
|
|
||||||
|
|
||||||
if not link:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
if link.wallet != wallet.wallet.id:
|
|
||||||
raise HTTPException(
|
|
||||||
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
await delete_pay_link(link_id)
|
|
||||||
return {"success": True}
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.get("/api/v1/rate/{currency}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_check_fiat_rate(currency):
|
|
||||||
try:
|
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
|
||||||
except AssertionError:
|
|
||||||
rate = None
|
|
||||||
|
|
||||||
return {"rate": rate}
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_ext.delete("/api/v1", status_code=HTTPStatus.OK)
|
|
||||||
async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
|
|
||||||
for t in scheduled_tasks:
|
|
||||||
try:
|
|
||||||
t.cancel()
|
|
||||||
except Exception as ex:
|
|
||||||
logger.warning(ex)
|
|
||||||
|
|
||||||
return {"success": True}
|
|
@ -125,14 +125,27 @@ async def test_get_extensions_no_user(client):
|
|||||||
|
|
||||||
|
|
||||||
# check GET /extensions: enable extension
|
# check GET /extensions: enable extension
|
||||||
@pytest.mark.asyncio
|
# TODO: test fails because of removing lnurlp extension
|
||||||
async def test_get_extensions_enable(client, to_user):
|
# @pytest.mark.asyncio
|
||||||
response = await client.get(
|
# async def test_get_extensions_enable(client, to_user):
|
||||||
"extensions", params={"usr": to_user.id, "enable": "lnurlp"}
|
# response = await client.get(
|
||||||
)
|
# "extensions", params={"usr": to_user.id, "enable": "lnurlp"}
|
||||||
assert response.status_code == 200, (
|
# )
|
||||||
str(response.url) + " " + str(response.status_code)
|
# assert response.status_code == 200, (
|
||||||
)
|
# str(response.url) + " " + str(response.status_code)
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
# check GET /extensions: enable and disable extensions, expect code 400 bad request
|
||||||
|
# @pytest.mark.asyncio
|
||||||
|
# async def test_get_extensions_enable_and_disable(client, to_user):
|
||||||
|
# response = await client.get(
|
||||||
|
# "extensions",
|
||||||
|
# params={"usr": to_user.id, "enable": "lnurlp", "disable": "lnurlp"},
|
||||||
|
# )
|
||||||
|
# assert response.status_code == 400, (
|
||||||
|
# str(response.url) + " " + str(response.status_code)
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
# check GET /extensions: enable nonexistent extension, expect code 400 bad request
|
# check GET /extensions: enable nonexistent extension, expect code 400 bad request
|
||||||
@ -144,15 +157,3 @@ async def test_get_extensions_enable_nonexistent_extension(client, to_user):
|
|||||||
assert response.status_code == 400, (
|
assert response.status_code == 400, (
|
||||||
str(response.url) + " " + str(response.status_code)
|
str(response.url) + " " + str(response.status_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# check GET /extensions: enable and disable extensions, expect code 400 bad request
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_extensions_enable_and_disable(client, to_user):
|
|
||||||
response = await client.get(
|
|
||||||
"extensions",
|
|
||||||
params={"usr": to_user.id, "enable": "lnurlp", "disable": "lnurlp"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 400, (
|
|
||||||
str(response.url) + " " + str(response.status_code)
|
|
||||||
)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user