Merge remote-tracking branch 'arcbtc/lnaddress' into FastAPI

This commit is contained in:
benarc 2021-11-24 08:42:14 +00:00
commit b122debd8c
15 changed files with 2030 additions and 0 deletions

View file

@ -1,7 +1,10 @@
import asyncio
import datetime
from http import HTTPStatus
from fastapi import HTTPException
from starlette.requests import Request
from lnbits import bolt11
from .. import core_app
@ -9,6 +12,13 @@ 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
domain = request.client.host
return await lnurl_response(username, domain)
@core_app.get("/public/v1/payment/{payment_hash}")
async def api_public_payment_longpolling(payment_hash):
payment = await get_standalone_payment(payment_hash)

View file

@ -0,0 +1,68 @@
<h1>Lightning Address</h1>
<h2>Rent Lightning Addresses on your domain</h2>
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 <your email>@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

View file

@ -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_lnaddress")
lnaddress_ext: APIRouter = APIRouter(
prefix="/lnaddress",
tags=["lnaddress"]
)
def lnaddress_renderer():
return template_renderer(["lnbits/extensions/lnaddress/templates"])
from .lnurl import * # noqa
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def lnurlp_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View file

@ -0,0 +1,58 @@
from lnbits.extensions.lnaddress.models import Domains
import httpx, json
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 = json.loads(r.text)
except AssertionError:
cf_response = "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"

View file

@ -0,0 +1,6 @@
{
"name": "Lightning Address",
"short_description": "Sell LN addresses for your domain",
"icon": "alternate_email",
"contributors": ["talvasconcelos"]
}

View file

@ -0,0 +1,193 @@
from datetime import datetime, timedelta
from typing import List, Optional, Union
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 = ?",
# "SELECT * FROM lnaddress.address WHERE username = ? AND domain = ?",
(username, domain,),
)
print("ADD", row)
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,),
)
print([Addresses(**row) for row in rows])
return [Addresses(**row) for row in rows]
async def set_address_paid(payment_hash: str) -> Addresses:
_address = await get_address(payment_hash)
address = _address._asdict()
if address["paid"] == 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)
address = _address._asdict()
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)._asdict()
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:
print("DELETE UNP_PAY_EXP", r["username"])
await delete_address(r["id"])
if paid and expired:
print("DELETE PAID_EXP", r["username"])
await delete_address(r["id"])

View file

@ -0,0 +1,92 @@
import hashlib
from datetime import datetime, timedelta
import httpx
from fastapi.params import Query
from lnurl import ( # type: ignore
LnurlErrorResponse,
LnurlPayActionResponse,
LnurlPayResponse,
)
from starlette.requests import Request
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 = LnurlPayResponse(
callback=request.url_for("lnaddress.lnurl_callback", address_id=address.id, _external=True),
min_sendable=1000,
max_sendable=1000000000,
metadata=await address.lnurlpay_metadata(),
)
return resp.dict()
@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=f"Address not found"
).dict()
amount_received = amount
# min = 1000
# max = 1000000000
# if amount_received < min:
# return LnurlErrorResponse(
# reason=f"Amount {amount_received} is smaller than minimum."
# ).dict()
# elif amount_received > max:
# return jsonify(
# LnurlErrorResponse(
# reason=f"Amount {amount_received} is greater than maximum."
# ).dict()
# )
domain = await get_domain(address.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": hashlib.sha256((await address.lnurlpay_metadata()).encode("utf-8")).hexdigest(),
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
},
timeout=40,
)
r = call.json()
except AssertionError as e:
return LnurlErrorResponse(reason="ERROR")
resp = LnurlPayActionResponse(
pr=r["payment_request"],
routes=[],
)
return resp.dict()

View file

@ -0,0 +1,41 @@
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
+ """
);
"""
)
# async def m003_create_unique_indexes(db):
# await db.execute("CREATE UNIQUE INDEX IF NOT EXISTS address_at_domain ON lnaddress.address (domain, username);")

View file

@ -0,0 +1,53 @@
import json
from typing import Optional
from fastapi.params import Query
from lnurl.types import LnurlPayMetadata
from pydantic.main import BaseModel # type: ignore
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: 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: str
username: str
wallet_key: str
wallet_endpoint: str
sats: int
duration: int
paid: bool
time: int
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
text = f"Payment to {self.username}"
metadata = [["text/plain", text]]
return LnurlPayMetadata(json.dumps(metadata))

