Merge pull request #1492 from lnbits/remove-nostrnip5

Removed nostrnip5
This commit is contained in:
Arc 2023-02-14 13:48:22 +00:00 committed by GitHub
commit 7a57690bd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 0 additions and 2109 deletions

View File

@ -1,59 +0,0 @@
# Nostr NIP-05
## Allow users to NIP-05 verify themselves at a domain you control
This extension allows users to sell NIP-05 verification to other nostr users on a domain they control.
## Usage
1. Create a Domain by clicking "NEW DOMAIN"\
2. Fill the options for your DOMAIN
- select the wallet
- select the fiat currency the invoice will be denominated in
- select an amount in fiat to charge users for verification
- enter the domain (or subdomain) you want to provide verification for
- Note, you must own this domain and have access to a web server
3. You can then use share your signup link with your users to allow them to sign up
## Installation
In order for this to work, you need to have ownership of a domain name, and access to a web server that this domain is pointed to.
Then, you'll need to set up a proxy that points `https://{your_domain}/.well-known/nostr.json` to `https://{your_lnbits}/nostrnip5/api/v1/domain/{domain_id}/nostr.json`
Example nginx configuration
```
## Proxy Server Caching
proxy_cache_path /tmp/nginx_cache keys_zone=nip5_cache:5m levels=1:2 inactive=300s max_size=100m use_temp_path=off;
location /.well-known/nostr.json {
proxy_pass https://{your_lnbits}/nostrnip5/api/v1/domain/{domain_id}/nostr.json;
proxy_set_header Host {your_lnbits};
proxy_ssl_server_name on;
expires 5m;
add_header Cache-Control "public, no-transform";
proxy_cache nip5_cache;
proxy_cache_lock on;
proxy_cache_valid 200 300s;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
}
```
Example Caddy configuration
```
my.lnbits.instance {
reverse_proxy {your_lnbits}
}
nip.5.domain {
route /.well-known/nostr.json {
rewrite * /nostrnip5/api/v1/domain/{domain_id}/nostr.json
reverse_proxy {your_lnbits}
}
}
```

View File

@ -1,36 +0,0 @@
import asyncio
from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_nostrnip5")
nostrnip5_static_files = [
{
"path": "/nostrnip5/static",
"app": StaticFiles(directory="lnbits/extensions/nostrnip5/static"),
"name": "nostrnip5_static",
}
]
nostrnip5_ext: APIRouter = APIRouter(prefix="/nostrnip5", tags=["nostrnip5"])
def nostrnip5_renderer():
return template_renderer(["lnbits/extensions/nostrnip5/templates"])
from .tasks import wait_for_paid_invoices
def nostrnip5_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403

View File

@ -1,6 +0,0 @@
{
"name": "Nostr NIP-5",
"short_description": "Verify addresses for Nostr NIP-5",
"tile": "/nostrnip5/static/image/nostrnip5.png",
"contributors": ["leesalminen"]
}

View File

