Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31cf2eb164 | |||
|
|
d299e15c2f |
||
|
|
dc37e259ba |
||
|
|
9281cb74fb |
||
|
|
6d8ee66019 |
||
|
|
17135b45ae |
||
|
|
76c5841bc8 |
||
|
|
33b06bcd9b |
||
|
|
a1a55cb974 |
27 changed files with 1964 additions and 1616 deletions
19
README.md
19
README.md
|
|
@ -1,3 +1,13 @@
|
||||||
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||||
|
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:280px">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
[](./LICENSE)
|
||||||
|
[](https://github.com/lnbits/lnbits)
|
||||||
|
|
||||||
# LNURLp - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
# LNURLp - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
||||||
|
|
||||||
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small>
|
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small>
|
||||||
|
|
@ -12,7 +22,6 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n
|
||||||
|
|
||||||
1. Create an LNURLp (New Pay link)\
|
1. Create an LNURLp (New Pay link)\
|
||||||

|

|
||||||
|
|
||||||
- select your wallets
|
- select your wallets
|
||||||
- make a small description
|
- make a small description
|
||||||
- enter amount
|
- enter amount
|
||||||
|
|
@ -25,7 +34,6 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n
|
||||||
|
|
||||||
2. Use the shareable link or view the LNURLp you just created\
|
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\
|
- you can now open your LNURLp and copy the LNURL, get the shareable link or print it\
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -57,3 +65,10 @@ Now you can receive sats to your newly created LN address. You will find this in
|
||||||
[](https://postimg.cc/3WwsXJHP)
|
[](https://postimg.cc/3WwsXJHP)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Powered by LNbits
|
||||||
|
|
||||||
|
[LNbits](https://lnbits.com) is a free and open-source lightning accounts system.
|
||||||
|
|
||||||
|
[](https://shop.lnbits.com/)
|
||||||
|
[](https://my.lnbits.com/login)
|
||||||
|
|
|
||||||
36
__init__.py
36
__init__.py
|
|
@ -42,6 +42,42 @@ def lnurlp_start():
|
||||||
task = create_permanent_unique_task("lnurlp", wait_for_paid_invoices)
|
task = create_permanent_unique_task("lnurlp", wait_for_paid_invoices)
|
||||||
scheduled_tasks.append(task)
|
scheduled_tasks.append(task)
|
||||||
|
|
||||||
|
# Expose lnurlp's CRUD over the LNbits nostr transport so an HTTP-
|
||||||
|
# allergic client (e.g. lamassu-next ATM) can manage PayLinks over
|
||||||
|
# kind-21000 encrypted events. Also wires the link-owner resolver so
|
||||||
|
# `subscribe_payments({tag:"lnurlp", link_id:...})` can verify
|
||||||
|
# ownership of the underlying wallet. No-op if the core transport
|
||||||
|
# module isn't present in the LNbits build.
|
||||||
|
try:
|
||||||
|
from lnbits.core.services.nostr_transport.dispatcher import (
|
||||||
|
AUTH_ACCOUNT,
|
||||||
|
AUTH_WALLET,
|
||||||
|
register_rpc,
|
||||||
|
)
|
||||||
|
from lnbits.core.services.nostr_transport.subscriptions import (
|
||||||
|
register_link_owner_resolver,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
return
|
||||||
|
|
||||||
|
from .transport_rpcs import (
|
||||||
|
handle_lnurlp_create,
|
||||||
|
handle_lnurlp_delete,
|
||||||
|
handle_lnurlp_get,
|
||||||
|
handle_lnurlp_list,
|
||||||
|
handle_lnurlp_update,
|
||||||
|
resolve_lnurlp_owner,
|
||||||
|
)
|
||||||
|
|
||||||
|
register_rpc("lnurlp_create", handle_lnurlp_create, AUTH_WALLET)
|
||||||
|
register_rpc("lnurlp_get", handle_lnurlp_get, AUTH_WALLET)
|
||||||
|
register_rpc("lnurlp_list", handle_lnurlp_list, AUTH_ACCOUNT)
|
||||||
|
register_rpc("lnurlp_update", handle_lnurlp_update, AUTH_WALLET)
|
||||||
|
register_rpc("lnurlp_delete", handle_lnurlp_delete, AUTH_WALLET)
|
||||||
|
# lnurlp stamps `extra["link"] = link.id` on settlement
|
||||||
|
# (views_lnurl.py:86), which is the default extras-key, so no override.
|
||||||
|
register_link_owner_resolver("lnurlp", resolve_lnurlp_owner)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"db",
|
"db",
|
||||||
|
|
|
||||||
13
config.json
13
config.json
|
|
@ -1,7 +1,10 @@
|
||||||
{
|
{
|
||||||
|
"id": "paylink",
|
||||||
|
"version": "1.3.0",
|
||||||
"name": "Pay Links",
|
"name": "Pay Links",
|
||||||
"version": "1.2.0",
|
"repo": "https://github.com/lnbits/lnurlp",
|
||||||
"short_description": "Make reusable LNURL pay links",
|
"short_description": "Make static reusable LNURL pay links or lightning addresses",
|
||||||
|
"description": "",
|
||||||
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
"tile": "/lnurlp/static/image/lnurl-pay.png",
|
||||||
"min_lnbits_version": "1.4.0",
|
"min_lnbits_version": "1.4.0",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
|
|
@ -51,5 +54,9 @@
|
||||||
],
|
],
|
||||||
"description_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/description.md",
|
"description_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/description.md",
|
||||||
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/toc.md",
|
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/lnurlp/main/toc.md",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"paid_features": "",
|
||||||
|
"tags": ["Merchant", "Payments"],
|
||||||
|
"donate": "",
|
||||||
|
"hidden": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
crud.py
1
crud.py
|
|
@ -65,6 +65,7 @@ async def create_pay_link(data: CreatePayLinkData) -> PayLink:
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
disposable=data.disposable if data.disposable is not None else True,
|
disposable=data.disposable if data.disposable is not None else True,
|
||||||
|
domain=data.domain,
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.insert("lnurlp.pay_links", link)
|
await db.insert("lnurlp.pay_links", link)
|
||||||
|
|
|
||||||
|
|
@ -1 +1,10 @@
|
||||||
Create a static LNURLp or LNaddress people can use to pay.
|
Create static LNURL-pay links and Lightning addresses for receiving payments.
|
||||||
|
|
||||||
|
Its functions include:
|
||||||
|
|
||||||
|
- Generating reusable LNURL-pay QR codes
|
||||||
|
- Creating custom Lightning addresses
|
||||||
|
- Setting minimum and maximum payment amounts
|
||||||
|
- Adding payment descriptions and metadata
|
||||||
|
|
||||||
|
A foundational tool for anyone who wants a simple, reusable way to receive Lightning payments with a static QR code or Lightning address.
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,16 @@ def parse_nostr_private_key(key: str) -> PrivateKey:
|
||||||
return PrivateKey(bytes.fromhex(key))
|
return PrivateKey(bytes.fromhex(key))
|
||||||
|
|
||||||
|
|
||||||
def lnurl_encode_link_id(req: Request, link_id: str) -> str:
|
def lnurl_encode_link(req: Request, link_id: str, domain: str | None = None) -> str:
|
||||||
|
if domain:
|
||||||
|
url_str = f"https://{domain}/lnurlp/{link_id}"
|
||||||
|
return str(lnurl_encode(url_str).bech32)
|
||||||
|
|
||||||
url = req.url_for("lnurlp.api_lnurl_response", link_id=link_id)
|
url = req.url_for("lnurlp.api_lnurl_response", link_id=link_id)
|
||||||
url = url.replace(path=url.path)
|
url = url.replace(path=url.path)
|
||||||
url_str = str(url)
|
url_str = str(url)
|
||||||
if url.netloc.endswith(".onion"):
|
if url.netloc.endswith(".onion"):
|
||||||
# change url string scheme to http
|
# change url string scheme to http
|
||||||
url_str = url_str.replace("https://", "http://")
|
url_str = url_str.replace("https://", "http://")
|
||||||
|
|
||||||
return str(lnurl_encode(url_str).bech32)
|
return str(lnurl_encode(url_str).bech32)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ async def m001_initial(db):
|
||||||
"""
|
"""
|
||||||
Initial pay table.
|
Initial pay table.
|
||||||
"""
|
"""
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE lnurlp.pay_links (
|
CREATE TABLE lnurlp.pay_links (
|
||||||
id {db.serial_primary_key},
|
id {db.serial_primary_key},
|
||||||
wallet TEXT NOT NULL,
|
wallet TEXT NOT NULL,
|
||||||
|
|
@ -17,8 +16,7 @@ async def m001_initial(db):
|
||||||
served_meta INTEGER NOT NULL,
|
served_meta INTEGER NOT NULL,
|
||||||
served_pr INTEGER NOT NULL
|
served_pr INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_webhooks_and_success_actions(db):
|
async def m002_webhooks_and_success_actions(db):
|
||||||
|
|
@ -28,16 +26,14 @@ async def m002_webhooks_and_success_actions(db):
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_url TEXT;")
|
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_text TEXT;")
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_url TEXT;")
|
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_url TEXT;")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE lnurlp.invoices (
|
CREATE TABLE lnurlp.invoices (
|
||||||
pay_link INTEGER NOT NULL REFERENCES {db.references_schema}pay_links (id),
|
pay_link INTEGER NOT NULL REFERENCES {db.references_schema}pay_links (id),
|
||||||
payment_hash TEXT NOT NULL,
|
payment_hash TEXT NOT NULL,
|
||||||
webhook_sent INT, -- null means not sent, otherwise store status
|
webhook_sent INT, -- null means not sent, otherwise store status
|
||||||
expiry INT
|
expiry INT
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m003_min_max_comment_fiat(db):
|
async def m003_min_max_comment_fiat(db):
|
||||||
|
|
@ -86,8 +82,7 @@ async def m006_redux(db):
|
||||||
else:
|
else:
|
||||||
# but we have to do this for sqlite
|
# but we have to do this for sqlite
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
|
await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE lnurlp.pay_links (
|
CREATE TABLE lnurlp.pay_links (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
wallet TEXT NOT NULL,
|
wallet TEXT NOT NULL,
|
||||||
|
|
@ -105,8 +100,7 @@ async def m006_redux(db):
|
||||||
webhook_headers TEXT,
|
webhook_headers TEXT,
|
||||||
webhook_body TEXT
|
webhook_body TEXT
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
for row in [
|
for row in [
|
||||||
list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
|
list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
|
||||||
|
|
@ -172,13 +166,11 @@ async def m009_add_settings(db):
|
||||||
"""
|
"""
|
||||||
Add extension settings table
|
Add extension settings table
|
||||||
"""
|
"""
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE lnurlp.settings (
|
CREATE TABLE lnurlp.settings (
|
||||||
nostr_private_key TEXT NOT NULL
|
nostr_private_key TEXT NOT NULL
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m010_add_pay_link_domain(db):
|
async def m010_add_pay_link_domain(db):
|
||||||
|
|
@ -193,14 +185,10 @@ async def m011_add_created_at(db: Connection):
|
||||||
Add created_at to pay links
|
Add created_at to pay links
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(f"""ALTER TABLE lnurlp.pay_links ADD COLUMN
|
||||||
f"""ALTER TABLE lnurlp.pay_links ADD COLUMN
|
created_at TIMESTAMP DEFAULT {db.timestamp_column_default}""")
|
||||||
created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"""
|
await db.execute(f"""ALTER TABLE lnurlp.pay_links ADD COLUMN
|
||||||
)
|
updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}""")
|
||||||
await db.execute(
|
|
||||||
f"""ALTER TABLE lnurlp.pay_links ADD COLUMN
|
|
||||||
updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"""
|
|
||||||
)
|
|
||||||
|
|
||||||
now = int(time())
|
now = int(time())
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|
|
||||||
24
models.py
24
models.py
|
|
@ -35,6 +35,7 @@ class CreatePayLinkData(BaseModel):
|
||||||
username: str | None = Query(None)
|
username: str | None = Query(None)
|
||||||
zaps: bool | None = Query(False)
|
zaps: bool | None = Query(False)
|
||||||
disposable: bool | None = Query(True)
|
disposable: bool | None = Query(True)
|
||||||
|
domain: str | None = Query(None)
|
||||||
|
|
||||||
|
|
||||||
class PayLink(BaseModel):
|
class PayLink(BaseModel):
|
||||||
|
|
@ -67,8 +68,25 @@ class PayLink(BaseModel):
|
||||||
success_url: str | None = None
|
success_url: str | None = None
|
||||||
currency: str | None = None
|
currency: str | None = None
|
||||||
fiat_base_multiplier: int | None = None
|
fiat_base_multiplier: int | None = None
|
||||||
|
|
||||||
disposable: bool
|
disposable: bool
|
||||||
|
|
||||||
# TODO deprecated, unused in the code, should be deleted from db.
|
|
||||||
domain: str | None = None
|
domain: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PublicPayLink(BaseModel):
|
||||||
|
id: str
|
||||||
|
username: str | None = None
|
||||||
|
description: str
|
||||||
|
min: float
|
||||||
|
max: float
|
||||||
|
domain: str | None = None
|
||||||
|
currency: str | None = None
|
||||||
|
lnurl: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
no_database=True,
|
||||||
|
deprecated=True,
|
||||||
|
description=(
|
||||||
|
"Deprecated: Instead of using this bech32 encoded string, dynamically "
|
||||||
|
"generate your own static link (lud17/bech32) on the client side. "
|
||||||
|
"Example: lnurlp://${window.location.hostname}/lnurlp/${paylink_id}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
||||||
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -9,8 +9,8 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"pyright": "^1.1.358"
|
"pyright": "^1.1.407"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
|
|
@ -27,9 +28,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.3.3",
|
"version": "3.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -41,9 +43,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pyright": {
|
"node_modules/pyright": {
|
||||||
"version": "1.1.372",
|
"version": "1.1.407",
|
||||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.372.tgz",
|
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.407.tgz",
|
||||||
"integrity": "sha512-S0XYmTQWK+ha9FTIWviNk91UnbD569wPUCNEltSqtHeTJhbHj5z3LkOKiqXAOvn72BBfylcgpQqyQHsocmQtiQ==",
|
"integrity": "sha512-zU+peTFEVUdokNQyUBhGQYt+NWI/3aiNlvBbDBSsn5Ti334XElFUs+GDjQzCbchYfkT+DvMAT3OkMcV4CuEfDg==",
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"pyright": "index.js",
|
"pyright": "index.js",
|
||||||
"pyright-langserver": "langserver.index.js"
|
"pyright-langserver": "langserver.index.js"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"pyright": "^1.1.358"
|
"pyright": "^1.1.407"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ dependencies = ["lnbits>1"]
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
package-mode = false
|
package-mode = false
|
||||||
|
|
||||||
[tool.uv]
|
[dependency-groups]
|
||||||
dev-dependencies = [
|
dev = [
|
||||||
"black>=24.3.0",
|
"black>=24.3.0",
|
||||||
"pytest-asyncio>=0.21.0",
|
"pytest-asyncio>=0.21.0",
|
||||||
"pytest>=7.3.2",
|
"pytest>=7.3.2",
|
||||||
"mypy>=1.5.1",
|
"mypy==1.17.1",
|
||||||
"pre-commit>=3.2.2",
|
"pre-commit>=3.2.2",
|
||||||
"ruff>=0.3.2",
|
"ruff>=0.3.2",
|
||||||
"types-cffi>=1.16.0.20240331",
|
"types-cffi>=1.16.0.20240331",
|
||||||
|
|
@ -34,14 +34,6 @@ warn_untyped_fields = true
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = [
|
module = [
|
||||||
"lnbits.*",
|
"lnbits.*",
|
||||||
"lnurl.*",
|
|
||||||
"loguru.*",
|
|
||||||
"fastapi.*",
|
|
||||||
"pydantic.*",
|
|
||||||
"pyqrcode.*",
|
|
||||||
"shortuuid.*",
|
|
||||||
"httpx.*",
|
|
||||||
"websocket.*",
|
|
||||||
"pynostr.*",
|
"pynostr.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
|
|
|
||||||
27
static/display.js
Normal file
27
static/display.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
window.PageLnurlpPublic = {
|
||||||
|
template: '#page-lnurlp-public',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
url: '',
|
||||||
|
payLink: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setUrl(link_id, domain) {
|
||||||
|
this.url = `https://${domain || window.location.host}/lnurlp/${link_id}`
|
||||||
|
},
|
||||||
|
getPayLink() {
|
||||||
|
this.api
|
||||||
|
.request('GET', `/lnurlp/api/v1/links/public/${this.$route.params.id}`)
|
||||||
|
.then(res => {
|
||||||
|
this.payLink = res.data
|
||||||
|
this.setUrl(this.payLink.id, this.payLink.domain)
|
||||||
|
})
|
||||||
|
.catch(this.utils.notifyApiError)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.setUrl(this.$route.params.id)
|
||||||
|
this.getPayLink()
|
||||||
|
}
|
||||||
|
}
|
||||||
67
static/display.vue
Normal file
67
static/display.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template id="page-lnurlp-public">
|
||||||
|
<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">
|
||||||
|
<lnbits-qrcode-lnurl :url="url" :nfc="true"></lnbits-qrcode-lnurl>
|
||||||
|
</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>
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="info"
|
||||||
|
label="Powered by LNURL"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
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>
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,31 +1,10 @@
|
||||||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
window.PageLnurlp = {
|
||||||
|
template: '#page-lnurlp',
|
||||||
const locationPath = [
|
|
||||||
window.location.protocol,
|
|
||||||
'//',
|
|
||||||
window.location.host,
|
|
||||||
window.location.pathname
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
const mapPayLink = obj => {
|
|
||||||
obj._data = _.clone(obj)
|
|
||||||
obj.created_at = LNbits.utils.formatDateString(obj.created_at)
|
|
||||||
obj.updated_at = LNbits.utils.formatDateString(obj.updated_at)
|
|
||||||
if (obj.currency) {
|
|
||||||
obj.min = obj.min / obj.fiat_base_multiplier
|
|
||||||
obj.max = obj.max / obj.fiat_base_multiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
|
||||||
obj.pay_url = [locationPath, 'link/', obj.id].join('')
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
window.app = Vue.createApp({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
computed: {
|
computed: {
|
||||||
endpoint: function () {
|
baseUrl() {
|
||||||
|
return window.location.origin + '/lnurlp/api/v1/links'
|
||||||
|
},
|
||||||
|
endpoint() {
|
||||||
return `/lnurlp/api/v1/settings?usr=${this.g.user.id}`
|
return `/lnurlp/api/v1/settings?usr=${this.g.user.id}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -40,7 +19,6 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
domain: window.location.host,
|
domain: window.location.host,
|
||||||
currencies: [],
|
|
||||||
fiatRates: {},
|
fiatRates: {},
|
||||||
payLinks: [],
|
payLinks: [],
|
||||||
payLinksTable: {
|
payLinksTable: {
|
||||||
|
|
@ -106,6 +84,28 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
lnaddress(link) {
|
||||||
|
const domain = link.domain || window.location.host
|
||||||
|
return `${link.username}@${domain}`
|
||||||
|
},
|
||||||
|
mapPayLink(obj) {
|
||||||
|
const locationPath = [
|
||||||
|
window.location.protocol,
|
||||||
|
'//',
|
||||||
|
window.location.host,
|
||||||
|
window.location.pathname
|
||||||
|
].join('')
|
||||||
|
obj._data = _.clone(obj)
|
||||||
|
obj.created_at = LNbits.utils.formatDate(obj.created_at)
|
||||||
|
obj.updated_at = LNbits.utils.formatDate(obj.updated_at)
|
||||||
|
if (obj.currency) {
|
||||||
|
obj.min = obj.min / obj.fiat_base_multiplier
|
||||||
|
obj.max = obj.max / obj.fiat_base_multiplier
|
||||||
|
}
|
||||||
|
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
||||||
|
obj.pay_url = [locationPath, 'link/', obj.id].join('')
|
||||||
|
return obj
|
||||||
|
},
|
||||||
getPayLinks() {
|
getPayLinks() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|
@ -114,7 +114,7 @@ window.app = Vue.createApp({
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.payLinks = response.data.map(mapPayLink)
|
this.payLinks = response.data.map(this.mapPayLink)
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
|
|
@ -144,11 +144,13 @@ window.app = Vue.createApp({
|
||||||
(link.success_url ? ' and URL "' + link.success_url + '"' : '')
|
(link.success_url ? ' and URL "' + link.success_url + '"' : '')
|
||||||
: 'do nothing',
|
: 'do nothing',
|
||||||
lnurl: link.lnurl,
|
lnurl: link.lnurl,
|
||||||
|
domain: link.domain,
|
||||||
pay_url: link.pay_url,
|
pay_url: link.pay_url,
|
||||||
print_url: link.print_url,
|
print_url: link.print_url,
|
||||||
username: link.username
|
username: link.username
|
||||||
}
|
}
|
||||||
this.activeUrl = window.location.origin + '/lnurlp/' + link.id
|
const domain = link.domain || window.location.host
|
||||||
|
this.activeUrl = `https://${domain}/lnurlp/${link.id}`
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
openUpdateDialog(linkId) {
|
openUpdateDialog(linkId) {
|
||||||
|
|
@ -191,7 +193,7 @@ window.app = Vue.createApp({
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
|
this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
|
||||||
this.payLinks.push(mapPayLink(response.data))
|
this.payLinks.push(this.mapPayLink(response.data))
|
||||||
this.formDialog.show = false
|
this.formDialog.show = false
|
||||||
this.resetFormData()
|
this.resetFormData()
|
||||||
})
|
})
|
||||||
|
|
@ -246,13 +248,5 @@ window.app = Vue.createApp({
|
||||||
if (this.g.user.wallets?.length) {
|
if (this.g.user.wallets?.length) {
|
||||||
this.getPayLinks()
|
this.getPayLinks()
|
||||||
}
|
}
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/api/v1/currencies')
|
|
||||||
.then(response => {
|
|
||||||
this.currencies = ['satoshis', ...response.data]
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
686
static/index.vue
Normal file
686
static/index.vue
Normal file
|
|
@ -0,0 +1,686 @@
|
||||||
|
<template id="page-lnurlp">
|
||||||
|
<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
|
||||||
|
>
|
||||||
|
<lnbits-extension-settings-btn-dialog
|
||||||
|
v-if="g.user.admin"
|
||||||
|
:endpoint="endpoint"
|
||||||
|
:options="settings"
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
:rows="payLinks"
|
||||||
|
:columns="payLinksTable.columns"
|
||||||
|
row-key="id"
|
||||||
|
v-model:pagination="payLinksTable.pagination"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr class="text-left" :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
<span v-text="col.label"></span>
|
||||||
|
</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"
|
||||||
|
class="q-ml-sm"
|
||||||
|
><q-tooltip>Shareable Page</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="visibility"
|
||||||
|
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
||||||
|
class="q-ml-sm"
|
||||||
|
@click="openQrCodeDialog(props.row.id)"
|
||||||
|
><q-tooltip>View Link</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="openUpdateDialog(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
class="q-ml-sm"
|
||||||
|
>
|
||||||
|
<q-tooltip>Edit</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deletePayLink(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
class="q-ml-sm"
|
||||||
|
><q-tooltip>Delete</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
v-text="col.value"
|
||||||
|
></q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-icon v-if="props.row.webhook_url" size="14px" name="http">
|
||||||
|
<q-tooltip
|
||||||
|
>Webhook to <span v-text="props.row.webhook_url"></span
|
||||||
|
></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 '<span
|
||||||
|
v-text="props.row.success_text"
|
||||||
|
></span
|
||||||
|
>'
|
||||||
|
<span v-if="props.row.success_url"
|
||||||
|
>and URL '<span v-text="props.row.success_url"></span
|
||||||
|
>'</span
|
||||||
|
>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
<q-icon
|
||||||
|
v-if="props.row.comment_chars > 0"
|
||||||
|
size="14px"
|
||||||
|
name="insert_comment"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="props.row.comment_chars"></span>-char
|
||||||
|
comment allowed
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</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">LNURL-pay extension</h6>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list>
|
||||||
|
<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 <span v-text="baseUrl"></span> -H "X-Api-Key:
|
||||||
|
<span v-text="g.user.wallets[0].inkey"></span>"
|
||||||
|
</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
|
||||||
|
<span v-text="baseUrl + '/<pay_id>'"></span>
|
||||||
|
-H "X-Api-Key:
|
||||||
|
<span v-text="g.user.wallets[0].inkey"></span>"
|
||||||
|
</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> "username":
|
||||||
|
<string> }</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 <span v-text="baseUrl"></span> -d
|
||||||
|
'{"description": <string>, "amount":
|
||||||
|
<integer>, "max": <integer>, "min":
|
||||||
|
<integer>, "comment_chars": <integer>}' -H
|
||||||
|
"Content-type: application/json" -H "X-Api-Key:
|
||||||
|
<span v-text="g.user.wallets[0].adminkey"></span>"
|
||||||
|
</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
|
||||||
|
<span v-text="baseUrl + '/<pay_id>'"></span>
|
||||||
|
-d '{"description": <string>, "amount":
|
||||||
|
<integer>}' -H "Content-type: application/json" -H
|
||||||
|
"X-Api-Key:
|
||||||
|
<span v-text="g.user.wallets[0].adminkey"></span>"
|
||||||
|
</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
|
||||||
|
<span v-text="baseUrl + '/<pay_id>'"></span>
|
||||||
|
-H "X-Api-Key:
|
||||||
|
<span v-text="g.user.wallets[0].adminkey"></span>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<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>
|
||||||
|
</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">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.username"
|
||||||
|
type="text"
|
||||||
|
label="Lightning Address"
|
||||||
|
@input="
|
||||||
|
formDialog.data.username =
|
||||||
|
formDialog.data.username.toLowerCase()
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="flex: 0 0 auto; margin-top: 10px">
|
||||||
|
<span class="label"> @ </span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.domain"
|
||||||
|
type="text"
|
||||||
|
:label="domain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-sm q-mx-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 *'"
|
||||||
|
:hint="
|
||||||
|
formDialog.data.currency &&
|
||||||
|
fiatRates[formDialog.data.currency] &&
|
||||||
|
formDialog.data.min
|
||||||
|
? `approx. ${parseInt(Math.round(formDialog.data.min * fiatRates[formDialog.data.currency]))} sat`
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
></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 *"
|
||||||
|
:hint="
|
||||||
|
formDialog.data.currency &&
|
||||||
|
fiatRates[formDialog.data.currency] &&
|
||||||
|
formDialog.data.max
|
||||||
|
? `approx. ${parseInt(Math.round(formDialog.data.max * fiatRates[formDialog.data.currency]))} sat`
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
></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="g.allowedCurrencies || g.currencies"
|
||||||
|
v-model="formDialog.data.currency"
|
||||||
|
:display-value="formDialog.data.currency || 'satoshis'"
|
||||||
|
label="Currency"
|
||||||
|
:hint="
|
||||||
|
'Converted to satoshis at each payment. ' +
|
||||||
|
(formDialog.data.currency &&
|
||||||
|
fiatRates[formDialog.data.currency]
|
||||||
|
? `Currently 1 ${formDialog.data.currency} = ${fiatRates[formDialog.data.currency]} sat`
|
||||||
|
: '')
|
||||||
|
"
|
||||||
|
@input="updateFiatRate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-expansion-item
|
||||||
|
group="advanced"
|
||||||
|
icon="settings"
|
||||||
|
label="Advanced options"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
LUD-11: Disposable and storeable payRequests.
|
||||||
|
</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-checkbox
|
||||||
|
dense
|
||||||
|
:toggle-indeterminate="false"
|
||||||
|
v-model="formDialog.data.disposable"
|
||||||
|
label="If enabled, the LNURL will not be stored (default)."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">LNURL</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="formDialog.data.comment_chars"
|
||||||
|
type="number"
|
||||||
|
label="Comment maximum characters"
|
||||||
|
hint="Allow the payer to attach a comment."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" v-if="formDialog.data.webhook_url">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="formDialog.data.success_url"
|
||||||
|
type="text"
|
||||||
|
label="Success URL (optional)"
|
||||||
|
hint="Link will be shown to the sender after a successful payment."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Nostr</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-checkbox
|
||||||
|
:toggle-indeterminate="false"
|
||||||
|
dense
|
||||||
|
v-model="formDialog.data.zaps"
|
||||||
|
label="Enable nostr zaps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<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">
|
||||||
|
<lnbits-qrcode-lnurl :url="activeUrl" :nfc="true"></lnbits-qrcode-lnurl>
|
||||||
|
<p style="word-break: break-all">
|
||||||
|
<strong>ID:</strong> <span v-text="qrCodeDialog.data.id"></span><br />
|
||||||
|
<strong>Amount:</strong>
|
||||||
|
<span v-text="qrCodeDialog.data.amount"></span><br />
|
||||||
|
|
||||||
|
<span v-if="qrCodeDialog.data.currency"
|
||||||
|
><strong
|
||||||
|
><span v-text="qrCodeDialog.data.currency"></span> price:</strong
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="fiatRates[qrCodeDialog.data.currency]"
|
||||||
|
v-text="fiatRates[qrCodeDialog.data.currency] + 'sat'"
|
||||||
|
></span>
|
||||||
|
<span v-else>Loading...</span>
|
||||||
|
<br
|
||||||
|
/></span>
|
||||||
|
<strong>Accepts comments:</strong>
|
||||||
|
<span v-text="qrCodeDialog.data.comments"></span><br />
|
||||||
|
<strong>Dispatches webhook to:</strong>
|
||||||
|
<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">
|
||||||
|
<strong>Lightning Address: </strong>
|
||||||
|
<span v-text="lnaddress(qrCodeDialog.data)"></span>
|
||||||
|
<q-icon
|
||||||
|
name="content_copy"
|
||||||
|
class="text-grey cursor-pointer q-ml-sm"
|
||||||
|
@click="utils.copyText(lnaddress(qrCodeDialog.data))"
|
||||||
|
></q-icon>
|
||||||
|
<q-icon name="qr_code" class="text-grey cursor-pointer q-ml-sm">
|
||||||
|
<q-popup-proxy>
|
||||||
|
<lnbits-qrcode
|
||||||
|
class="q-pa-md"
|
||||||
|
:value="lnaddress(qrCodeDialog.data)"
|
||||||
|
:show-buttons="false"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
icon="link"
|
||||||
|
@click="
|
||||||
|
utils.copyText(
|
||||||
|
qrCodeDialog.data.pay_url,
|
||||||
|
'Link copied to clipboard!'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
><q-tooltip>Copy sharable link</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>
|
||||||
|
</template>
|
||||||
14
static/routes.json
Normal file
14
static/routes.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "/lnurlp/",
|
||||||
|
"name": "PageLnurlp",
|
||||||
|
"template": "/lnurlp/static/index.vue",
|
||||||
|
"component": "/lnurlp/static/index.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/lnurlp/link/:id",
|
||||||
|
"name": "PageLnurlpPublic",
|
||||||
|
"template": "/lnurlp/static/display.vue",
|
||||||
|
"component": "/lnurlp/static/display.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
12
tasks.py
12
tasks.py
|
|
@ -120,7 +120,7 @@ async def send_zap(payment: Payment):
|
||||||
return res[0] if res else None
|
return res[0] if res else None
|
||||||
|
|
||||||
tags = []
|
tags = []
|
||||||
for t in ["p", "e"]:
|
for t in ["p", "e", "a"]:
|
||||||
tag = get_tag(event_json, t)
|
tag = get_tag(event_json, t)
|
||||||
if tag:
|
if tag:
|
||||||
tags.append([t, tag[0]])
|
tags.append([t, tag[0]])
|
||||||
|
|
@ -144,6 +144,16 @@ async def send_zap(payment: Payment):
|
||||||
async with websockets.connect(relay_url, open_timeout=5) as websocket:
|
async with websockets.connect(relay_url, open_timeout=5) as websocket:
|
||||||
logger.debug(f"Sending zap to {relay_url}")
|
logger.debug(f"Sending zap to {relay_url}")
|
||||||
await websocket.send(event_message)
|
await websocket.send(event_message)
|
||||||
|
response = await asyncio.wait_for(websocket.recv(), timeout=5)
|
||||||
|
relay_response = json.loads(response)
|
||||||
|
if relay_response[0] != "OK" or not relay_response[2]:
|
||||||
|
logger.debug(
|
||||||
|
f"Relay did not acknowledge zap receipt: {relay_response}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
logger.debug(f"Zap sent to {relay_url} successfully")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.debug(f"Relay did not acknowledge zap receipt: {relay_url}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to send zap to {relay_url}: {e}")
|
logger.warning(f"Failed to send zap to {relay_url}: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
<span v-text="g.user.wallets[0].inkey"></span> "
|
|
||||||
</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: <span v-text="g.user.wallets[0].inkey"></span>"
|
|
||||||
</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> "username": <string> }</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:
|
|
||||||
<span v-text="g.user.wallets[0].adminkey"></span>"
|
|
||||||
</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:
|
|
||||||
<span v-text="g.user.wallets[0].adminkey"></span>"
|
|
||||||
</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:
|
|
||||||
<span v-text="g.user.wallets[0].adminkey"></span>"
|
|
||||||
</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,37 +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">
|
|
||||||
<lnbits-qrcode-lnurl :url="url" :nfc="true"></lnbits-qrcode-lnurl>
|
|
||||||
</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>
|
|
||||||
window.app = Vue.createApp({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
url: window.location.origin + '/lnurlp/{{ link_id }}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,427 +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
|
|
||||||
>
|
|
||||||
<lnbits-extension-settings-btn-dialog
|
|
||||||
v-if="this.g.user.admin"
|
|
||||||
:endpoint="endpoint"
|
|
||||||
:options="settings"
|
|
||||||
/>
|
|
||||||
</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
|
|
||||||
:rows="payLinks"
|
|
||||||
:columns="payLinksTable.columns"
|
|
||||||
row-key="id"
|
|
||||||
v-model:pagination="payLinksTable.pagination"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr class="text-left" :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
<span v-text="col.label"></span>
|
|
||||||
</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"
|
|
||||||
class="q-ml-sm"
|
|
||||||
><q-tooltip>Shareable Page</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="visibility"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
class="q-ml-sm"
|
|
||||||
@click="openQrCodeDialog(props.row.id)"
|
|
||||||
><q-tooltip>View Link</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
</q-td>
|
|
||||||
<q-td
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
v-text="col.value"
|
|
||||||
></q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-icon v-if="props.row.webhook_url" size="14px" name="http">
|
|
||||||
<q-tooltip
|
|
||||||
>Webhook to <span v-text="props.row.webhook_url"></span
|
|
||||||
></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 '<span
|
|
||||||
v-text="props.row.success_text"
|
|
||||||
></span
|
|
||||||
>'
|
|
||||||
<span v-if="props.row.success_url"
|
|
||||||
>and URL '<span v-text="props.row.success_url"></span
|
|
||||||
>'</span
|
|
||||||
>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
<q-icon
|
|
||||||
v-if="props.row.comment_chars > 0"
|
|
||||||
size="14px"
|
|
||||||
name="insert_comment"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="props.row.comment_chars"></span>-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-tooltip>Edit</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deletePayLink(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
><q-tooltip>Delete</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</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">
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="formDialog.data.username"
|
|
||||||
type="text"
|
|
||||||
label="Lightning Address"
|
|
||||||
@input="formDialog.data.username = formDialog.data.username.toLowerCase()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col" style="margin-top: 10px">
|
|
||||||
<span class="label">
|
|
||||||
@ <span v-text="domain"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-sm q-mx-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 *'"
|
|
||||||
:hint="formDialog.data.currency && fiatRates[formDialog.data.currency] && formDialog.data.min ? `approx. ${parseInt(Math.round(formDialog.data.min * fiatRates[formDialog.data.currency]))} sat` : ''"
|
|
||||||
></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 *"
|
|
||||||
:hint="formDialog.data.currency && fiatRates[formDialog.data.currency] && formDialog.data.max ? `approx. ${parseInt(Math.round(formDialog.data.max * fiatRates[formDialog.data.currency]))} sat` : ''"
|
|
||||||
></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="'Converted to satoshis at each payment. ' + (formDialog.data.currency && fiatRates[formDialog.data.currency] ? `Currently 1 ${formDialog.data.currency} = ${fiatRates[formDialog.data.currency]} sat` : '')"
|
|
||||||
@input="updateFiatRate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-expansion-item
|
|
||||||
group="advanced"
|
|
||||||
icon="settings"
|
|
||||||
label="Advanced options"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
LUD-11: Disposable and storeable payRequests.
|
|
||||||
</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
:toggle-indeterminate="false"
|
|
||||||
v-model="formDialog.data.disposable"
|
|
||||||
label="If enabled, the LNURL will not be stored (default)."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">LNURL</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.number="formDialog.data.comment_chars"
|
|
||||||
type="number"
|
|
||||||
label="Comment maximum characters"
|
|
||||||
hint="Allow the payer to attach a comment."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" v-if="formDialog.data.webhook_url">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model="formDialog.data.success_url"
|
|
||||||
type="text"
|
|
||||||
label="Success URL (optional)"
|
|
||||||
hint="Link will be shown to the sender after a successful payment."
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Nostr</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-checkbox
|
|
||||||
:toggle-indeterminate="false"
|
|
||||||
dense
|
|
||||||
v-model="formDialog.data.zaps"
|
|
||||||
label="Enable nostr zaps"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<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">
|
|
||||||
<lnbits-qrcode-lnurl :url="activeUrl" :nfc="true"></lnbits-qrcode-lnurl>
|
|
||||||
<p style="word-break: break-all">
|
|
||||||
<strong>ID:</strong> <span v-text="qrCodeDialog.data.id"></span><br />
|
|
||||||
<strong>Amount:</strong> <span v-text="qrCodeDialog.data.amount"></span
|
|
||||||
><br />
|
|
||||||
|
|
||||||
<span v-if="qrCodeDialog.data.currency"
|
|
||||||
><strong
|
|
||||||
><span v-text="qrCodeDialog.data.currency"></span> price:</strong
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="fiatRates[qrCodeDialog.data.currency]"
|
|
||||||
v-text="fiatRates[qrCodeDialog.data.currency] + 'sat'"
|
|
||||||
></span>
|
|
||||||
<span v-else>Loading...</span>
|
|
||||||
<br
|
|
||||||
/></span>
|
|
||||||
<strong>Accepts comments:</strong>
|
|
||||||
<span v-text="qrCodeDialog.data.comments"></span><br />
|
|
||||||
<strong>Dispatches webhook to:</strong>
|
|
||||||
<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">
|
|
||||||
<strong>Lightning Address: </strong>
|
|
||||||
<span v-text="qrCodeDialog.data.username+'@'+domain"></span>
|
|
||||||
<br />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<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="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,32 +0,0 @@
|
||||||
{% extends "print.html" %} {% block page %}
|
|
||||||
<div class="row justify-center">
|
|
||||||
<div class="qr">
|
|
||||||
<lnbits-qrcode :value="lnurl" :show-buttons="false"></lnbits-qrcode>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block styles %}
|
|
||||||
<style>
|
|
||||||
.qr {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
window.app = Vue.createApp({
|
|
||||||
el: '#vue',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
lnurl: '',
|
|
||||||
width: window.innerWidth * 0.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const url = window.location.origin + '/lnurlp/{{ link_id }}'
|
|
||||||
const bytes = new TextEncoder().encode(url)
|
|
||||||
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
|
|
||||||
this.lnurl = `lightning:${bech32.toUpperCase()}`
|
|
||||||
window.print()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
151
transport_rpcs.py
Normal file
151
transport_rpcs.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
"""
|
||||||
|
Nostr-transport RPC handlers for the lnurlp (LNURL-pay) extension.
|
||||||
|
|
||||||
|
Exposes the same CRUD surface that `views_api.py` exposes via HTTP, but
|
||||||
|
encrypted over kind-21000 events through the LNbits nostr transport.
|
||||||
|
Mirrors the withdraw extension's `transport_rpcs.py`; both extensions
|
||||||
|
hang their handlers off the core dispatcher via their `*_start()` hook.
|
||||||
|
|
||||||
|
Auth model (set by the registrations in `__init__.py:lnurlp_start`):
|
||||||
|
- *_create / *_get / *_update / *_delete → AUTH_WALLET. The transport
|
||||||
|
resolves the caller's pubkey to a wallet (admin access) before
|
||||||
|
invoking the handler, so we know `auth.wallet.id` and `auth.wallet.user`.
|
||||||
|
- *_list → AUTH_ACCOUNT. The caller may list links across all wallets
|
||||||
|
they own, optionally narrowed by `request.wallet_id`.
|
||||||
|
|
||||||
|
Ownership: *_get / *_update / *_delete also verify the link's stored
|
||||||
|
`wallet` field matches the caller's wallet id — defense in depth, since
|
||||||
|
a malicious client could otherwise probe link metadata they don't own.
|
||||||
|
|
||||||
|
`resolve_lnurlp_owner` is registered with the core subscription module
|
||||||
|
under tag `"lnurlp"` (default link_extra_key `"link"` — that's where
|
||||||
|
`views_lnurl.py:86` stamps the link id on settlement). That lets clients
|
||||||
|
call `subscribe_payments({tag:"lnurlp", link_id:...})` and stream real-
|
||||||
|
time pay events without polling, with ownership enforced server-side.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from lnbits.core.crud.wallets import get_wallets
|
||||||
|
from lnbits.core.models import Account
|
||||||
|
from lnbits.core.models.wallets import WalletTypeInfo
|
||||||
|
from lnbits.core.services.nostr_transport.models import NostrRpcRequest
|
||||||
|
|
||||||
|
from .crud import (
|
||||||
|
create_pay_link,
|
||||||
|
delete_pay_link,
|
||||||
|
get_pay_link,
|
||||||
|
get_pay_links,
|
||||||
|
update_pay_link,
|
||||||
|
)
|
||||||
|
from .models import CreatePayLinkData
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lnurlp_create(
|
||||||
|
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||||
|
) -> dict:
|
||||||
|
body = request.body or {}
|
||||||
|
body["wallet"] = auth.wallet.id # always create under the calling wallet
|
||||||
|
data = CreatePayLinkData(**body)
|
||||||
|
link = await create_pay_link(data)
|
||||||
|
return _to_dict(link)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lnurlp_get(
|
||||||
|
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||||
|
) -> dict:
|
||||||
|
link_id = _require_id(request)
|
||||||
|
link = await _require_owned_link(link_id, auth.wallet.id)
|
||||||
|
return _to_dict(link)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lnurlp_list(
|
||||||
|
auth: Account, request: NostrRpcRequest
|
||||||
|
) -> list[dict]:
|
||||||
|
"""List PayLinks across all wallets owned by the calling account.
|
||||||
|
If `request.wallet_id` is set and is one of those wallets, narrow to
|
||||||
|
just that wallet."""
|
||||||
|
wallets = await get_wallets(auth.id)
|
||||||
|
wallet_ids = [w.id for w in wallets]
|
||||||
|
if not wallet_ids:
|
||||||
|
return []
|
||||||
|
if request.wallet_id and request.wallet_id in wallet_ids:
|
||||||
|
wallet_ids = [request.wallet_id]
|
||||||
|
links = await get_pay_links(wallet_ids)
|
||||||
|
return [_to_dict(link) for link in links]
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lnurlp_update(
|
||||||
|
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||||
|
) -> dict:
|
||||||
|
link_id = _require_id(request)
|
||||||
|
link = await _require_owned_link(link_id, auth.wallet.id)
|
||||||
|
body = request.body or {}
|
||||||
|
# Only patchable fields. Identity / counter fields (id, wallet,
|
||||||
|
# served_meta, served_pr, created_at) are not client-mutable.
|
||||||
|
_MUTABLE = {
|
||||||
|
"description",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"comment_chars",
|
||||||
|
"currency",
|
||||||
|
"webhook_url",
|
||||||
|
"webhook_headers",
|
||||||
|
"webhook_body",
|
||||||
|
"success_text",
|
||||||
|
"success_url",
|
||||||
|
"fiat_base_multiplier",
|
||||||
|
"username",
|
||||||
|
"zaps",
|
||||||
|
"disposable",
|
||||||
|
"domain",
|
||||||
|
}
|
||||||
|
for k, v in body.items():
|
||||||
|
if k in _MUTABLE:
|
||||||
|
setattr(link, k, v)
|
||||||
|
updated = await update_pay_link(link)
|
||||||
|
return _to_dict(updated)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_lnurlp_delete(
|
||||||
|
auth: WalletTypeInfo, request: NostrRpcRequest
|
||||||
|
) -> dict:
|
||||||
|
link_id = _require_id(request)
|
||||||
|
await _require_owned_link(link_id, auth.wallet.id)
|
||||||
|
await delete_pay_link(link_id)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_lnurlp_owner(link_id: str) -> str | None:
|
||||||
|
"""For the core subscription module: link_id -> wallet_id (or None)."""
|
||||||
|
link = await get_pay_link(link_id)
|
||||||
|
return link.wallet if link else None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _require_id(request: NostrRpcRequest) -> str:
|
||||||
|
body = request.body or {}
|
||||||
|
link_id = body.get("id")
|
||||||
|
if not link_id:
|
||||||
|
raise ValueError("lnurlp: body.id is required")
|
||||||
|
return str(link_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _require_owned_link(link_id: str, wallet_id: str):
|
||||||
|
link = await get_pay_link(link_id)
|
||||||
|
if link is None:
|
||||||
|
raise ValueError(f"lnurlp: link not found: {link_id}")
|
||||||
|
if link.wallet != wallet_id:
|
||||||
|
raise PermissionError(
|
||||||
|
"lnurlp: link does not belong to caller's wallet"
|
||||||
|
)
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
def _to_dict(link) -> dict:
|
||||||
|
import json
|
||||||
|
return json.loads(link.json())
|
||||||
50
views.py
50
views.py
|
|
@ -1,45 +1,17 @@
|
||||||
from http import HTTPStatus
|
from fastapi import APIRouter, Depends
|
||||||
|
from lnbits.core.views.generic import index, index_public
|
||||||
from fastapi import APIRouter, Depends, Request
|
|
||||||
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 starlette.exceptions import HTTPException
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from .crud import get_pay_link
|
|
||||||
|
|
||||||
lnurlp_generic_router = APIRouter()
|
lnurlp_generic_router = APIRouter()
|
||||||
|
|
||||||
|
lnurlp_generic_router.add_api_route(
|
||||||
|
"/", methods=["GET"], endpoint=index, dependencies=[Depends(check_user_exists)]
|
||||||
|
)
|
||||||
|
|
||||||
def lnurlp_renderer():
|
lnurlp_generic_router.add_api_route(
|
||||||
return template_renderer(["lnurlp/templates"])
|
"/link/{link_id}", methods=["GET"], endpoint=index_public
|
||||||
|
)
|
||||||
|
|
||||||
|
lnurlp_generic_router.add_api_route(
|
||||||
@lnurlp_generic_router.get("/", response_class=HTMLResponse)
|
"/print/{link_id}", methods=["GET"], endpoint=index_public
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
)
|
||||||
return lnurlp_renderer().TemplateResponse(
|
|
||||||
"lnurlp/index.html", {"request": request, "user": user.json()}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_generic_router.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, "link_id": link.id}
|
|
||||||
return lnurlp_renderer().TemplateResponse("lnurlp/display.html", ctx)
|
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_generic_router.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, "link_id": link.id}
|
|
||||||
return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", ctx)
|
|
||||||
|
|
|
||||||
44
views_api.py
44
views_api.py
|
|
@ -10,6 +10,7 @@ from lnbits.decorators import (
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
|
from lnurl import InvalidUrl
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_pay_link,
|
create_pay_link,
|
||||||
|
|
@ -22,12 +23,30 @@ from .crud import (
|
||||||
update_lnurlp_settings,
|
update_lnurlp_settings,
|
||||||
update_pay_link,
|
update_pay_link,
|
||||||
)
|
)
|
||||||
from .helpers import lnurl_encode_link_id, parse_nostr_private_key
|
from .helpers import lnurl_encode_link, parse_nostr_private_key
|
||||||
from .models import CreatePayLinkData, LnurlpSettings, PayLink
|
from .models import CreatePayLinkData, LnurlpSettings, PayLink, PublicPayLink
|
||||||
|
|
||||||
lnurlp_api_router = APIRouter()
|
lnurlp_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def check_lnurl_encode(req: Request, link: PayLink) -> str:
|
||||||
|
try:
|
||||||
|
return lnurl_encode_link(req, link.id, link.domain)
|
||||||
|
except InvalidUrl as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
detail=(
|
||||||
|
f"Invalid URL for LNURL encoding: `{req.base_url}`. "
|
||||||
|
"Check proxy settings."
|
||||||
|
),
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
) from exc
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Error encoding LNURL.",
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
@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,
|
||||||
|
|
@ -41,11 +60,11 @@ async def api_links(
|
||||||
|
|
||||||
links = await get_pay_links(wallet_ids)
|
links = await get_pay_links(wallet_ids)
|
||||||
for link in links:
|
for link in links:
|
||||||
link.lnurl = lnurl_encode_link_id(req=req, link_id=link.id)
|
link.lnurl = check_lnurl_encode(req, link)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
|
|
||||||
@lnurlp_api_router.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
@lnurlp_api_router.get("/api/v1/links/{link_id}")
|
||||||
async def api_link_retrieve(
|
async def api_link_retrieve(
|
||||||
req: Request, link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key)
|
req: Request, link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
) -> PayLink:
|
) -> PayLink:
|
||||||
|
|
@ -66,7 +85,18 @@ async def api_link_retrieve(
|
||||||
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
link.lnurl = lnurl_encode_link_id(req, link.id)
|
link.lnurl = check_lnurl_encode(req, link)
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
@lnurlp_api_router.get("/api/v1/links/public/{link_id}", response_model=PublicPayLink)
|
||||||
|
async def api_link_public_retrieve(req: Request, link_id: str) -> PayLink:
|
||||||
|
link = await get_pay_link(link_id)
|
||||||
|
if not link:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
|
||||||
|
)
|
||||||
|
link.lnurl = lnurl_encode_link(req, link.id, link.domain)
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -149,7 +179,7 @@ async def api_link_create_or_update(
|
||||||
detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN
|
detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
# admins are allowed to create/edit paylinks beloging to regular users
|
# admins are allowed to create/edit paylinks belonging to regular users
|
||||||
user = await get_user(key_info.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 new_wallet.user != key_info.wallet.user:
|
if not admin_user and new_wallet.user != key_info.wallet.user:
|
||||||
|
|
@ -178,7 +208,7 @@ async def api_link_create_or_update(
|
||||||
|
|
||||||
link = await create_pay_link(data)
|
link = await create_pay_link(data)
|
||||||
|
|
||||||
link.lnurl = lnurl_encode_link_id(req, link.id)
|
link.lnurl = check_lnurl_encode(req, link)
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ async def api_lnurl_callback(
|
||||||
extra["nostr"] = nostr # put it here for later publishing in tasks.py
|
extra["nostr"] = nostr # put it here for later publishing in tasks.py
|
||||||
|
|
||||||
if link.username:
|
if link.username:
|
||||||
identifier = f"{link.username}@{request.url.netloc}"
|
identifier = f"{link.username}@{link.domain or request.url.netloc}"
|
||||||
text = f"Payment to {link.username}"
|
text = f"Payment to {link.username}"
|
||||||
_metadata = [["text/plain", text], ["text/identifier", identifier]]
|
_metadata = [["text/plain", text], ["text/identifier", identifier]]
|
||||||
extra["lnaddress"] = identifier
|
extra["lnaddress"] = identifier
|
||||||
|
|
@ -173,7 +173,7 @@ async def api_lnurl_response(
|
||||||
callback_url = parse_obj_as(CallbackUrl, str(url))
|
callback_url = parse_obj_as(CallbackUrl, str(url))
|
||||||
|
|
||||||
if link.username:
|
if link.username:
|
||||||
identifier = f"{link.username}@{request.url.netloc}"
|
identifier = f"{link.username}@{link.domain or request.url.netloc}"
|
||||||
text = f"Payment to {link.username}"
|
text = f"Payment to {link.username}"
|
||||||
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
metadata = [["text/plain", text], ["text/identifier", identifier]]
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue