diff --git a/docs/devs/installation.md b/docs/devs/installation.md
index 224aedd..81ae2a4 100644
--- a/docs/devs/installation.md
+++ b/docs/devs/installation.md
@@ -23,6 +23,9 @@ $ pipenv shell
$ pipenv install --dev
```
+If any of the modules fails to install, try checking and upgrading your setupTool module.
+`pip install -U setuptools`
+
If you wish to use a version of Python higher than 3.7:
```sh
diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py
index b66ab9b..5b0d572 100644
--- a/lnbits/core/crud.py
+++ b/lnbits/core/crud.py
@@ -285,7 +285,11 @@ async def create_payment(
async def update_payment_status(checking_id: str, pending: bool) -> None:
await db.execute(
- "UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,),
+ "UPDATE apipayments SET pending = ? WHERE checking_id = ?",
+ (
+ int(pending),
+ checking_id,
+ ),
)
diff --git a/lnbits/core/models.py b/lnbits/core/models.py
index a79d73f..9e37baa 100644
--- a/lnbits/core/models.py
+++ b/lnbits/core/models.py
@@ -40,7 +40,11 @@ class Wallet(NamedTuple):
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
- return SigningKey.from_string(linking_key, curve=SECP256k1, hashfunc=hashlib.sha256,)
+ return SigningKey.from_string(
+ linking_key,
+ curve=SECP256k1,
+ hashfunc=hashlib.sha256,
+ )
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
from .crud import get_wallet_payment
diff --git a/lnbits/core/services.py b/lnbits/core/services.py
index a5f7d99..82409b0 100644
--- a/lnbits/core/services.py
+++ b/lnbits/core/services.py
@@ -133,7 +133,10 @@ async def pay_invoice(
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
if payment.ok and payment.checking_id:
await create_payment(
- checking_id=payment.checking_id, fee=payment.fee_msat, preimage=payment.preimage, **payment_kwargs,
+ checking_id=payment.checking_id,
+ fee=payment.fee_msat,
+ preimage=payment.preimage,
+ **payment_kwargs,
)
await delete_payment(temp_id)
else:
@@ -153,7 +156,8 @@ async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo
async with httpx.AsyncClient() as client:
await client.get(
- res.callback.base, params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
+ res.callback.base,
+ params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
)
@@ -210,7 +214,11 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
async with httpx.AsyncClient() as client:
r = await client.get(
callback,
- params={"k1": k1.hex(), "key": key.verifying_key.to_string("compressed").hex(), "sig": sig.hex(),},
+ params={
+ "k1": k1.hex(),
+ "key": key.verifying_key.to_string("compressed").hex(),
+ "sig": sig.hex(),
+ },
)
try:
resp = json.loads(r.text)
@@ -219,7 +227,9 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
return LnurlErrorResponse(reason=resp["reason"])
except (KeyError, json.decoder.JSONDecodeError):
- return LnurlErrorResponse(reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,)
+ return LnurlErrorResponse(
+ reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,
+ )
async def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:
diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py
index 8d1d5a9..763ef99 100644
--- a/lnbits/core/tasks.py
+++ b/lnbits/core/tasks.py
@@ -38,7 +38,11 @@ async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client:
data = payment._asdict()
try:
- r = await client.post(payment.webhook, json=data, timeout=40,)
+ r = await client.post(
+ payment.webhook,
+ json=data,
+ timeout=40,
+ )
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 75749de..1225c4a 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -21,7 +21,13 @@ from ..tasks import sse_listeners
@api_check_wallet_key("invoice")
async def api_wallet():
return (
- jsonify({"id": g.wallet.id, "name": g.wallet.name, "balance": g.wallet.balance_msat,}),
+ jsonify(
+ {
+ "id": g.wallet.id,
+ "name": g.wallet.name,
+ "balance": g.wallet.balance_msat,
+ }
+ ),
HTTPStatus.OK,
)
@@ -78,7 +84,11 @@ async def api_payments_create_invoice():
if g.data.get("lnurl_callback"):
async with httpx.AsyncClient() as client:
try:
- r = await client.get(g.data["lnurl_callback"], params={"pr": payment_request}, timeout=10,)
+ r = await client.get(
+ g.data["lnurl_callback"],
+ params={"pr": payment_request},
+ timeout=10,
+ )
if r.is_error:
lnurl_response = r.text
else:
@@ -154,7 +164,9 @@ async def api_payments_pay_lnurl():
async with httpx.AsyncClient() as client:
try:
r = await client.get(
- g.data["callback"], params={"amount": g.data["amount"], "comment": g.data["comment"]}, timeout=40,
+ g.data["callback"],
+ params={"amount": g.data["amount"], "comment": g.data["comment"]},
+ timeout=40,
)
if r.is_error:
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
@@ -194,7 +206,10 @@ async def api_payments_pay_lnurl():
extra["comment"] = g.data["comment"]
payment_hash = await pay_invoice(
- wallet_id=g.wallet.id, payment_request=params["pr"], description=g.data.get("description", ""), extra=extra,
+ wallet_id=g.wallet.id,
+ payment_request=params["pr"],
+ description=g.data.get("description", ""),
+ extra=extra,
)
except Exception as exc:
await db.rollback()
@@ -354,7 +369,9 @@ async def api_lnurlscan(code: str):
@core_app.route("/api/v1/lnurlauth", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
- schema={"callback": {"type": "string", "required": True},}
+ schema={
+ "callback": {"type": "string", "required": True},
+ }
)
async def api_perform_lnurlauth():
err = await perform_lnurlauth(g.data["callback"])
diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py
index 04336b5..aca4a27 100644
--- a/lnbits/extensions/example/migrations.py
+++ b/lnbits/extensions/example/migrations.py
@@ -1,4 +1,4 @@
-#async def m001_initial(db):
+# async def m001_initial(db):
# await db.execute(
# """
@@ -8,4 +8,4 @@
# time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
# );
# """
-# )
\ No newline at end of file
+# )
diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py
index 15382e8..be52323 100644
--- a/lnbits/extensions/example/models.py
+++ b/lnbits/extensions/example/models.py
@@ -1,11 +1,11 @@
-#from sqlite3 import Row
-#from typing import NamedTuple
+# from sqlite3 import Row
+# from typing import NamedTuple
-#class Example(NamedTuple):
+# class Example(NamedTuple):
# id: str
# wallet: str
#
# @classmethod
# def from_row(cls, row: Row) -> "Example":
-# return cls(**dict(row))
\ No newline at end of file
+# return cls(**dict(row))
diff --git a/lnbits/extensions/lnticket/config.json b/lnbits/extensions/lnticket/config.json
index dc45ece..99581b8 100644
--- a/lnbits/extensions/lnticket/config.json
+++ b/lnbits/extensions/lnticket/config.json
@@ -1,6 +1,6 @@
{
- "name": "Support Tickets",
- "short_description": "LN support ticket system",
- "icon": "contact_support",
- "contributors": ["benarc"]
+ "name": "Support Tickets",
+ "short_description": "LN support ticket system",
+ "icon": "contact_support",
+ "contributors": ["benarc"]
}
diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py
index b38d352..8a071cb 100644
--- a/lnbits/extensions/lnticket/crud.py
+++ b/lnbits/extensions/lnticket/crud.py
@@ -6,6 +6,7 @@ from . import db
from .models import Tickets, Forms
import httpx
+
async def create_ticket(
payment_hash: str,
wallet: str,
@@ -52,19 +53,14 @@ async def set_ticket_paid(payment_hash: str) -> Tickets:
""",
(amount, row[1]),
)
-
+
ticket = await get_ticket(payment_hash)
if formdata.webhook:
async with httpx.AsyncClient() as client:
try:
r = await client.post(
formdata.webhook,
- json={
- "form": ticket.form,
- "name": ticket.name,
- "email": ticket.email,
- "content": ticket.ltext
- },
+ json={"form": ticket.form, "name": ticket.name, "email": ticket.email, "content": ticket.ltext},
timeout=40,
)
except AssertionError:
@@ -96,7 +92,9 @@ async def delete_ticket(ticket_id: str) -> None:
# FORMS
-async def create_form(*, wallet: str, name: str, webhook: Optional[str] = None, description: str, costpword: int) -> Forms:
+async def create_form(
+ *, wallet: str, name: str, webhook: Optional[str] = None, description: str, costpword: int
+) -> Forms:
form_id = urlsafe_short_hash()
await db.execute(
"""
diff --git a/lnbits/extensions/lnticket/migrations.py b/lnbits/extensions/lnticket/migrations.py
index f2edb7b..8ced65e 100644
--- a/lnbits/extensions/lnticket/migrations.py
+++ b/lnbits/extensions/lnticket/migrations.py
@@ -102,7 +102,6 @@ async def m003_changed(db):
"""
)
-
for row in [list(row) for row in await db.fetchall("SELECT * FROM forms")]:
usescsv = ""
diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html
index e56954b..9a5acca 100644
--- a/lnbits/extensions/lnticket/templates/lnticket/display.html
+++ b/lnbits/extensions/lnticket/templates/lnticket/display.html
@@ -142,7 +142,7 @@
name: self.formDialog.data.name,
email: self.formDialog.data.email,
ltext: self.formDialog.data.text,
- sats: self.formDialog.data.sats,
+ sats: self.formDialog.data.sats
})
.then(function (response) {
self.paymentReq = response.data.payment_request
@@ -171,7 +171,6 @@
paymentReq: null
}
dismissMsg()
-
self.formDialog.data.name = ''
self.formDialog.data.email = ''
@@ -179,9 +178,8 @@
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
- icon: 'thumb_up',
+ icon: 'thumb_up'
})
-
}
})
.catch(function (error) {
diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html
index ec6a15a..8486230 100644
--- a/lnbits/extensions/lnticket/templates/lnticket/index.html
+++ b/lnbits/extensions/lnticket/templates/lnticket/index.html
@@ -252,7 +252,12 @@
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
- {name: 'webhook', align: 'left', label: 'Webhook', field: 'webhook'},
+ {
+ name: 'webhook',
+ align: 'left',
+ label: 'Webhook',
+ field: 'webhook'
+ },
{
name: 'description',
align: 'left',
diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md
new file mode 100644
index 0000000..49dfc22
--- /dev/null
+++ b/lnbits/extensions/subdomains/README.md
@@ -0,0 +1,54 @@
+
Subdomains Extension
+
+So the goal of the extension is to allow the owner of a domain to sell their subdomain to the anyone who is willing to pay some money for it.
+
+## Requirements
+
+- Free cloudflare account
+- Cloudflare as a dns server provider
+- Cloudflare TOKEN and Cloudflare zone-id where the domain is parked
+
+## Usage
+
+1. Register at cloudflare and setup your domain with them. (Just follow instructions they provide...)
+2. Change DNS server at your domain registrar to point to cloudflare's
+3. Get Cloudflare zoneID for your domain
+
+4. get Cloudflare API TOKEN
+
+
+5. Open the lnbits subdomains extension and register your domain with lnbits
+6. Click on the button in the table to open the public form that was generated for your domain
+
+- Extension also supports webhooks so you can get notified when someone buys a new domain
+
+
+## API Endpoints
+
+- **Domains**
+ - GET /api/v1/domains
+ - POST /api/v1/domains
+ - PUT /api/v1/domains/
+ - DELETE /api/v1/domains/
+- **Subdomains**
+ - GET /api/v1/subdomains
+ - POST /api/v1/subdomains/
+ - GET /api/v1/subdomains/
+ - DELETE /api/v1/subdomains/
+
+## Useful
+
+### Cloudflare
+
+- Cloudflare offers programmatic subdomain registration... (create new A record)
+- you can keep your existing domain's registrar, you just have to transfer dns records to the cloudflare (free service)
+- more information:
+ - https://api.cloudflare.com/#getting-started-requests
+ - API endpoints needed for our project:
+ - https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
+ - https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
+ - https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record
+ - https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
+- api can be used by providing authorization token OR authorization key
+ - check API Tokens and API Keys : https://api.cloudflare.com/#getting-started-requests
+- Cloudflare API postman collection: https://support.cloudflare.com/hc/en-us/articles/115002323852-Using-Cloudflare-API-with-Postman-Collections
diff --git a/lnbits/extensions/subdomains/__init__.py b/lnbits/extensions/subdomains/__init__.py
new file mode 100644
index 0000000..cad9fee
--- /dev/null
+++ b/lnbits/extensions/subdomains/__init__.py
@@ -0,0 +1,15 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_subdomains")
+
+subdomains_ext: Blueprint = Blueprint("subdomains", __name__, static_folder="static", template_folder="templates")
+
+
+from .views_api import * # noqa
+from .views import * # noqa
+
+from .tasks import register_listeners
+from lnbits.tasks import record_async
+
+subdomains_ext.record(record_async(register_listeners))
diff --git a/lnbits/extensions/subdomains/cloudflare.py b/lnbits/extensions/subdomains/cloudflare.py
new file mode 100644
index 0000000..7af85f4
--- /dev/null
+++ b/lnbits/extensions/subdomains/cloudflare.py
@@ -0,0 +1,44 @@
+from lnbits.extensions.subdomains.models import Domains
+import httpx, json
+
+
+async def cloudflare_create_subdomain(domain: Domains, subdomain: str, record_type: str, ip: str):
+ # Call to cloudflare sort of a dry-run - if success delete the domain and wait for payment
+ ### SEND REQUEST TO CLOUDFLARE
+ url = "https://api.cloudflare.com/client/v4/zones/" + domain.cf_zone_id + "/dns_records"
+ header = {"Authorization": "Bearer " + domain.cf_token, "Content-Type": "application/json"}
+ aRecord = subdomain + "." + domain.domain
+ cf_response = ""
+ async with httpx.AsyncClient() as client:
+ try:
+ r = await client.post(
+ url,
+ headers=header,
+ json={
+ "type": record_type,
+ "name": aRecord,
+ "content": ip,
+ "ttl": 0,
+ "proxed": False,
+ },
+ timeout=40,
+ )
+ cf_response = json.loads(r.text)
+ except AssertionError:
+ cf_response = "Error occured"
+ return cf_response
+
+
+async def cloudflare_deletesubdomain(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"
diff --git a/lnbits/extensions/subdomains/config.json b/lnbits/extensions/subdomains/config.json
new file mode 100644
index 0000000..6bf9480
--- /dev/null
+++ b/lnbits/extensions/subdomains/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Subdomains",
+ "short_description": "Sell subdomains of your domain",
+ "icon": "domain",
+ "contributors": ["grmkris"]
+}
diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py
new file mode 100644
index 0000000..6966652
--- /dev/null
+++ b/lnbits/extensions/subdomains/crud.py
@@ -0,0 +1,153 @@
+from typing import List, Optional, Union
+
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import Domains, Subdomains
+from lnbits.extensions import subdomains
+
+
+async def create_subdomain(
+ payment_hash: str,
+ wallet: str,
+ domain: str,
+ subdomain: str,
+ email: str,
+ ip: str,
+ sats: int,
+ duration: int,
+ record_type: str,
+) -> Subdomains:
+ await db.execute(
+ """
+ INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (payment_hash, domain, email, subdomain, ip, wallet, sats, duration, False, record_type),
+ )
+
+ subdomain = await get_subdomain(payment_hash)
+ assert subdomain, "Newly created subdomain couldn't be retrieved"
+ return subdomain
+
+
+async def set_subdomain_paid(payment_hash: str) -> Subdomains:
+ row = await db.fetchone(
+ "SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
+ (payment_hash,),
+ )
+ if row[8] == False:
+ await db.execute(
+ """
+ UPDATE subdomain
+ SET paid = true
+ WHERE id = ?
+ """,
+ (payment_hash,),
+ )
+
+ domaindata = await get_domain(row[1])
+ assert domaindata, "Couldn't get domain from paid subdomain"
+
+ amount = domaindata.amountmade + row[8]
+ await db.execute(
+ """
+ UPDATE domain
+ SET amountmade = ?
+ WHERE id = ?
+ """,
+ (amount, row[1]),
+ )
+
+ subdomain = await get_subdomain(payment_hash)
+ return subdomain
+
+
+async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
+ row = await db.fetchone(
+ "SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
+ (subdomain_id,),
+ )
+ print(row)
+ return Subdomains(**row) if row else None
+
+
+async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]:
+ row = await db.fetchone(
+ "SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
+ (subdomain,),
+ )
+ print(row)
+ return Subdomains(**row) if row else None
+
+
+async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
+ (*wallet_ids,),
+ )
+
+ return [Subdomains(**row) for row in rows]
+
+
+async def delete_subdomain(subdomain_id: str) -> None:
+ await db.execute("DELETE FROM subdomain WHERE id = ?", (subdomain_id,))
+
+
+# Domains
+
+
+async def create_domain(
+ *,
+ wallet: str,
+ domain: str,
+ cf_token: str,
+ cf_zone_id: str,
+ webhook: Optional[str] = None,
+ description: str,
+ cost: int,
+ allowed_record_types: str,
+) -> Domains:
+ domain_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (domain_id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, 0, allowed_record_types),
+ )
+
+ domain = await get_domain(domain_id)
+ assert domain, "Newly created domain couldn't be retrieved"
+ return 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 domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id))
+ row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,))
+ assert row, "Newly updated domain couldn't be retrieved"
+ return Domains(**row)
+
+
+async def get_domain(domain_id: str) -> Optional[Domains]:
+ row = await db.fetchone("SELECT * FROM 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 domain WHERE wallet IN ({q})", (*wallet_ids,))
+
+ return [Domains(**row) for row in rows]
+
+
+async def delete_domain(domain_id: str) -> None:
+ await db.execute("DELETE FROM domain WHERE id = ?", (domain_id,))
diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py
new file mode 100644
index 0000000..4864377
--- /dev/null
+++ b/lnbits/extensions/subdomains/migrations.py
@@ -0,0 +1,37 @@
+async def m001_initial(db):
+
+ await db.execute(
+ """
+ CREATE TABLE IF NOT EXISTS 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,
+ description TEXT NOT NULL,
+ cost INTEGER NOT NULL,
+ amountmade INTEGER NOT NULL,
+ allowed_record_types TEXT NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
+ );
+ """
+ )
+
+ await db.execute(
+ """
+ CREATE TABLE IF NOT EXISTS subdomain (
+ id TEXT PRIMARY KEY,
+ domain TEXT NOT NULL,
+ email TEXT NOT NULL,
+ subdomain TEXT NOT NULL,
+ ip TEXT NOT NULL,
+ wallet TEXT NOT NULL,
+ sats INTEGER NOT NULL,
+ duration INTEGER NOT NULL,
+ paid BOOLEAN NOT NULL,
+ record_type TEXT NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
+ );
+ """
+ )
diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py
new file mode 100644
index 0000000..a519311
--- /dev/null
+++ b/lnbits/extensions/subdomains/models.py
@@ -0,0 +1,30 @@
+from typing import NamedTuple
+
+
+class Domains(NamedTuple):
+ id: str
+ wallet: str
+ domain: str
+ cf_token: str
+ cf_zone_id: str
+ webhook: str
+ description: str
+ cost: int
+ amountmade: int
+ time: int
+ allowed_record_types: str
+
+
+class Subdomains(NamedTuple):
+ id: str
+ wallet: str
+ domain: str
+ domain_name: str
+ subdomain: str
+ email: str
+ ip: str
+ sats: int
+ duration: int
+ paid: bool
+ time: int
+ record_type: str
diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py
new file mode 100644
index 0000000..f5f193a
--- /dev/null
+++ b/lnbits/extensions/subdomains/tasks.py
@@ -0,0 +1,58 @@
+from http import HTTPStatus
+from quart.json import jsonify
+import trio # type: ignore
+import httpx
+
+from .crud import get_domain, set_subdomain_paid
+from lnbits.core.crud import get_user, get_wallet
+from lnbits.core import db as core_db
+from lnbits.core.models import Payment
+from lnbits.tasks import register_invoice_listener
+from .cloudflare import cloudflare_create_subdomain
+
+
+async def register_listeners():
+ invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
+ register_invoice_listener(invoice_paid_chan_send)
+ await wait_for_paid_invoices(invoice_paid_chan_recv)
+
+
+async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
+ async for payment in invoice_paid_chan:
+ await on_invoice_paid(payment)
+
+
+async def on_invoice_paid(payment: Payment) -> None:
+ if "lnsubdomain" != payment.extra.get("tag"):
+ # not an lnurlp invoice
+ return
+
+ await payment.set_pending(False)
+ subdomain = await set_subdomain_paid(payment_hash=payment.payment_hash)
+ domain = await get_domain(subdomain.domain)
+
+ ### Create subdomain
+ cf_response = cloudflare_create_subdomain(
+ domain=domain, subdomain=subdomain.subdomain, record_type=subdomain.record_type, ip=subdomain.ip
+ )
+
+ ### Use webhook to notify about cloudflare registration
+ if domain.webhook:
+ async with httpx.AsyncClient() as client:
+ try:
+ r = await client.post(
+ domain.webhook,
+ json={
+ "domain": subdomain.domain_name,
+ "subdomain": subdomain.subdomain,
+ "record_type": subdomain.record_type,
+ "email": subdomain.email,
+ "ip": subdomain.ip,
+ "cost:": str(subdomain.sats) + " sats",
+ "duration": str(subdomain.duration) + " days",
+ "cf_response": cf_response,
+ },
+ timeout=40,
+ )
+ except AssertionError:
+ webhook = None
diff --git a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html
new file mode 100644
index 0000000..e78ae4a
--- /dev/null
+++ b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html
@@ -0,0 +1,23 @@
+
+
+
+
+ lnSubdomains: Get paid sats to sell your subdomains
+
+
+ Charge people for using your subdomain name...
+ Are you the owner of cool-domain.com and want to sell
+ cool-subdomain.cool-domain.com
+
+
+ Created by, Kris
+
+
+
+
diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html
new file mode 100644
index 0000000..e46228c
--- /dev/null
+++ b/lnbits/extensions/subdomains/templates/subdomains/display.html
@@ -0,0 +1,221 @@
+{% extends "public.html" %} {% block page %}
+
+
+
+
+ {{ domain_domain }}
+
+ {{ domain_desc }}
+
+
+
+
+
+
+
+
+
+
+
+ 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/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html
new file mode 100644
index 0000000..d62f8f3
--- /dev/null
+++ b/lnbits/extensions/subdomains/templates/subdomains/index.html
@@ -0,0 +1,545 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+
+
+ New Domain
+
+
+
+
+
+
+
+
Domains
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
Subdomains
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+ LNbits Subdomain extension
+
+
+
+ {% include "subdomains/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update Form
+ Create Domain
+ Cancel
+
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+{% endblock %}
diff --git a/lnbits/extensions/subdomains/util.py b/lnbits/extensions/subdomains/util.py
new file mode 100644
index 0000000..c7d6630
--- /dev/null
+++ b/lnbits/extensions/subdomains/util.py
@@ -0,0 +1,36 @@
+from lnbits.extensions.subdomains.models import Subdomains
+
+# Python3 program to validate
+# domain name
+# using regular expression
+import re
+import socket
+
+# Function to validate domain name.
+def isValidDomain(str):
+ # Regex to check valid
+ # domain name.
+ regex = "^((?!-)[A-Za-z0-9-]{1,63}(?")
+async def display(domain_id):
+ domain = await get_domain(domain_id)
+ if not domain:
+ abort(HTTPStatus.NOT_FOUND, "Domain does not exist.")
+ allowed_records = domain.allowed_record_types.replace('"', "").replace(" ", "").split(",")
+ print(allowed_records)
+ return await render_template(
+ "subdomains/display.html",
+ domain_id=domain.id,
+ domain_domain=domain.domain,
+ domain_desc=domain.description,
+ domain_cost=domain.cost,
+ domain_allowed_record_types=allowed_records,
+ )
diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py
new file mode 100644
index 0000000..e7d2d21
--- /dev/null
+++ b/lnbits/extensions/subdomains/views_api.py
@@ -0,0 +1,191 @@
+import re
+from quart import g, jsonify, request
+from http import HTTPStatus
+from lnbits.core import crud
+import json
+
+import httpx
+from lnbits.core.crud import get_user, get_wallet
+from lnbits.core.services import create_invoice, check_invoice_status
+from lnbits.decorators import api_check_wallet_key, api_validate_post_request
+
+from .util import isValidDomain, isvalidIPAddress
+from . import subdomains_ext
+from .crud import (
+ create_subdomain,
+ get_subdomain,
+ get_subdomains,
+ delete_subdomain,
+ create_domain,
+ update_domain,
+ get_domain,
+ get_domains,
+ delete_domain,
+ get_subdomainBySubdomain,
+)
+from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain
+
+
+# domainS
+
+
+@subdomains_ext.route("/api/v1/domains", methods=["GET"])
+@api_check_wallet_key("invoice")
+async def api_domains():
+ wallet_ids = [g.wallet.id]
+
+ if "all_wallets" in request.args:
+ wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+
+ return jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]), HTTPStatus.OK
+
+
+@subdomains_ext.route("/api/v1/domains", methods=["POST"])
+@subdomains_ext.route("/api/v1/domains/", methods=["PUT"])
+@api_check_wallet_key("invoice")
+@api_validate_post_request(
+ schema={
+ "wallet": {"type": "string", "empty": False, "required": True},
+ "domain": {"type": "string", "empty": False, "required": True},
+ "cf_token": {"type": "string", "empty": False, "required": True},
+ "cf_zone_id": {"type": "string", "empty": False, "required": True},
+ "webhook": {"type": "string", "empty": False, "required": False},
+ "description": {"type": "string", "min": 0, "required": True},
+ "cost": {"type": "integer", "min": 0, "required": True},
+ "allowed_record_types": {"type": "string", "required": True},
+ }
+)
+async def api_domain_create(domain_id=None):
+ if domain_id:
+ domain = await get_domain(domain_id)
+
+ if not domain:
+ return jsonify({"message": "domain does not exist."}), HTTPStatus.NOT_FOUND
+
+ if domain.wallet != g.wallet.id:
+ return jsonify({"message": "Not your domain."}), HTTPStatus.FORBIDDEN
+
+ domain = await update_domain(domain_id, **g.data)
+ else:
+ domain = await create_domain(**g.data)
+ return jsonify(domain._asdict()), HTTPStatus.CREATED
+
+
+@subdomains_ext.route("/api/v1/domains/", methods=["DELETE"])
+@api_check_wallet_key("invoice")
+async def api_domain_delete(domain_id):
+ domain = await get_domain(domain_id)
+
+ if not domain:
+ return jsonify({"message": "domain does not exist."}), HTTPStatus.NOT_FOUND
+
+ if domain.wallet != g.wallet.id:
+ return jsonify({"message": "Not your domain."}), HTTPStatus.FORBIDDEN
+
+ await delete_domain(domain_id)
+
+ return "", HTTPStatus.NO_CONTENT
+
+
+#########subdomains##########
+
+
+@subdomains_ext.route("/api/v1/subdomains", methods=["GET"])
+@api_check_wallet_key("invoice")
+async def api_subdomains():
+ wallet_ids = [g.wallet.id]
+
+ if "all_wallets" in request.args:
+ wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+
+ return jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]), HTTPStatus.OK
+
+
+@subdomains_ext.route("/api/v1/subdomains/", methods=["POST"])
+@api_validate_post_request(
+ schema={
+ "domain": {"type": "string", "empty": False, "required": True},
+ "subdomain": {"type": "string", "empty": False, "required": True},
+ "email": {"type": "string", "empty": True, "required": True},
+ "ip": {"type": "string", "empty": False, "required": True},
+ "sats": {"type": "integer", "min": 0, "required": True},
+ "duration": {"type": "integer", "empty": False, "required": True},
+ "record_type": {"type": "string", "empty": False, "required": True},
+ }
+)
+async def api_subdomain_make_subdomain(domain_id):
+ domain = await get_domain(domain_id)
+
+ # If the request is coming for the non-existant domain
+ if not domain:
+ return jsonify({"message": "LNsubdomain does not exist."}), HTTPStatus.NOT_FOUND
+
+ ## If record_type is not one of the allowed ones reject the request
+ if g.data["record_type"] not in domain.allowed_record_types:
+ return jsonify({"message": g.data["record_type"] + "Not a valid record"}), HTTPStatus.BAD_REQUEST
+
+ ## If domain already exist in our database reject it
+ if await get_subdomainBySubdomain(g.data["subdomain"]) is not None:
+ return (
+ jsonify({"message": g.data["subdomain"] + "." + domain.domain + " domain already taken"}),
+ HTTPStatus.BAD_REQUEST,
+ )
+
+ ## Dry run cloudflare... (create and if create is sucessful delete it)
+ cf_response = await cloudflare_create_subdomain(
+ domain=domain, subdomain=g.data["subdomain"], record_type=g.data["record_type"], ip=g.data["ip"]
+ )
+ if cf_response["success"] == True:
+ cloudflare_deletesubdomain(domain=domain, domain_id=cf_response["result"]["id"])
+ else:
+ return (
+ jsonify({"message": "Problem with cloudflare: " + cf_response["errors"][0]["message"]}),
+ HTTPStatus.BAD_REQUEST,
+ )
+
+ ## ALL OK - create an invoice and return it to the user
+ sats = g.data["sats"]
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=domain.wallet,
+ amount=sats,
+ memo=f"subdomain {g.data['subdomain']}.{domain.domain} for {sats} sats for {g.data['duration']} days",
+ extra={"tag": "lnsubdomain"},
+ )
+
+ subdomain = await create_subdomain(payment_hash=payment_hash, wallet=domain.wallet, **g.data)
+
+ if not subdomain:
+ return jsonify({"message": "LNsubdomain could not be fetched."}), HTTPStatus.NOT_FOUND
+
+ return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK
+
+
+@subdomains_ext.route("/api/v1/subdomains/", methods=["GET"])
+async def api_subdomain_send_subdomain(payment_hash):
+ subdomain = await get_subdomain(payment_hash)
+ try:
+ status = await check_invoice_status(subdomain.wallet, payment_hash)
+ is_paid = not status.pending
+ except Exception:
+ return jsonify({"paid": False}), HTTPStatus.OK
+
+ if is_paid:
+ return jsonify({"paid": True}), HTTPStatus.OK
+
+ return jsonify({"paid": False}), HTTPStatus.OK
+
+
+@subdomains_ext.route("/api/v1/subdomains/", methods=["DELETE"])
+@api_check_wallet_key("invoice")
+async def api_subdomain_delete(subdomain_id):
+ subdomain = await get_subdomain(subdomain_id)
+
+ if not subdomain:
+ return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
+
+ if subdomain.wallet != g.wallet.id:
+ return jsonify({"message": "Not your subdomain."}), HTTPStatus.FORBIDDEN
+
+ await delete_subdomain(subdomain_id)
+
+ return "", HTTPStatus.NO_CONTENT