@ -1,211 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Address, CreateAddressData, CreateDomainData, Domain, EditDomainData
async def get_domain(domain_id: str) -> Optional[Domain]:
row = await db.fetchone(
"SELECT * FROM nostrnip5.domains WHERE id = ?", (domain_id,)
)
return Domain.from_row(row) if row else None
async def get_domain_by_name(domain: str) -> Optional[Domain]:
row = await db.fetchone(
"SELECT * FROM nostrnip5.domains WHERE domain = ?", (domain,)
)
return Domain.from_row(row) if row else None
async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domain]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM nostrnip5.domains WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Domain.from_row(row) for row in rows]
async def get_address(domain_id: str, address_id: str) -> Optional[Address]:
row = await db.fetchone(
"SELECT * FROM nostrnip5.addresses WHERE domain_id = ? AND id = ?",
(
domain_id,
address_id,
),
)
return Address.from_row(row) if row else None
async def get_address_by_local_part(
domain_id: str, local_part: str
) -> Optional[Address]:
row = await db.fetchone(
"SELECT * FROM nostrnip5.addresses WHERE domain_id = ? AND local_part = ?",
(
domain_id,
local_part.lower(),
),
)
return Address.from_row(row) if row else None
async def get_addresses(domain_id: str) -> List[Address]:
rows = await db.fetchall(
"SELECT * FROM nostrnip5.addresses WHERE domain_id = ?", (domain_id,)
)
return [Address.from_row(row) for row in rows]
async def get_all_addresses(wallet_ids: Union[str, List[str]]) -> List[Address]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"""
SELECT a.*
FROM nostrnip5.addresses a
JOIN nostrnip5.domains d ON d.id = a.domain_id
WHERE d.wallet IN ({q})
""",
(*wallet_ids,),
)
return [Address.from_row(row) for row in rows]
async def activate_address(domain_id: str, address_id: str) -> Address:
await db.execute(
"""
UPDATE nostrnip5.addresses
SET active = true
WHERE domain_id = ?
AND id = ?
""",
(
domain_id,
address_id,
),
)
address = await get_address(domain_id, address_id)
assert address, "Newly updated address couldn't be retrieved"
return address
async def rotate_address(domain_id: str, address_id: str, pubkey: str) -> Address:
await db.execute(
"""
UPDATE nostrnip5.addresses
SET pubkey = ?
WHERE domain_id = ?
AND id = ?
""",
(
pubkey,
domain_id,
address_id,
),
)
address = await get_address(domain_id, address_id)
assert address, "Newly updated address couldn't be retrieved"
return address
async def delete_domain(domain_id) -> bool:
await db.execute(
"""
DELETE FROM nostrnip5.addresses WHERE domain_id = ?
""",
(domain_id,),
)
await db.execute(
"""
DELETE FROM nostrnip5.domains WHERE id = ?
""",
(domain_id,),
)
return True
async def delete_address(address_id):
await db.execute(
"""
DELETE FROM nostrnip5.addresses WHERE id = ?
""",
(address_id,),
)
async def create_address_internal(domain_id: str, data: CreateAddressData) -> Address:
address_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO nostrnip5.addresses (id, domain_id, local_part, pubkey, active)
VALUES (?, ?, ?, ?, ?)
""",
(
address_id,
domain_id,
data.local_part.lower(),
data.pubkey,
False,
),
)
address = await get_address(domain_id, address_id)
assert address, "Newly created address couldn't be retrieved"
return address
async def update_domain_internal(wallet_id: str, data: EditDomainData) -> Domain:
if data.currency != "Satoshis":
amount = data.amount * 100
else:
amount = data.amount
print(data)
await db.execute(
"""
UPDATE nostrnip5.domains
SET amount = ?, currency = ?
WHERE id = ?
""",
(int(amount), data.currency, data.id),
)
domain = await get_domain(data.id)
assert domain, "Domain couldn't be updated"
return domain
async def create_domain_internal(wallet_id: str, data: CreateDomainData) -> Domain:
domain_id = urlsafe_short_hash()
if data.currency != "Satoshis":
amount = data.amount * 100
else:
amount = data.amount
await db.execute(
"""
INSERT INTO nostrnip5.domains (id, wallet, currency, amount, domain)
VALUES (?, ?, ?, ?, ?)
""",
(domain_id, wallet_id, data.currency, int(amount), data.domain),
)
domain = await get_domain(domain_id)
assert domain, "Newly created domain couldn't be retrieved"
return domain

View File

@ -1,35 +0,0 @@
async def m001_initial_invoices(db):
await db.execute(
f"""
CREATE TABLE nostrnip5.domains (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
currency TEXT NOT NULL,
amount INTEGER NOT NULL,
domain TEXT NOT NULL,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
f"""
CREATE TABLE nostrnip5.addresses (
id TEXT PRIMARY KEY,
domain_id TEXT NOT NULL,
local_part TEXT NOT NULL,
pubkey TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT false,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
FOREIGN KEY(domain_id) REFERENCES {db.references_schema}domains(id)
);
"""
)

View File

