From 4211998959b26a592ba3d8fef1717b5fa5b6cce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 20 Feb 2023 09:08:23 +0100 Subject: [PATCH] remove lnaddress (#1525) * remove lnaddress --- lnbits/core/views/public_api.py | 10 - lnbits/extensions/lnaddress/README.md | 68 --- lnbits/extensions/lnaddress/__init__.py | 35 -- lnbits/extensions/lnaddress/cloudflare.py | 54 -- lnbits/extensions/lnaddress/config.json | 6 - lnbits/extensions/lnaddress/crud.py | 198 ------- lnbits/extensions/lnaddress/lnurl.py | 81 --- lnbits/extensions/lnaddress/migrations.py | 39 -- lnbits/extensions/lnaddress/models.py | 57 -- .../lnaddress/static/image/lnaddress.png | Bin 21553 -> 0 bytes lnbits/extensions/lnaddress/tasks.py | 64 --- .../templates/lnaddress/_api_docs.html | 184 ------- .../templates/lnaddress/display.html | 435 --------------- .../lnaddress/templates/lnaddress/index.html | 504 ------------------ lnbits/extensions/lnaddress/views.py | 49 -- lnbits/extensions/lnaddress/views_api.py | 258 --------- tests/core/views/test_public_api.py | 10 - 17 files changed, 2052 deletions(-) delete mode 100644 lnbits/extensions/lnaddress/README.md delete mode 100644 lnbits/extensions/lnaddress/__init__.py delete mode 100644 lnbits/extensions/lnaddress/cloudflare.py delete mode 100644 lnbits/extensions/lnaddress/config.json delete mode 100644 lnbits/extensions/lnaddress/crud.py delete mode 100644 lnbits/extensions/lnaddress/lnurl.py delete mode 100644 lnbits/extensions/lnaddress/migrations.py delete mode 100644 lnbits/extensions/lnaddress/models.py delete mode 100644 lnbits/extensions/lnaddress/static/image/lnaddress.png delete mode 100644 lnbits/extensions/lnaddress/tasks.py delete mode 100644 lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html delete mode 100644 lnbits/extensions/lnaddress/templates/lnaddress/display.html delete mode 100644 lnbits/extensions/lnaddress/templates/lnaddress/index.html delete mode 100644 lnbits/extensions/lnaddress/views.py delete mode 100644 lnbits/extensions/lnaddress/views_api.py diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py index b5773bbe9..303929feb 100644 --- a/lnbits/core/views/public_api.py +++ b/lnbits/core/views/public_api.py @@ -1,11 +1,9 @@ import asyncio import datetime from http import HTTPStatus -from urllib.parse import urlparse from fastapi import HTTPException from loguru import logger -from starlette.requests import Request from lnbits import bolt11 @@ -14,14 +12,6 @@ from ..crud import get_standalone_payment from ..tasks import api_invoice_listeners -@core_app.get("/.well-known/lnurlp/{username}") -async def lnaddress(username: str, request: Request): - from lnbits.extensions.lnaddress.lnurl import lnurl_response # type: ignore - - domain = urlparse(str(request.url)).netloc - return await lnurl_response(username, domain, request) - - @core_app.get("/public/v1/payment/{payment_hash}") async def api_public_payment_longpolling(payment_hash): payment = await get_standalone_payment(payment_hash) diff --git a/lnbits/extensions/lnaddress/README.md b/lnbits/extensions/lnaddress/README.md deleted file mode 100644 index d7e405033..000000000 --- a/lnbits/extensions/lnaddress/README.md +++ /dev/null @@ -1,68 +0,0 @@ -

Lightning Address

-

Rent Lightning Addresses on your domain

-LNAddress extension allows for someone to rent users lightning addresses on their domain. - -The extension is muted by default on the .env file and needs the admin of the LNbits instance to meet a few requirements on the server. - -## Requirements - -- Free Cloudflare account -- Cloudflare as a DNS server provider -- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked - -The server must provide SSL/TLS certificates to domain owners. If using caddy, this can be easily achieved with the Caddyfife snippet: - -``` -:443 { - reverse_proxy localhost:5000 - - tls @example.com { - on_demand - } -} -``` - -fill in with your email. - -Certbot is also a possibity. - -## Usage - -1. Before adding a domain, you need to add the domain to Cloudflare and get an API key and Secret key\ - ![add domain to Cloudflare](https://i.imgur.com/KTJK7uT.png)\ - You can use the _Edit zone DNS_ template Cloudflare provides.\ - ![DNS template](https://i.imgur.com/ciRXuGd.png)\ - Edit the template as you like, if only using one domain you can narrow the scope of the template\ - ![edit template](https://i.imgur.com/NCUF72C.png) - -2. Back on LNbits, click "ADD DOMAIN"\ - ![add domain](https://i.imgur.com/9Ed3NX4.png) - -3. Fill the form with the domain information\ - ![fill form](https://i.imgur.com/JMcXXbS.png) - - - select your wallet - add your domain - - cloudflare keys - - an optional webhook to get notified - - the amount, in sats, you'll rent the addresses, per day - -4. Your domains will show up on the _Domains_ section\ - ![domains card](https://i.imgur.com/Fol1Arf.png)\ - On the left side, is the link to share with users so they can rent an address on your domain. When someone creates an address, after pay, they will be shown on the _Addresses_ section\ - ![address card](https://i.imgur.com/judrIeo.png) - -5. Addresses get automatically purged if expired or unpaid, after 24 hours. After expiration date, users will be granted a 24 hours period to renew their address! - -6. On the user/buyer side, the webpage will present the _Create_ or _Renew_ address tabs. On the Create tab:\ - ![create address](https://i.imgur.com/lSYWGeT.png) - - optional email - - the alias or username they want on your domain - - the LNbits URL, if not the same instance (for example the user has an LNbits wallet on https://s.lnbits.com and is renting an address from https://lnbits.com) - - the _Admin key_ for the wallet - - how many days to rent a username for - bellow shows the per day cost and total cost the user will have to pay -7. On the Renew tab:\ - ![renew address](https://i.imgur.com/rzU46ps.png) - - enter the Alias/username - - enter the wallet key - - press the _GET INFO_ button to retrieve your address data - - an expiration date will appear and the option to extend the duration of your address diff --git a/lnbits/extensions/lnaddress/__init__.py b/lnbits/extensions/lnaddress/__init__.py deleted file mode 100644 index dcc4a9516..000000000 --- a/lnbits/extensions/lnaddress/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio - -from fastapi import APIRouter -from starlette.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_lnaddress") - -lnaddress_ext: APIRouter = APIRouter(prefix="/lnaddress", tags=["lnaddress"]) - -lnaddress_static_files = [ - { - "path": "/lnaddress/static", - "app": StaticFiles(directory="lnbits/extensions/lnaddress/static"), - "name": "lnaddress_static", - } -] - - -def lnaddress_renderer(): - return template_renderer(["lnbits/extensions/lnaddress/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 lnaddress_start(): - loop = asyncio.get_event_loop() - loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/lnaddress/cloudflare.py b/lnbits/extensions/lnaddress/cloudflare.py deleted file mode 100644 index 558bca7d0..000000000 --- a/lnbits/extensions/lnaddress/cloudflare.py +++ /dev/null @@ -1,54 +0,0 @@ -import httpx - -from .models import Domains - - -async def cloudflare_create_record(domain: Domains, ip: str): - url = ( - "https://api.cloudflare.com/client/v4/zones/" - + domain.cf_zone_id - + "/dns_records" - ) - header = { - "Authorization": "Bearer " + domain.cf_token, - "Content-Type": "application/json", - } - - cf_response = {} - async with httpx.AsyncClient() as client: - try: - r = await client.post( - url, - headers=header, - json={ - "type": "CNAME", - "name": domain.domain, - "content": ip, - "ttl": 0, - "proxied": False, - }, - timeout=40, - ) - cf_response = r.json() - except AssertionError: - cf_response = {"error": "Error occured"} - return cf_response - - -async def cloudflare_deleterecord(domain: Domains, domain_id: str): - url = ( - "https://api.cloudflare.com/client/v4/zones/" - + domain.cf_zone_id - + "/dns_records" - ) - header = { - "Authorization": "Bearer " + domain.cf_token, - "Content-Type": "application/json", - } - async with httpx.AsyncClient() as client: - try: - r = await client.delete(url + "/" + domain_id, headers=header, timeout=40) - cf_response = r.text - except AssertionError: - cf_response = "Error occured" - return cf_response diff --git a/lnbits/extensions/lnaddress/config.json b/lnbits/extensions/lnaddress/config.json deleted file mode 100644 index 5eaa49482..000000000 --- a/lnbits/extensions/lnaddress/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Lightning Address", - "short_description": "Sell LN addresses for your domain", - "tile": "/lnaddress/static/image/lnaddress.png", - "contributors": ["talvasconcelos"] -} diff --git a/lnbits/extensions/lnaddress/crud.py b/lnbits/extensions/lnaddress/crud.py deleted file mode 100644 index a0201ee64..000000000 --- a/lnbits/extensions/lnaddress/crud.py +++ /dev/null @@ -1,198 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Optional, Union - -from loguru import logger - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import Addresses, CreateAddress, CreateDomain, Domains - - -async def create_domain(data: CreateDomain) -> Domains: - domain_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO lnaddress.domain (id, wallet, domain, webhook, cf_token, cf_zone_id, cost) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - ( - domain_id, - data.wallet, - data.domain, - data.webhook, - data.cf_token, - data.cf_zone_id, - data.cost, - ), - ) - - new_domain = await get_domain(domain_id) - assert new_domain, "Newly created domain couldn't be retrieved" - return new_domain - - -async def update_domain(domain_id: str, **kwargs) -> Domains: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE lnaddress.domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id) - ) - row = await db.fetchone("SELECT * FROM lnaddress.domain WHERE id = ?", (domain_id,)) - assert row, "Newly updated domain couldn't be retrieved" - return Domains(**row) - - -async def delete_domain(domain_id: str) -> None: - - await db.execute("DELETE FROM lnaddress.domain WHERE id = ?", (domain_id,)) - - -async def get_domain(domain_id: str) -> Optional[Domains]: - row = await db.fetchone("SELECT * FROM lnaddress.domain WHERE id = ?", (domain_id,)) - return Domains(**row) if row else None - - -async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM lnaddress.domain WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [Domains(**row) for row in rows] - - -## ADRESSES - - -async def create_address( - payment_hash: str, wallet: str, data: CreateAddress -) -> Addresses: - await db.execute( - """ - INSERT INTO lnaddress.address (id, wallet, domain, email, username, wallet_key, wallet_endpoint, sats, duration, paid) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - payment_hash, - wallet, - data.domain, - data.email, - data.username, - data.wallet_key, - data.wallet_endpoint, - data.sats, - data.duration, - False, - ), - ) - - new_address = await get_address(payment_hash) - assert new_address, "Newly created address couldn't be retrieved" - return new_address - - -async def get_address(address_id: str) -> Optional[Addresses]: - row = await db.fetchone( - "SELECT a.* FROM lnaddress.address AS a INNER JOIN lnaddress.domain AS d ON a.id = ? AND a.domain = d.id", - (address_id,), - ) - return Addresses(**row) if row else None - - -async def get_address_by_username(username: str, domain: str) -> Optional[Addresses]: - row = await db.fetchone( - "SELECT a.* FROM lnaddress.address AS a INNER JOIN lnaddress.domain AS d ON a.username = ? AND d.domain = ?", - (username, domain), - ) - - return Addresses(**row) if row else None - - -async def delete_address(address_id: str) -> None: - await db.execute("DELETE FROM lnaddress.address WHERE id = ?", (address_id,)) - - -async def get_addresses(wallet_ids: Union[str, List[str]]) -> List[Addresses]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM lnaddress.address WHERE wallet IN ({q})", (*wallet_ids,) - ) - return [Addresses(**row) for row in rows] - - -async def set_address_paid(payment_hash: str) -> Addresses: - address = await get_address(payment_hash) - assert address - - if address.paid is False: - await db.execute( - """ - UPDATE lnaddress.address - SET paid = true - WHERE id = ? - """, - (payment_hash,), - ) - - new_address = await get_address(payment_hash) - assert new_address, "Newly paid address couldn't be retrieved" - return new_address - - -async def set_address_renewed(address_id: str, duration: int): - address = await get_address(address_id) - assert address - - extend_duration = int(address.duration) + duration - await db.execute( - """ - UPDATE lnaddress.address - SET duration = ? - WHERE id = ? - """, - (extend_duration, address_id), - ) - updated_address = await get_address(address_id) - assert updated_address, "Renewed address couldn't be retrieved" - return updated_address - - -async def check_address_available(username: str, domain: str): - (row,) = await db.fetchone( - "SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?", - (username, domain), - ) - return row - - -async def purge_addresses(domain_id: str): - - rows = await db.fetchall( - "SELECT * FROM lnaddress.address WHERE domain = ?", (domain_id,) - ) - - now = datetime.now().timestamp() - - for row in rows: - r = Addresses(**row).dict() - - start = datetime.fromtimestamp(r["time"]) - paid = r["paid"] - pay_expire = now > start.timestamp() + 86400 # if payment wasn't made in 1 day - expired = ( - now > (start + timedelta(days=r["duration"] + 1)).timestamp() - ) # give user 1 day to topup is address - - if not paid and pay_expire: - logger.debug("DELETE UNP_PAY_EXP", r["username"]) - await delete_address(r["id"]) - - if paid and expired: - logger.debug("DELETE PAID_EXP", r["username"]) - await delete_address(r["id"]) diff --git a/lnbits/extensions/lnaddress/lnurl.py b/lnbits/extensions/lnaddress/lnurl.py deleted file mode 100644 index b38da954b..000000000 --- a/lnbits/extensions/lnaddress/lnurl.py +++ /dev/null @@ -1,81 +0,0 @@ -from datetime import datetime, timedelta - -import httpx -from fastapi import Query, Request -from lnurl import LnurlErrorResponse -from loguru import logger - -from . import lnaddress_ext -from .crud import get_address, get_address_by_username, get_domain - - -async def lnurl_response(username: str, domain: str, request: Request): - address = await get_address_by_username(username, domain) - - if not address: - return {"status": "ERROR", "reason": "Address not found."} - - ## CHECK IF USER IS STILL VALID/PAYING - now = datetime.now().timestamp() - start = datetime.fromtimestamp(address.time) - expiration = (start + timedelta(days=address.duration)).timestamp() - - if now > expiration: - return LnurlErrorResponse(reason="Address has expired.").dict() - - resp = { - "tag": "payRequest", - "callback": request.url_for("lnaddress.lnurl_callback", address_id=address.id), - "metadata": await address.lnurlpay_metadata(domain=domain), - "minSendable": 1000, - "maxSendable": 1000000000, - } - - logger.debug("RESP", resp) - return resp - - -@lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback") -async def lnurl_callback(address_id, amount: int = Query(...)): - address = await get_address(address_id) - if not address: - return LnurlErrorResponse(reason="Address not found").dict() - - amount_received = amount - - domain = await get_domain(address.domain) - assert domain - - base_url = ( - address.wallet_endpoint[:-1] - if address.wallet_endpoint.endswith("/") - else address.wallet_endpoint - ) - - async with httpx.AsyncClient() as client: - try: - call = await client.post( - base_url + "/api/v1/payments", - headers={ - "X-Api-Key": address.wallet_key, - "Content-Type": "application/json", - }, - json={ - "out": False, - "amount": int(amount_received / 1000), - "description_hash": ( - await address.lnurlpay_metadata(domain=domain.domain) - ).encode(), - "extra": {"tag": f"Payment to {address.username}@{domain.domain}"}, - }, - timeout=40, - ) - - r = call.json() - except Exception: - return LnurlErrorResponse(reason="ERROR") - - # resp = LnurlPayActionResponse(pr=r["payment_request"], routes=[]) - resp = {"pr": r["payment_request"], "routes": []} - - return resp diff --git a/lnbits/extensions/lnaddress/migrations.py b/lnbits/extensions/lnaddress/migrations.py deleted file mode 100644 index 1724e1865..000000000 --- a/lnbits/extensions/lnaddress/migrations.py +++ /dev/null @@ -1,39 +0,0 @@ -async def m001_initial(db): - await db.execute( - """ - CREATE TABLE lnaddress.domain ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - domain TEXT NOT NULL, - webhook TEXT, - cf_token TEXT NOT NULL, - cf_zone_id TEXT NOT NULL, - cost INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - -async def m002_addresses(db): - await db.execute( - """ - CREATE TABLE lnaddress.address ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - domain TEXT NOT NULL, - email TEXT, - username TEXT NOT NULL, - wallet_key TEXT NOT NULL, - wallet_endpoint TEXT NOT NULL, - sats INTEGER NOT NULL, - duration INTEGER NOT NULL, - paid BOOLEAN NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) diff --git a/lnbits/extensions/lnaddress/models.py b/lnbits/extensions/lnaddress/models.py deleted file mode 100644 index 77eb3cd3f..000000000 --- a/lnbits/extensions/lnaddress/models.py +++ /dev/null @@ -1,57 +0,0 @@ -import json -from typing import Optional - -from fastapi import Query -from lnurl.types import LnurlPayMetadata -from pydantic import BaseModel - - -class CreateDomain(BaseModel): - wallet: str = Query(...) - domain: str = Query(...) - cf_token: str = Query(...) - cf_zone_id: str = Query(...) - webhook: str = Query(None) - cost: int = Query(..., ge=0) - - -class Domains(BaseModel): - id: str - wallet: str - domain: str - cf_token: str - cf_zone_id: str - webhook: Optional[str] - cost: int - time: int - - -class CreateAddress(BaseModel): - domain: str = Query(...) - username: str = Query(...) - email: str = Query(None) - wallet_endpoint: str = Query(...) - wallet_key: str = Query(...) - sats: int = Query(..., ge=0) - duration: int = Query(..., ge=1) - - -class Addresses(BaseModel): - id: str - wallet: str - domain: str - email: Optional[str] - username: str - wallet_key: str - wallet_endpoint: str - sats: int - duration: int - paid: bool - time: int - - async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata: - text = f"Payment to {self.username}" - identifier = f"{self.username}@{domain}" - metadata = [["text/plain", text], ["text/identifier", identifier]] - - return LnurlPayMetadata(json.dumps(metadata)) diff --git a/lnbits/extensions/lnaddress/static/image/lnaddress.png b/lnbits/extensions/lnaddress/static/image/lnaddress.png deleted file mode 100644 index c94dedbc43d2a471ae98c3c988506990367e2f24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21553 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 T!=p4o00000NkvXXu0mjf9?~Da diff --git a/lnbits/extensions/lnaddress/tasks.py b/lnbits/extensions/lnaddress/tasks.py deleted file mode 100644 index 3699c4630..000000000 --- a/lnbits/extensions/lnaddress/tasks.py +++ /dev/null @@ -1,64 +0,0 @@ -import asyncio - -import httpx -from loguru import logger - -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_address, get_domain, set_address_paid, set_address_renewed - - -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 call_webhook_on_paid(payment_hash): - ### Use webhook to notify about cloudflare registration - address = await get_address(payment_hash) - assert address - domain = await get_domain(address.domain) - assert domain - - if not domain.webhook: - return - - async with httpx.AsyncClient() as client: - try: - r = await client.post( - domain.webhook, - json={ - "domain": domain.domain, - "address": address.username, - "email": address.email, - "cost": str(address.sats) + " sats", - "duration": str(address.duration) + " days", - }, - timeout=40, - ) - r.raise_for_status() - except Exception as e: - logger.error(f"lnaddress: error calling webhook on paid: {str(e)}") - - -async def on_invoice_paid(payment: Payment) -> None: - - if payment.extra.get("tag") == "lnaddress": - await payment.set_pending(False) - await set_address_paid(payment_hash=payment.payment_hash) - await call_webhook_on_paid(payment_hash=payment.payment_hash) - - elif payment.extra.get("tag") == "renew lnaddress": - await payment.set_pending(False) - await set_address_renewed( - address_id=payment.extra["id"], duration=payment.extra["duration"] - ) - await call_webhook_on_paid(payment_hash=payment.payment_hash) - else: - return diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html b/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html deleted file mode 100644 index f4d1f651d..000000000 --- a/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html +++ /dev/null @@ -1,184 +0,0 @@ - - - -
- lnAddress: Get paid sats to sell lightning addresses on your domains -
-

- Charge people for using your domain name...
- - More details -
- - Created by, - talvasconcelos -

-
-
-
- - - - - - GET - lnaddress/api/v1/domains -
Body (application/json)
-
- Returns 200 OK (application/json) -
- JSON list of users -
Curl example
- curl -X GET {{ request.base_url }}lnaddress/api/v1/domains -H - "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - POST - /lnAddress/api/v1/domains -
Headers
- {"X-Api-Key": <string>, "Content-type": - "application/json"} -
- Body (application/json) - "wallet" is a YOUR wallet ID -
- {"wallet": <string>, "domain": <string>, "cf_token": - <string>,"cf_zone_id": <string>,"webhook": <Optional - string> ,"cost": <integer>} -
- Returns 201 CREATED (application/json) -
- {"id": <string>, "wallet": <string>, "domain": - <string>, "webhook": <string>, "cf_token": <string>, - "cf_zone_id": <string>, "cost": <integer>} -
Curl example
- curl -X POST {{ request.base_url }}lnaddress/api/v1/domains -d - '{"wallet": "{{ user.wallets[0].id }}", "domain": <string>, - "cf_token": <string>,"cf_zone_id": <string>,"webhook": - <Optional string> ,"cost": <integer>}' -H "X-Api-Key: {{ - user.wallets[0].inkey }}" -H "Content-type: application/json" - -
-
-
- - - - DELETE - /lnaddress/api/v1/domains/<domain_id> -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X DELETE {{ request.base_url - }}lnaddress/api/v1/domains/<domain_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" - -
-
-
- - - - GET - lnaddress/api/v1/addresses -
Body (application/json)
-
- Returns 200 OK (application/json) -
- JSON list of addresses -
Curl example
- curl -X GET {{ request.base_url }}lnaddress/api/v1/addresses -H - "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - GET - lnaddress/api/v1/address/<domain>/<username>/<wallet_key> -
Body (application/json)
-
- Returns 200 OK (application/json) -
- JSON list of addresses -
Curl example
- curl -X GET {{ request.base_url - }}lnaddress/api/v1/address/<domain>/<username>/<wallet_key> - -H "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - POST - /lnaddress/api/v1/address/<domain_id> -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X POST {{ request.base_url - }}lnaddress/api/v1/address/<domain_id> -d '{"domain": - <string>, "username": <string>,"email": <Optional - string>, "wallet_endpoint": <string>, "wallet_key": - <string>, "sats": <integer> "duration": <integer>,}' - -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type: - application/json" - -
-
-
-
diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/display.html b/lnbits/extensions/lnaddress/templates/lnaddress/display.html deleted file mode 100644 index 2dca8f488..000000000 --- a/lnbits/extensions/lnaddress/templates/lnaddress/display.html +++ /dev/null @@ -1,435 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- -
-
- - - - -
-
- - - - -

{{ domain_domain }}

-
-
- Your Lightning Address: {% raw - %}{{this.formDialog.data.username}}{% endraw %}@{{domain_domain}} -
-
- - - - - - - - - - -

- Cost per day: {{ domain_cost }} sats
- {% raw %} Total cost: {{amountSats}} sats {% endraw %} -

-
- Submit - Cancel -
-
-
-
- - -

{{ domain_domain }}

-
-
- Renew your Lightning Address: {% raw - %}{{this.formDialog.data.username}}{% endraw %}@{{domain_domain}} -
-
- - - - - -
-
- {% raw %} -

- LN Address: - {{renewDialog.data.username}}@{{renewDialog.data.domain}} -
- Expires at: {{renewDialog.data.expiration}} -

- {% endraw %} -
- Get Info -
- - -

- Cost per day: {{ domain_cost }} sats
- {% raw %} Total cost: {{amountSats}} sats {% endraw %} -

-
- Submit - Cancel -
-
-
-
-
-
-
- - - - - - -
- Copy invoice - Close -
-
-
-
- -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/index.html b/lnbits/extensions/lnaddress/templates/lnaddress/index.html deleted file mode 100644 index 41602581a..000000000 --- a/lnbits/extensions/lnaddress/templates/lnaddress/index.html +++ /dev/null @@ -1,504 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - Add Domain - - - - -
-
-
Domains
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - -
-
-
Addresses
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
-
- - -
- {{SITE_TITLE}} LN Address extension -
-
- - - {% include "lnaddress/_api_docs.html" %} - -
-
- - - - - - The domain to use ex: "example.com" - - - Your API key in cloudflare - - - Create a "Edit zone DNS" API token in cloudflare - - - How much to charge per day -
- Update Form - Create Domain - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/lnaddress/views.py b/lnbits/extensions/lnaddress/views.py deleted file mode 100644 index d1a7be83c..000000000 --- a/lnbits/extensions/lnaddress/views.py +++ /dev/null @@ -1,49 +0,0 @@ -from http import HTTPStatus -from urllib.parse import urlparse - -from fastapi import Depends, HTTPException, Request -from fastapi.templating import Jinja2Templates -from starlette.responses import HTMLResponse - -from lnbits.core.crud import get_wallet -from lnbits.core.models import User -from lnbits.decorators import check_user_exists - -from . import lnaddress_ext, lnaddress_renderer -from .crud import get_domain, purge_addresses - -templates = Jinja2Templates(directory="templates") - - -@lnaddress_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return lnaddress_renderer().TemplateResponse( - "lnaddress/index.html", {"request": request, "user": user.dict()} - ) - - -@lnaddress_ext.get("/{domain_id}", response_class=HTMLResponse) -async def display(domain_id, request: Request): - domain = await get_domain(domain_id) - if not domain: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist." - ) - - await purge_addresses(domain_id) - - wallet = await get_wallet(domain.wallet) - assert wallet - url = urlparse(str(request.url)) - - return lnaddress_renderer().TemplateResponse( - "lnaddress/display.html", - { - "request": request, - "domain_id": domain.id, - "domain_domain": domain.domain, - "domain_cost": domain.cost, - "domain_wallet_inkey": wallet.inkey, - "root_url": f"{url.scheme}://{url.netloc}", - }, - ) diff --git a/lnbits/extensions/lnaddress/views_api.py b/lnbits/extensions/lnaddress/views_api.py deleted file mode 100644 index cdf5b91fb..000000000 --- a/lnbits/extensions/lnaddress/views_api.py +++ /dev/null @@ -1,258 +0,0 @@ -from http import HTTPStatus -from urllib.parse import urlparse - -from fastapi import Depends, HTTPException, Query, Request -from loguru import logger - -from lnbits.core.crud import get_user -from lnbits.core.services import check_transaction_status, create_invoice -from lnbits.decorators import WalletTypeInfo, get_key_type - -from . import lnaddress_ext -from .cloudflare import cloudflare_create_record -from .crud import ( - check_address_available, - create_address, - create_domain, - delete_address, - delete_domain, - get_address, - get_address_by_username, - get_addresses, - get_domain, - get_domains, - update_domain, -) -from .models import CreateAddress, CreateDomain - - -# DOMAINS -@lnaddress_ext.get("/api/v1/domains") -async def api_domains( - g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) -): - wallet_ids = [g.wallet.id] - - if all_wallets: - user = await get_user(g.wallet.user) - wallet_ids = user.wallet_ids if user else [] - - return [domain.dict() for domain in await get_domains(wallet_ids)] - - -@lnaddress_ext.post("/api/v1/domains") -@lnaddress_ext.put("/api/v1/domains/{domain_id}") -async def api_domain_create( - request: Request, - data: CreateDomain, - domain_id=None, - g: WalletTypeInfo = Depends(get_key_type), -): - if domain_id: - domain = await get_domain(domain_id) - - if not domain: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist." - ) - - if domain.wallet != g.wallet.id: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not your domain" - ) - - domain = await update_domain(domain_id, **data.dict()) - else: - - domain = await create_domain(data=data) - root_url = urlparse(str(request.url)).netloc - - cf_response = await cloudflare_create_record(domain=domain, ip=root_url) - - if not cf_response or not cf_response["success"]: - await delete_domain(domain.id) - logger.error("Cloudflare failed with: " + cf_response["errors"][0]["message"]) # type: ignore - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Problem with cloudflare." - ) - - return domain.dict() - - -@lnaddress_ext.delete("/api/v1/domains/{domain_id}") -async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)): - domain = await get_domain(domain_id) - - if not domain: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist." - ) - - if domain.wallet != g.wallet.id: - raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain") - - await delete_domain(domain_id) - return "", HTTPStatus.NO_CONTENT - - -# ADDRESSES - - -@lnaddress_ext.get("/api/v1/addresses") -async def api_addresses( - g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) -): - wallet_ids = [g.wallet.id] - - if all_wallets: - user = await get_user(g.wallet.user) - wallet_ids = user.wallet_ids if user else [] - - return [address.dict() for address in await get_addresses(wallet_ids)] - - -@lnaddress_ext.get("/api/v1/address/availabity/{domain_id}/{username}") -async def api_check_available_username(domain_id, username): - used_username = await check_address_available(username, domain_id) - - return used_username - - -@lnaddress_ext.get("/api/v1/address/{domain}/{username}/{wallet_key}") -async def api_get_user_info(username, wallet_key, domain): - address = await get_address_by_username(username, domain) - - if not address: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Address does not exist." - ) - - if address.wallet_key != wallet_key: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="Incorrect user/wallet information.", - ) - - return address.dict() - - -@lnaddress_ext.post("/api/v1/address/{domain_id}") -@lnaddress_ext.put("/api/v1/address/{domain_id}/{user}/{wallet_key}") -async def api_lnaddress_make_address( - domain_id, data: CreateAddress, user=None, wallet_key=None -): - domain = await get_domain(domain_id) - - # If the request is coming for the non-existant domain - if not domain: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="The domain does not exist." - ) - - domain_cost = domain.cost - sats = data.sats - - ## FAILSAFE FOR CREATING ADDRESSES BY API - if domain_cost * data.duration != data.sats: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="The amount is not correct. Either 'duration', or 'sats' are wrong.", - ) - - if user: - address = await get_address_by_username(user, domain.domain) - - if not address: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="The address does not exist." - ) - - if address.wallet_key != wallet_key: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not your address." - ) - - try: - payment_hash, payment_request = await create_invoice( - wallet_id=domain.wallet, - amount=data.sats, - memo=f"Renew {data.username}@{domain.domain} for {sats} sats for {data.duration} more days", - extra={ - "tag": "renew lnaddress", - "id": address.id, - "duration": data.duration, - }, - ) - - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) - ) - else: - used_username = await check_address_available(data.username, data.domain) - # If username is already taken - if used_username: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Alias/username already taken.", - ) - - ## ALL OK - create an invoice and return it to the user - - try: - payment_hash, payment_request = await create_invoice( - wallet_id=domain.wallet, - amount=sats, - memo=f"LNAddress {data.username}@{domain.domain} for {sats} sats for {data.duration} days", - extra={"tag": "lnaddress"}, - ) - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) - ) - - address = await create_address( - payment_hash=payment_hash, wallet=domain.wallet, data=data - ) - - if not address: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail="LNAddress could not be fetched.", - ) - - return {"payment_hash": payment_hash, "payment_request": payment_request} - - -@lnaddress_ext.get("/api/v1/addresses/{payment_hash}") -async def api_address_send_address(payment_hash): - address = await get_address(payment_hash) - assert address - domain = await get_domain(address.domain) - assert domain - try: - status = await check_transaction_status(domain.wallet, payment_hash) - is_paid = not status.pending - except Exception as e: - return {"paid": False, "error": str(e)} - - if is_paid: - return {"paid": True} - - return {"paid": False} - - -@lnaddress_ext.delete("/api/v1/addresses/{address_id}") -async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_type)): - address = await get_address(address_id) - if not address: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Address does not exist." - ) - if address.wallet != g.wallet.id: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not your address." - ) - - await delete_address(address_id) - return "", HTTPStatus.NO_CONTENT diff --git a/tests/core/views/test_public_api.py b/tests/core/views/test_public_api.py index 144cd161e..9c351cf1a 100644 --- a/tests/core/views/test_public_api.py +++ b/tests/core/views/test_public_api.py @@ -24,13 +24,3 @@ async def test_api_public_payment_longpolling_wrong_hash(client, invoice): ) assert response.status_code == 404 assert response.json()["detail"] == "Payment does not exist." - - -# check GET /.well-known/lnurlp/{username}: wrong username [should fail] -@pytest.mark.asyncio -async def test_lnaddress_wrong_hash(client): - username = "wrong_name" - response = await client.get(f"/.well-known/lnurlp/{username}") - assert response.status_code == 200 - assert response.json()["status"] == "ERROR" - assert response.json()["reason"] == "Address not found."