diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md
new file mode 100644
index 00000000..729f40f4
--- /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 subdomains to anyone who is willing to pay some money for it.
+
+[](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains')
+
+## 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 zone-ID for your domain
+
+4. Get Cloudflare API TOKEN
+
+
+5. Open the LNBits subdomains extension and register your domain
+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 subdomain\
+
+
+## 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/
+
+### 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 00000000..9701095c
--- /dev/null
+++ b/lnbits/extensions/subdomains/__init__.py
@@ -0,0 +1,28 @@
+import asyncio
+
+from fastapi import APIRouter
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+from lnbits.tasks import catch_everything_and_restart
+
+db = Database("ext_subdomains")
+
+subdomains_ext: APIRouter = APIRouter(
+ prefix="/subdomains",
+ tags=["subdomains"]
+)
+
+def subdomains_renderer():
+ return template_renderer(["lnbits/extensions/subdomains/templates"])
+
+
+from .tasks import wait_for_paid_invoices
+from .views import * # noqa
+from .views_api import * # noqa
+
+
+def subdomains_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+
diff --git a/lnbits/extensions/subdomains/cloudflare.py b/lnbits/extensions/subdomains/cloudflare.py
new file mode 100644
index 00000000..089dbc82
--- /dev/null
+++ b/lnbits/extensions/subdomains/cloudflare.py
@@ -0,0 +1,60 @@
+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,
+ "proxied": 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 00000000..6bf9480c
--- /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 00000000..1ca6b66b
--- /dev/null
+++ b/lnbits/extensions/subdomains/crud.py
@@ -0,0 +1,168 @@
+from typing import List, Optional, Union
+
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import CreateDomain, Domains, Subdomains
+
+
+async def create_subdomain(
+ payment_hash,
+ wallet,
+ data: CreateDomain
+) -> Subdomains:
+ await db.execute(
+ """
+ INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ payment_hash,
+ data.domain,
+ data.email,
+ data.subdomain,
+ data.ip,
+ wallet,
+ data.sats,
+ data.duration,
+ False,
+ data.record_type,
+ ),
+ )
+
+ new_subdomain = await get_subdomain(payment_hash)
+ assert new_subdomain, "Newly created subdomain couldn't be retrieved"
+ return new_subdomain
+
+
+async def set_subdomain_paid(payment_hash: str) -> Subdomains:
+ row = await db.fetchone(
+ "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
+ (payment_hash,),
+ )
+ if row[8] == False:
+ await db.execute(
+ """
+ UPDATE subdomains.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 subdomains.domain
+ SET amountmade = ?
+ WHERE id = ?
+ """,
+ (amount, row[1]),
+ )
+
+ new_subdomain = await get_subdomain(payment_hash)
+ assert new_subdomain, "Newly paid subdomain couldn't be retrieved"
+ return new_subdomain
+
+
+async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
+ row = await db.fetchone(
+ "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
+ (subdomain_id,),
+ )
+ 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 subdomains.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 subdomains.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 subdomains.subdomain WHERE id = ?", (subdomain_id,))
+
+
+# Domains
+
+
+async def create_domain(
+ data: CreateDomain
+) -> Domains:
+ domain_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO subdomains.domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ domain_id,
+ data.wallet,
+ data.domain,
+ data.webhook,
+ data.cf_token,
+ data.cf_zone_id,
+ data.description,
+ data.cost,
+ 0,
+ data.allowed_record_types,
+ ),
+ )
+
+ 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 subdomains.domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)
+ )
+ row = await db.fetchone(
+ "SELECT * FROM subdomains.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 subdomains.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 subdomains.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 subdomains.domain WHERE id = ?", (domain_id,))
diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py
new file mode 100644
index 00000000..292d1f18
--- /dev/null
+++ b/lnbits/extensions/subdomains/migrations.py
@@ -0,0 +1,41 @@
+async def m001_initial(db):
+
+ await db.execute(
+ """
+ CREATE TABLE subdomains.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 """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
+
+ await db.execute(
+ """
+ CREATE TABLE subdomains.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 """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py
new file mode 100644
index 00000000..a8ee6c95
--- /dev/null
+++ b/lnbits/extensions/subdomains/models.py
@@ -0,0 +1,49 @@
+from fastapi.params import Query
+from pydantic.main import BaseModel
+
+
+class CreateDomain(BaseModel):
+ wallet: str = Query(...)
+ domain: str = Query(...)
+ cf_token: str = Query(...)
+ cf_zone_id: str = Query(...)
+ webhook: str = Query("")
+ description: str = Query(..., min_length=0)
+ cost: int = Query(..., ge=0)
+ allowed_record_types: str = Query(...)
+
+class CreateSubdomain(BaseModel):
+ domain: str = Query(...)
+ subdomain: str = Query(...)
+ email: str = Query(...)
+ ip: str = Query(...)
+ sats: int = Query(..., ge=0)
+ duration: int = Query(...)
+ record_type: str = Query(...)
+
+class Domains(BaseModel):
+ 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(BaseModel):
+ 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 00000000..39312fa1
--- /dev/null
+++ b/lnbits/extensions/subdomains/tasks.py
@@ -0,0 +1,67 @@
+import asyncio
+
+import httpx
+
+from lnbits.core.models import Payment
+from lnbits.tasks import register_invoice_listener
+
+from .cloudflare import cloudflare_create_subdomain
+from .crud import get_domain, set_subdomain_paid
+
+
+async def wait_for_paid_invoices():
+ invoice_queue = asyncio.Queue()
+ register_invoice_listener(invoice_queue)
+
+ while True:
+ payment = await invoice_queue.get()
+ await on_invoice_paid(payment)
+
+# 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 00000000..b839c641
--- /dev/null
+++ b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html
@@ -0,0 +1,26 @@
+
+
+
+
+ lnSubdomains: Get paid sats to sell your subdomains
+
+
+ Charge people for using your subdomain name...
+
+ More details
+
+
+ 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 00000000..e52ac73c
--- /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 00000000..06c80d35
--- /dev/null
+++ b/lnbits/extensions/subdomains/templates/subdomains/index.html
@@ -0,0 +1,550 @@
+{% 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 %}
+
+
+
+
+
+
+
+
+ {{SITE_TITLE}} 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 00000000..c7d66307
--- /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}(?