@ -1,58 +0,0 @@
from sqlite3 import Row
from fastapi.param_functions import Query
from pydantic import BaseModel
class RotateAddressData(BaseModel):
pubkey: str
class CreateAddressData(BaseModel):
domain_id: str
local_part: str
pubkey: str
active: bool = False
class CreateDomainData(BaseModel):
wallet: str
currency: str
amount: float = Query(..., ge=0.01)
domain: str
class EditDomainData(BaseModel):
id: str
currency: str
amount: float = Query(..., ge=0.01)
@classmethod
def from_row(cls, row: Row) -> "EditDomainData":
return cls(**dict(row))
class Domain(BaseModel):
id: str
wallet: str
currency: str
amount: int
domain: str
time: int
@classmethod
def from_row(cls, row: Row) -> "Domain":
return cls(**dict(row))
class Address(BaseModel):
id: str
domain_id: str
local_part: str
pubkey: str
active: bool
time: int
@classmethod
def from_row(cls, row: Row) -> "Address":
return cls(**dict(row))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,33 +0,0 @@
import asyncio
from loguru import logger
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from .crud import activate_address
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 on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "nostrnip5":
return
domain_id = payment.extra.get("domain_id")
address_id = payment.extra.get("address_id")
if domain_id and address_id:
logger.info("Activating NOSTR NIP-05")
logger.info(domain_id)
logger.info(address_id)
await activate_address(domain_id, address_id)
return

View File

@ -1,238 +0,0 @@
<q-expansion-item
group="extras"
icon="info"
label="Guide"
:content-inset-level="0.5"
>
<q-card>
<p>
<q-card-section>
<strong>Usage</strong><br />
1. Create a Domain by clicking "NEW DOMAIN"\<br />
2. Fill the options for your DOMAIN<br />
- select the wallet<br />
- select the fiat currency the invoice will be denominated in<br />
- select an amount in fiat to charge users for verification<br />
- enter the domain (or subdomain) you want to provide verification
for<br />
3. You can then use share your signup link with your users to allow them
to sign up *Note, you must own this domain and have access to a web
server*
<br /><br />
<strong>Installation</strong><br />
In order for this to work, you need to have ownership of a domain name,
and access to a web server that this domain is pointed to. Then, you'll
need to set up a proxy that points
`https://{your_domain}/.well-known/nostr.json` to
`https://{your_lnbits}/nostrnip5/api/v1/domain/{domain_id}/nostr.json`
<br /><br />
<strong>Example nginx configuration</strong>
<br />
<q-card>
<q-card-section>
<small class="text-caption">
proxy_cache_path /tmp/nginx_cache keys_zone=nip5_cache:5m
levels=1:2 inactive=300s max_size=100m use_temp_path=off;<br />
location /.well-known/nostr.json {<br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_pass
https://{your_lnbits}/nostrnip5/api/v1/domain/{domain_id}/nostr.json;<br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_set_header Host {your_lnbits};<br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_ssl_server_name on;<br /><br />
&nbsp;&nbsp;&nbsp;&nbsp;expires 5m;<br />
&nbsp;&nbsp;&nbsp;&nbsp;add_header Cache-Control "public,
no-transform";<br /><br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_cache nip5_cache;<br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_cache_lock on;<br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_cache_valid 200 300s;<br />
&nbsp;&nbsp;&nbsp;&nbsp;proxy_cache_use_stale error timeout
invalid_header updating http_500 http_502 http_503 http_504;<br />
}
</small>
</q-card-section>
</q-card>
</q-card-section>
</p>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="api"
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/nostrnip5"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List Domains">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /nostrnip5/api/v1/domains</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;domain_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}nostrnip5/api/v1/domains -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="List Addresses">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /nostrnip5/api/v1/addresses</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;address_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}nostrnip5/api/v1/addresses -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Fetch Domain">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/nostrnip5/api/v1/domain/{domain_id}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{domain_object}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}nostrnip5/api/v1/domain/{domain_id}
-H "X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create Domain">
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span> /nostrnip5/api/v1/domain</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{domain_object}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}nostrnip5/api/v1/domain -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create Address">
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span>
/nostrnip5/api/v1/domain/{domain_id}/address</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{address_object}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url
}}nostrnip5/api/v1/domain/{domain_id}/address -H "X-Api-Key:
&lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create Invoice Payment"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span>
/invoices/api/v1/invoice/{invoice_id}/payments</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<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>{payment_object}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url
}}invoices/api/v1/invoice/{invoice_id}/payments -H "X-Api-Key:
&lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check Invoice Payment Status"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/nostrnip5/api/v1/domain/{domain_id}/payments/{payment_hash}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<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>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}nostrnip5/api/v1/domain/{domain_id}/payments/{payment_hash} -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View File