View file

@ -0,0 +1,59 @@
import asyncio
import httpx
from lnbits.core.models import Payment
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)
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)
domain = await get_domain(address.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,
)
except AssertionError:
webhook = None
async def on_invoice_paid(payment: Payment) -> None:
if "lnaddress" == payment.extra.get("tag"):
await payment.set_pending(False)
await set_address_paid(payment_hash=payment.payment_hash)
await call_webhook_on_paid(payment.payment_hash)
elif "renew lnaddress" == payment.extra.get("tag"):
await payment.set_pending(False)
await set_address_renewed(address_id=payment.extra["id"], duration=payment.extra["duration"])
await call_webhook_on_paid(payment.payment_hash)
else:
return

View file

@ -0,0 +1,174 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About lnAddress"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
lnAddress: Get paid sats to sell lightning addresses on your domains
</h5>
<p>
Charge people for using your domain name...<br />
<a
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/lnaddress"
>More details</a
>
<br />
<small>
Created by,
<a href="https://twitter.com/talvasconcelos">talvasconcelos</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="GET domains">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
lnaddress/api/v1/domains</code
>
<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>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}lnaddress/api/v1/domains -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST domain">
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/lnAddress/api/v1/domains</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": &lt;string&gt;, "Content-type":
"application/json"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "wallet" is a YOUR wallet ID
</h5>
<code
>{"wallet": &lt;string&gt;, "domain": &lt;string&gt;, "cf_token":
&lt;string&gt;,"cf_zone_id": &lt;string&gt;,"webhook": &lt;Optional
string&gt; ,"cost": &lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "wallet": &lt;string&gt;, "domain":
&lt;string&gt;, "webhook": &lt;string&gt;, "cf_token": &lt;string&gt;,
"cf_zone_id": &lt;string&gt;, "cost": &lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}lnaddress/api/v1/domains -d
'{"wallet": "{{ user.wallets[0].id }}", "domain": &lt;string&gt;,
"cf_token": &lt;string&gt;,"cf_zone_id": &lt;string&gt;,"webhook":
&lt;Optional string&gt; ,"cost": &lt;integer&gt;}' -H "X-Api-Key: {{
user.wallets[0].inkey }}" -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="DELETE domain">
<q-card>
<q-card-section>
<code
><span class="text-red">DELETE</span>
/lnaddress/api/v1/domains/&lt;domain_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}lnaddress/api/v1/domains/&lt;domain_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET addresses">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
lnaddress/api/v1/addresses</code
>
<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>JSON list of addresses</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}lnaddress/api/v1/addresses -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET address info">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
lnaddress/api/v1/address/&lt;domain&gt;/&lt;username&gt;/&lt;wallet_key&gt;</code
>
<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>JSON list of addresses</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}lnaddress/api/v1/address/&lt;domain&gt;/&lt;username&gt;/&lt;wallet_key&gt;
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST address">
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/lnaddress/api/v1/address/&lt;domain_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}lnaddress/api/v1/address/&lt;domain_id&gt; -d '{"domain":
&lt;string&gt;, "username": &lt;string&gt;,"email": &lt;Optional
string&gt;, "wallet_endpoint": &lt;string&gt;, "wallet_key":
&lt;string&gt;, "sats": &lt;integer&gt; "duration": &lt;integer&gt;,}'
-H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -0,0 +1,436 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<div class="q-pa-md">
<div class="q-gutter-y-md">
<q-tabs v-model="tab" active-color="primary">
<q-tab
name="create"
label="Create"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="renew"
label="Renew"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="create">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ domain_domain }}</h3>
<br />
<h6 class="q-my-none">
Your Lightning Address: {% raw
%}{{this.formDialog.data.username}}{% endraw %}@{{domain_domain}}
</h6>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.email"
type="email"
label="Your email (optional, if you want a reply)"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.username"
type="text"
label="Alias/username"
:rules="[
val => checkUsername || 'Sorry, alias already taken',
val => isValidUsername || 'Alias is not valid'
]"
lazy-rules
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.wallet_endpoint"
type="text"
label="Endpoint of LNbits instance, defaults to this instance"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.wallet_key"
type="text"
label="Admin key for your wallet"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.duration"
type="number"
label="Number of days"
>
</q-input>
<p>
Cost per day: {{ domain_cost }} sats<br />
{% raw %} Total cost: {{amountSats}} sats {% endraw %}
</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="!formDialog.data.username || !formDialog.data.wallet_key || !formDialog.data.duration || !checkUsername"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card-section>
</q-tab-panel>
<q-tab-panel name="renew">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ domain_domain }}</h3>
<br />
<h6 class="q-my-none">
Renew your Lightning Address: {% raw
%}{{this.formDialog.data.username}}{% endraw %}@{{domain_domain}}
</h6>
<br />
<q-form @submit="renewAddress()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="renewDialog.data.username"
type="text"
label="Alias/username"
:rules="[
val => isValidUsername || 'Alias is not valid'
]"
>
</q-input>
<q-input
filled
dense
v-model.trim="renewDialog.data.wallet_key"
type="text"
label="Admin key for your wallet"
>
</q-input>
<div>
<div v-if="renewDialog.info">
{% raw %}
<p>
<strong>LN Address:</strong>
<span
>{{renewDialog.data.username}}@{{renewDialog.data.domain}}</span
>
<br />
<span>Expires at: {{renewDialog.data.expiration}}</span>
</p>
{% endraw %}
</div>
<q-btn
unelevated
color="primary"
v-if="!renewDialog.info"
:disable="!renewDialog.data.username || !renewDialog.data.wallet_key"
@click="getUserInfo()"
>Get Info</q-btn
>
</div>
<q-input
v-if="renewDialog.info"
filled
dense
v-model.trim="renewDialog.data.duration"
type="number"
label="Number of days"
>
</q-input>
<p>
Cost per day: {{ domain_cost }} sats<br />
{% raw %} Total cost: {{amountSats}} sats {% endraw %}
</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="!renewDialog.data.username || !renewDialog.data.wallet_key || !renewDialog.data.duration || !isValidUsername"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
</q-card>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</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 %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {}
},
renewDialog: {
show: false,
data: {},
info: false
},
receive: {
show: false,
status: 'pending',
paymentReq: null
},
tab: 'create',
wallet: {
inkey: ''
},
cancelListener: () => {}
}
},
computed: {
amountSats() {
let dialog = this.renewDialog.info ? this.renewDialog : this.formDialog
if (!dialog.data.duration) return 0
let sats = dialog.data.duration * parseInt('{{ domain_cost }}')
dialog.data.sats = parseInt(sats)
return sats
},
checkUsername: async function () {
let username = this.formDialog.data.username
if (!this.isValidUsername) {
return true
}
let available = await axios
.get(
`/lnaddress/api/v1/address/availabity/${'{{domain_id}}'}/${username}`
)
.then(res => {
return res.data < 1
})
console.log(available)
return available
},
isValidUsername: function () {
let username = this.formDialog.data.username
return /^[a-z0-9_\.]+$/.test(username)
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data = {}
this.renewDialog.data = {}
this.renewDialog.info = false
},
closeReceiveDialog: function () {
let checker = this.startPaymentNotifier
dismissMsg()
if (this.tab == 'create') {
clearInterval(paymentChecker)
}
setTimeout(function () {}, 10000)
},
getUserInfo() {
let {username, wallet_key} = this.renewDialog.data
axios
.get(
`/lnaddress/api/v1/address/{{ domain_domain }}/${username}/${wallet_key}`
)
.then(res => {
if (res) {
let dt = {}
let result = new Date(res.data.time * 1000)
dt.start = new Date(res.data.time * 1000)
dt.expiration = moment(
result.setDate(result.getDate() + res.data.duration)
).format('dddd, MMMM Do YYYY, h:mm:ss a')
dt.domain = '{{domain_domain}}'
dt.wallet_endpoint = res.data.wallet_endpoint
this.renewDialog.data = {
...this.renewDialog.data,
...dt
}
this.renewDialog.info = true
console.log(this.renewDialog)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
renewAddress() {
let {data} = this.renewDialog
data.duration = parseInt(data.duration)
axios
.put(
'/lnaddress/api/v1/address/{{ domain_id }}/' +
data.username +
'/' +
data.wallet_key,
data
)
.then(response => {
this.paymentReq = response.data.payment_request
this.paymentCheck = response.data.payment_hash
dismissMsg = this.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
this.receive = {
show: true,
status: 'pending',
paymentReq: this.paymentReq
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
return
},
startPaymentNotifier() {
this.cancelListener()
this.cancelListener = LNbits.events.onInvoicePaid(
this.wallet,
payment => {
this.receive = {
show: false,
status: 'complete',
paymentReq: null
}
dismissMsg()
this.renewDialog.data = {}
this.renewDialog.info = false
this.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: 'thumb_up'
})
}
)
},
Invoice: function () {
let {data} = this.formDialog
data.domain = '{{ domain_id }}'
if (data.wallet_endpoint == '') {
data.wallet_endpoint = null
}
data.wallet_endpoint = data.wallet_endpoint ?? '{{ request.url_root }}'
data.duration = parseInt(data.duration)
console.log('data', data)
axios
.post('/lnaddress/api/v1/address/{{ domain_id }}', data)
.then(response => {
this.paymentReq = response.data.payment_request
this.paymentCheck = response.data.payment_hash
dismissMsg = this.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
this.receive = {
show: true,
status: 'pending',
paymentReq: this.paymentReq
}
paymentChecker = setInterval(() => {
axios
.get(`/lnaddress/api/v1/addresses/${this.paymentCheck}`)
.then(res => {
console.log('pay_check', res.data)
if (res.data.paid) {
clearInterval(paymentChecker)
this.receive = {
show: false,
status: 'complete',
paymentReq: null
}
dismissMsg()
console.log(this.formDialog)
this.formDialog.data = {}
this.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: 'thumb_up'
})
console.log('END')
}
})
.catch(function (error) {
console.log(error)
LNbits.utils.notifyApiError(error)
})
}, 5000)
})
.catch(function (error) {
console.log(error)
LNbits.utils.notifyApiError(error)
})
}
},
created() {
this.wallet.inkey = '{{domain_wallet_inkey}}'
this.startPaymentNotifier()
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,507 @@
{% 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-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="domainDialog.show = true"
>Add Domain</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Domains</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportDomainsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="domains"
row-key="id"
:columns="domainsTable.columns"
:pagination.sync="domainsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</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="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateDomainDialog(props.row.id)"
icon="edit"
color="light-blue"
>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteDomain(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</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">Addresses</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportAddressesCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="addresses"
row-key="id"
:columns="addressesTable.columns"
:pagination.sync="addressesTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props" v-if="props.row.paid">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="email"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'mailto:' + props.row.email"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteAddress(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LN Address extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "lnaddress/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="domainDialog.show" position="top">
<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="domainDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
emit-value
v-model.trim="domainDialog.data.domain"
type="text"
label="Domain name "
><q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>The domain to use ex: "example.com"</q-tooltip
></q-input
>
<q-input
filled
dense
v-model.trim="domainDialog.data.cf_token"
type="text"
label="Cloudflare API token"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>Your API key in cloudflare</q-tooltip
>
</q-input>
<q-input
filled
dense
v-model.trim="domainDialog.data.cf_zone_id"
type="text"
label="Cloudflare Zone Id"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>Create a "Edit zone DNS" API token in cloudflare</q-tooltip
>
</q-input>
<q-input
filled
dense
v-model.trim="domainDialog.data.webhook"
type="text"
label="Webhook (optional)"
hint="A URL to be called whenever this link receives a payment."
></q-input>
<q-input
filled
dense
v-model.number="domainDialog.data.cost"
type="number"
label="Amount per day in satoshis"
><q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>How much to charge per day</q-tooltip
></q-input
>
<div class="row q-mt-lg">
<q-btn
v-if="domainDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="domainDialog.data.cost == null || domainDialog.data.cost < 0 || domainDialog.data.domain == null"
type="submit"
>Create Domain</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
const mapLNDomain = obj => {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.displayUrl = ['/lnaddress/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
domains: [],
addresses: [],
domainDialog: {
show: false,
data: {}
},
domainsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{
name: 'domain',
align: 'left',
label: 'Domain name',
field: 'domain'
},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
{
name: 'webhook',
align: 'left',
label: 'Webhook',
field: 'webhook'
},
{
name: 'cost',
align: 'left',
label: 'Cost Per Day',
field: 'cost'
}
],
pagination: {
rowsPerPage: 10
}
},
addressesTable: {
columns: [
{
name: 'username',
align: 'left',
label: 'Alias/username',
field: 'username'
},
{
name: 'domain',
align: 'left',
label: 'Domain name',
field: 'domain'
},
{
name: 'email',
align: 'left',
label: 'Email',
field: 'email'
},
{
name: 'sats',
align: 'left',
label: 'Sats paid',
field: 'sats'
},
{
name: 'duration',
align: 'left',
label: 'Duration in days',
field: 'duration'
},
{name: 'id', align: 'left', label: 'ID', field: 'id'}
],
pagination: {
rowsPerPage: 10
}
}
}
},
methods: {
//DOMAINS
getDomains: function () {
LNbits.api
.request(
'GET',
'/lnaddress/api/v1/domains?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.domains = response.data.map(function (obj) {
return mapLNDomain(obj)
})
})
},
sendFormData: function () {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.domainDialog.data.wallet
})
let data = this.domainDialog.data
if (data.id) {
this.updateDomain(wallet, data)
} else {
this.createDomain(wallet, data)
}
},
createDomain: function (wallet, data) {
var self = this
console.log(data)
LNbits.api
.request('POST', '/lnaddress/api/v1/domains', wallet.inkey, data)
.then(response => {
this.domains.push(mapLNDomain(response.data))
this.domainDialog.show = false
this.domainDialog.data = {}
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
updateDomainDialog: function (formId) {
var link = _.findWhere(this.domains, {id: formId})
console.log(link.id)
this.domainDialog.data = _.clone(link)
this.domainDialog.show = true
},
updateDomain: function (wallet, data) {
console.log(data)
if (!data.webhook) {
delete data.webhook
}
LNbits.api
.request(
'PUT',
'/lnaddress/api/v1/domains/' + data.id,
wallet.inkey,
data
)
.then(response => {
this.domains = _.reject(this.domains, function (obj) {
return obj.id == data.id
})
this.domains.push(mapLNDomain(response.data))
this.domainDialog.show = false
this.domainDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteDomain: function (domainId) {
var self = this
var domains = _.findWhere(this.domains, {id: domainId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this domain link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnaddress/api/v1/domains/' + domainId,
_.findWhere(self.g.user.wallets, {id: domains.wallet}).inkey
)
.then(function (response) {
self.domains = _.reject(self.domains, function (obj) {
return obj.id == domainId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportDomainsCSV: function () {
LNbits.utils.exportCSV(this.domainsTable.columns, this.domains)
},
//ADDRESSES
getAddresses: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnaddress/api/v1/addresses?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.addresses = response.data
.filter(d => d.paid)
.map(function (obj) {
// obj.domain_name = this.domains.find(d => d.id == obj.domain)
return mapLNDomain(obj)
})
console.log(self.addresses)
})
},
deleteAddress: function (addressId) {
let self = this
let addresses = _.findWhere(this.addresses, {id: addressId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this LN address')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnaddress/api/v1/addresses/' + addressId,
_.findWhere(self.g.user.wallets, {id: addresses.wallet}).inkey
)
.then(function (response) {
self.addresses = _.reject(self.addresses, function (obj) {
return obj.id == addressId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportAddressesCSV: function () {
LNbits.utils.exportCSV(this.addressesTable.columns, this.addresses)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getDomains()
this.getAddresses()
}
// var self = this
//
// // axios is available for making requests
// axios({
// method: 'GET',
// url: '/example/api/v1/tools',
// headers: {
// 'X-example-header': 'not-used'
// }
// }).then(function (response) {
// self.tools = response.data
// })
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,43 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
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}")
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, response_class=HTMLResponse)
wallet = await get_wallet(domain.wallet)
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
}
)

View file

@ -0,0 +1,262 @@
from http import HTTPStatus
from urllib.parse import urlparse
from fastapi import Request
from fastapi.params import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import check_invoice_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.lnaddress.models import CreateAddress, CreateDomain
from . import lnaddress_ext
from .cloudflare import cloudflare_create_record, cloudflare_deleterecord
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,
)
# 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:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
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(request.url.path).netloc
#root_url = request.url_root
cf_response = await cloudflare_create_record(
domain=domain,
ip=root_url,
)
if not cf_response or cf_response["success"] != True:
await delete_domain(domain.id)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Problem with cloudflare: " + cf_response["errors"][0]["message"],
)
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)
raise HTTPException(status_code=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:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return [address.dict() for address in await get_addresses(wallet_ids)]
@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.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.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[6]
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:
print("USER", user, domain.domain)
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)
domain = await get_domain(address.domain)
try:
status = await check_invoice_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)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)