@ -1,785 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New Domain</q-btn
>
<q-btn unelevated color="primary" @click="addressFormDialog.show = true"
>New Address</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="exportCSV">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="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'signup/' + props.row.id"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'api/v1/domain/' + props.row.id + '/nostr.json'"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="delete"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="deleteDomain(props.row.id)"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="edit"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="editDomain(props.row.id)"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</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-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="edit"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
target="_blank"
:href="'rotate/' + props.row.domain_id + '/' + props.row.id"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="check"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
:disable="props.row.active == true"
@click="activateAddress(props.row.domain_id, props.row.id)"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="delete"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="deleteAddress(props.row.id)"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Nostr NIP-5 extension
</h6>
<p>
<strong
>Allow users to NIP-05 verify themselves at a domain you
control</strong
>
</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "nostrnip5/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="saveDomain" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-select
filled
dense
emit-value
v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"
></q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.amount"
label="Amount"
placeholder="How much do you want to charge?"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.domain"
label="Domain"
placeholder="nostr.com"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.wallet == null || formDialog.data.currency == 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>
<q-dialog
v-model="editFormDialog.show"
position="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="saveEditedDomain" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="editFormDialog.data.currency"
:options="currencyOptions"
label="Currency *"
></q-select>
<q-input
filled
dense
v-model.trim="editFormDialog.data.amount"
label="Amount"
placeholder="How much do you want to charge?"
></q-input>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Update Amount</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog
v-model="addressFormDialog.show"
position="top"
@hide="closeAddressFormDialog"
>
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="saveAddress" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="addressFormDialog.data.domain_id"
:options="domainOptions"
label="Domain *"
></q-select>
<q-input
filled
dense
v-model.trim="addressFormDialog.data.pubkey"
label="Public Key"
placeholder="npub..."
></q-input>
<q-input
filled
dense
v-model.trim="addressFormDialog.data.local_part"
label="Local Part"
placeholder="benarc"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="addressFormDialog.data.domain_id == null || addressFormDialog.data.pubkey == null || addressFormDialog.data.local_part == null"
type="submit"
>Create Address</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>
var mapDomain = function (obj) {
obj.time = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
if (obj.currency != 'Satoshis') {
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
}
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
domains: [],
addresses: [],
currencyOptions: [
'Satoshis',
'USD',
'EUR',
'GBP',
'AED',
'AFN',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BTN',
'BWP',
'BYN',
'BZD',
'CAD',
'CDF',
'CHF',
'CLF',
'CLP',
'CNH',
'CNY',
'COP',
'CRC',
'CUC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ERN',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GGP',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'IMP',
'INR',
'IQD',
'IRR',
'IRT',
'ISK',
'JEP',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KPW',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRO',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SDG',
'SEK',
'SGD',
'SHP',
'SLL',
'SOS',
'SRD',
'SSP',
'STD',
'SVC',
'SYP',
'SZL',
'THB',
'TJS',
'TMT',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VEF',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XAG',
'XAU',
'XCD',
'XDR',
'XOF',
'XPD',
'XPF',
'XPT',
'YER',
'ZAR',
'ZMW',
'ZWL'
],
domainsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'domain', align: 'left', label: 'Domain', field: 'domain'},
{
name: 'currency',
align: 'left',
label: 'Currency',
field: 'currency'
},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'},
{name: 'time', align: 'left', label: 'Created At', field: 'time'}
],
pagination: {
rowsPerPage: 10
}
},
addressesTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'active', align: 'left', label: 'Active', field: 'active'},
{
name: 'domain_id',
align: 'left',
label: 'Domain',
field: 'domain_id'
},
{
name: 'local_part',
align: 'left',
label: 'Local Part',
field: 'local_part'
},
{name: 'pubkey', align: 'left', label: 'Pubkey', field: 'pubkey'},
{name: 'time', align: 'left', label: 'Created At', field: 'time'}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
},
editFormDialog: {
show: false,
data: {}
},
addressFormDialog: {
show: false,
data: {}
}
}
},
methods: {
closeAddressFormDialog: function () {
this.formDialog.data = {}
},
closeFormDialog: function () {
this.formDialog.data = {}
},
getDomains: function () {
var self = this
LNbits.api
.request(
'GET',
'/nostrnip5/api/v1/domains?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.domains = response.data.map(function (obj) {
return mapDomain(obj)
})
})
},
getAddresses: function () {
var self = this
LNbits.api
.request(
'GET',
'/nostrnip5/api/v1/addresses?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.addresses = response.data.map(function (obj) {
return mapDomain(obj)
})
})
},
saveDomain: function () {
var data = this.formDialog.data
var self = this
LNbits.api
.request(
'POST',
'/nostrnip5/api/v1/domain',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
self.domains.push(mapDomain(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
saveEditedDomain: function () {
var data = this.editFormDialog.data
var self = this
LNbits.api
.request(
'PUT',
'/nostrnip5/api/v1/domain',
_.findWhere(this.g.user.wallets, {
id: this.editFormDialog.data.wallet
}).inkey,
data
)
.then(function (response) {
self.editFormDialog.show = false
self.editFormDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
editDomain: function (domain_id) {
var self = this
var data = _.findWhere(this.domains, {id: domain_id})
self.editFormDialog.show = true
self.editFormDialog.data = data
},
deleteDomain: function (domain_id) {
var self = this
var domain = _.findWhere(this.domains, {id: domain_id})
LNbits.utils
.confirmDialog('Are you sure you want to delete this domain?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/nostrnip5/api/v1/domain/' + domain_id,
_.findWhere(self.g.user.wallets, {id: domain.wallet}).adminkey
)
.then(function (response) {
self.domains = _.reject(self.domain, function (obj) {
return obj.id == domain_id
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
saveAddress: function () {
var self = this
var formDialog = this.addressFormDialog
var domain = _.findWhere(this.domains, {id: formDialog.data.domain_id})
axios
.post(
'/nostrnip5/api/v1/domain/' +
formDialog.data.domain_id +
'/address',
formDialog.data
)
.then(function (response) {
return LNbits.api.request(
'POST',
'/nostrnip5/api/v1/domain/' +
formDialog.data.domain_id +
'/address/' +
response.data.address_id +
'/activate',
_.findWhere(self.g.user.wallets, {id: domain.wallet}).adminkey
)
})
.then(function (response) {
self.addressFormDialog.data = {}
self.addressFormDialog.show = false
self.getAddresses()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteAddress: function (address_id) {
var self = this
var address = _.findWhere(this.addresses, {id: address_id})
var domain = _.findWhere(this.domains, {id: address.domain_id})
LNbits.utils
.confirmDialog('Are you sure you want to delete this address?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/nostrnip5/api/v1/address/' + address_id,
_.findWhere(self.g.user.wallets, {id: domain.wallet}).adminkey
)
.then(function (response) {
self.addresses = _.reject(self.addresses, function (obj) {
return obj.id == address_id
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
activateAddress: function (domain_id, address_id) {
var self = this
var address = _.findWhere(this.addresses, {id: address_id})
var domain = _.findWhere(this.domains, {id: address.domain_id})
LNbits.utils
.confirmDialog(
'Are you sure you want to manually activate this address?'
)
.onOk(function () {
return LNbits.api
.request(
'POST',
'/nostrnip5/api/v1/domain/' +
domain_id +
'/address/' +
address_id +
'/activate',
_.findWhere(self.g.user.wallets, {id: domain.wallet}).adminkey
)
.then(function (response) {
self.getAddresses()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.domainsTable.columns, this.domains)
},
exportAddressesCSV: function () {
LNbits.utils.exportCSV(this.addressesTable.columns, this.addresses)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getDomains()
this.getAddresses()
}
},
computed: {
domainOptions: function () {
return this.domains.map(el => {
return {
label: el.domain,
value: el.id
}
})
}
}
})
</script>
{% endblock %}

View File

@ -1,88 +0,0 @@
{% extends "public.html" %} {% block toolbar_title %} Rotate Keys For {{
domain.domain }} {% endblock %} {% from "macros.jinja" import window_vars with
context %} {% block page %}
<link rel="stylesheet" href="/nostrnip5/static/css/signup.css" />
<div>
<q-card class="q-pa-lg q-pt-lg">
<q-form @submit="updateAddress" class="q-gutter-md">
<p>
You can use this page to change the public key associated with your
NIP-5 identity.
</p>
<p>
Your current NIP-5 identity is {{ address.local_part }}@{{ domain.domain
}} with nostr public key {{ address.pubkey }}.
</p>
<p>Input your new pubkey below to update it.</p>
<q-input
filled
dense
v-model.trim="formDialog.data.pubkey"
label="Pub Key"
placeholder="abc234"
:rules="[ val => val.length = 64 || val.indexOf('npub') === 0 ||'Please enter a hex pubkey' ]"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.pubkey == null"
type="submit"
>Rotate Keys</q-btn
>
</div>
</q-form>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
domain: '{{ domain.domain }}',
domain_id: '{{ domain_id }}',
address_id: '{{ address_id }}',
formDialog: {
data: {
pubkey: null
}
}
}
},
methods: {
updateAddress: function () {
var self = this
var formDialog = this.formDialog
var newPubKey = this.formDialog.data.pubkey
axios
.post(
'/nostrnip5/api/v1/domain/' +
this.domain_id +
'/address/' +
this.address_id +
'/rotate',
formDialog.data
)
.then(function (response) {
formDialog.data = {}
alert(
`Success! Your pubkey has been updated. Please allow clients time to refresh the data.`
)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}

View File

@ -1,209 +0,0 @@
{% extends "public.html" %} {% block toolbar_title %} Verify NIP-5 For {{
domain.domain }} {% endblock %} {% from "macros.jinja" import window_vars with
context %} {% block page %}
<link rel="stylesheet" href="/nostrnip5/static/css/signup.css" />
<div>
<q-card class="q-pa-lg q-pt-lg" v-if="success == true">
{% raw %}
<p>
Success! Your username is now active at {{ successData.local_part }}@{{
domain }}. Please add this to your nostr profile accordingly. If you ever
need to rotate your keys, you can still keep your identity!
</p>
<h3>Important!</h3>
<p>
Bookmark this link:
<a
class="text-secondary"
v-bind:href="'/nostrnip5/rotate/' + domain_id + '/' + successData.address_id"
target="_blank"
>{{ base_url }}nostrnip5/rotate/{{ domain_id }}/{{
successData.address_id }}</a
>
</p>
<p>
In case you ever need to change your pubkey, you can still keep this NIP-5
identity. Just come back to the above linked page to change the pubkey
associated to your identity.
</p>
{% endraw %}
</q-card>
<q-card class="q-pa-lg q-pt-lg" v-if="success == false">
<q-form @submit="createAddress" class="q-gutter-md">
<p>
You can use this page to get NIP-5 verified on the nostr protocol under
the {{ domain.domain }} domain.
</p>
<p>
The current price is {% if domain.currency != "Satoshis" %}
<b
>{{ "{:0,.2f}".format(domain.amount / 100) }} {{ domain.currency }}</b
>
{% else %}
<b>{{ "{}".format(domain.amount) }} {{ domain.currency }}</b>
{% endif %} for an account (if you do not own the domain, the service
provider can disable at any time).
</p>
<p>After submitting payment, your address will be</p>
<q-input
filled
dense
v-model.trim="formDialog.data.local_part"
label="Local Part"
placeholder="benarc"
>
<template v-slot:append>
<span style="font-size: 18px">@{{ domain.domain }} </span>
</template>
</q-input>
<p>and will be tied to this nostr pubkey</p>
<q-input
filled
dense
v-model.trim="formDialog.data.pubkey"
label="Pub Key"
placeholder="abc234"
:rules="[ val => val.length = 64 || val.indexOf('npub') === 0 ||'Please enter a hex pubkey' ]"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.local_part == null || formDialog.data.pubkey == null"
type="submit"
>Create Address</q-btn
>
</div>
</q-form>
</q-card>
<q-dialog
v-model="qrCodeDialog.show"
position="top"
@hide="closeQrCodeDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card text-center">
<a
class="text-secondary"
:href="'lightning:' + qrCodeDialog.data.payment_request"
>
<q-responsive :ratio="1" class="q-mx-xs">
<qrcode
:value="qrCodeDialog.data.payment_request"
:options="{width: 400}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
<br />
<q-btn
outline
color="grey"
@click="copyText('lightning:' + qrCodeDialog.data.payment_request, 'Invoice copied to clipboard!')"
>Copy Invoice</q-btn
>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
base_url: '{{ request.base_url }}',
domain: '{{ domain.domain }}',
domain_id: '{{ domain_id }}',
wallet: '{{ domain.wallet }}',
currency: '{{ domain.currency }}',
amount: '{{ domain.amount }}',
success: false,
successData: {
local_part: null,
address_id: null
},
qrCodeDialog: {
data: {
payment_request: null
},
show: false
},
formDialog: {
data: {
local_part: null,
pubkey: null
}
},
urlDialog: {
show: false
}
}
},
methods: {
closeQrCodeDialog: function () {
this.qrCodeDialog.show = false
},
createAddress: function () {
var self = this
var qrCodeDialog = this.qrCodeDialog
var formDialog = this.formDialog
formDialog.data.domain_id = this.domain_id
var localPart = formDialog.data.local_part
axios
.post(
'/nostrnip5/api/v1/domain/' + this.domain_id + '/address',
formDialog.data
)
.then(function (response) {
formDialog.data = {}
qrCodeDialog.data = response.data
qrCodeDialog.show = true
console.log(qrCodeDialog.data)
qrCodeDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
qrCodeDialog.paymentChecker = setInterval(function () {
axios
.get(
'/nostrnip5/api/v1/domain/' +
self.domain_id +
'/payments/' +
response.data.payment_hash
)
.then(function (res) {
if (res.data.paid) {
clearInterval(qrCodeDialog.paymentChecker)
qrCodeDialog.dismissMsg()
qrCodeDialog.show = false
self.successData.local_part = localPart
self.successData.address_id = qrCodeDialog.data.address_id
self.success = true
}
})
}, 3000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}

View File

@ -1,67 +0,0 @@
from http import HTTPStatus
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import nostrnip5_ext, nostrnip5_renderer
from .crud import get_address, get_domain
templates = Jinja2Templates(directory="templates")
@nostrnip5_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return nostrnip5_renderer().TemplateResponse(
"nostrnip5/index.html", {"request": request, "user": user.dict()}
)
@nostrnip5_ext.get("/signup/{domain_id}", response_class=HTMLResponse)
async def signup(request: Request, domain_id: str):
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
return nostrnip5_renderer().TemplateResponse(
"nostrnip5/signup.html",
{
"request": request,
"domain_id": domain_id,
"domain": domain,
},
)
@nostrnip5_ext.get("/rotate/{domain_id}/{address_id}", response_class=HTMLResponse)
async def rotate(request: Request, domain_id: str, address_id: str):
domain = await get_domain(domain_id)
address = await get_address(domain_id, address_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
if not address:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Address does not exist."
)
return nostrnip5_renderer().TemplateResponse(
"nostrnip5/rotate.html",
{
"request": request,
"domain_id": domain_id,
"domain": domain,
"address_id": address_id,
"address": address,
},
)

View File

@ -1,284 +0,0 @@
import re
from http import HTTPStatus
from bech32 import bech32_decode, convertbits
from fastapi import Depends, Query, Response
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from . import nostrnip5_ext
from .crud import (
activate_address,
create_address_internal,
create_domain_internal,
delete_address,
delete_domain,
get_address_by_local_part,
get_addresses,
get_all_addresses,
get_domain,
get_domain_by_name,
get_domains,
rotate_address,
update_domain_internal,
)
from .models import (
CreateAddressData,
CreateDomainData,
EditDomainData,
RotateAddressData,
)
@nostrnip5_ext.get("/api/v1/domains", status_code=HTTPStatus.OK)
async def api_domains(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
if not user:
return []
wallet_ids = user.wallet_ids
return [domain.dict() for domain in await get_domains(wallet_ids)]
@nostrnip5_ext.get("/api/v1/addresses", status_code=HTTPStatus.OK)
async def api_addresses(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
if not user:
return []
wallet_ids = user.wallet_ids
return [address.dict() for address in await get_all_addresses(wallet_ids)]
@nostrnip5_ext.get(
"/api/v1/domain/{domain_id}",
status_code=HTTPStatus.OK,
dependencies=[Depends(get_key_type)],
)
async def api_invoice(domain_id: str):
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
return domain
@nostrnip5_ext.post("/api/v1/domain", status_code=HTTPStatus.CREATED)
async def api_domain_create(
data: CreateDomainData, wallet: WalletTypeInfo = Depends(get_key_type)
):
exists = await get_domain_by_name(data.domain)
logger.error(exists)
if exists:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain already exists."
)
domain = await create_domain_internal(wallet_id=wallet.wallet.id, data=data)
return domain
@nostrnip5_ext.put("/api/v1/domain", status_code=HTTPStatus.OK)
async def api_domain_update(
data: EditDomainData, wallet: WalletTypeInfo = Depends(get_key_type)
):
domain = await update_domain_internal(wallet_id=wallet.wallet.id, data=data)
return domain
@nostrnip5_ext.delete("/api/v1/domain/{domain_id}", status_code=HTTPStatus.CREATED)
async def api_domain_delete(
domain_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
await delete_domain(domain_id)
return True
@nostrnip5_ext.delete("/api/v1/address/{address_id}", status_code=HTTPStatus.CREATED)
async def api_address_delete(
address_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
await delete_address(address_id)
return True
@nostrnip5_ext.post(
"/api/v1/domain/{domain_id}/address/{address_id}/activate",
status_code=HTTPStatus.OK,
dependencies=[Depends(require_admin_key)],
)
async def api_address_activate(
domain_id: str,
address_id: str,
):
await activate_address(domain_id, address_id)
return True
@nostrnip5_ext.post(
"/api/v1/domain/{domain_id}/address/{address_id}/rotate",
status_code=HTTPStatus.OK,
)
async def api_address_rotate(
domain_id: str,
address_id: str,
post_data: RotateAddressData,
):
if post_data.pubkey.startswith("npub"):
_, data = bech32_decode(post_data.pubkey)
if data:
decoded_data = convertbits(data, 5, 8, False)
if decoded_data:
post_data.pubkey = bytes(decoded_data).hex()
if len(bytes.fromhex(post_data.pubkey)) != 32:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pubkey must be in hex format."
)
await rotate_address(domain_id, address_id, post_data.pubkey)
return True
@nostrnip5_ext.post(
"/api/v1/domain/{domain_id}/address", status_code=HTTPStatus.CREATED
)
async def api_address_create(
post_data: CreateAddressData,
domain_id: str,
):
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
if post_data.local_part == "_":
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="You're sneaky, nice try."
)
regex = re.compile(r"^[a-z0-9_.]+$")
if not re.fullmatch(regex, post_data.local_part.lower()):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Only a-z, 0-9 and .-_ are allowed characters, case insensitive.",
)
exists = await get_address_by_local_part(domain_id, post_data.local_part)
if exists:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Local part already exists."
)
if post_data and post_data.pubkey.startswith("npub"):
_, data = bech32_decode(post_data.pubkey)
if data:
decoded_data = convertbits(data, 5, 8, False)
if decoded_data:
post_data.pubkey = bytes(decoded_data).hex()
if len(bytes.fromhex(post_data.pubkey)) != 32:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pubkey must be in hex format."
)
address = await create_address_internal(domain_id=domain_id, data=post_data)
if domain.currency == "Satoshis":
price_in_sats = domain.amount
else:
price_in_sats = await fiat_amount_as_satoshis(
domain.amount / 100, domain.currency
)
try:
payment_hash, payment_request = await create_invoice(
wallet_id=domain.wallet,
amount=price_in_sats,
memo=f"Payment for NIP-05 for {address.local_part}@{domain.domain}",
extra={
"tag": "nostrnip5",
"domain_id": domain_id,
"address_id": address.id,
},
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return {
"payment_hash": payment_hash,
"payment_request": payment_request,
"address_id": address.id,
}
@nostrnip5_ext.get(
"/api/v1/domain/{domain_id}/payments/{payment_hash}", status_code=HTTPStatus.OK
)
async def api_nostrnip5_check_payment(domain_id: str, payment_hash: str):
domain = await get_domain(domain_id)
if not domain:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
)
try:
status = await api_payment(payment_hash)
except Exception as exc:
logger.error(exc)
return {"paid": False}
return status
@nostrnip5_ext.get("/api/v1/domain/{domain_id}/nostr.json", status_code=HTTPStatus.OK)
async def api_get_nostr_json(
response: Response, domain_id: str, name: str = Query(None)
):
addresses = [address.dict() for address in await get_addresses(domain_id)]
output = {}
for address in addresses:
local_part = address.get("local_part")
if not local_part:
continue
if address.get("active") is False:
continue
if name and name.lower() != local_part.lower():
continue
output[local_part.lower()] = address.get("pubkey")
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS"
return {"names": output}