WIP nostr nip5 extension
This commit is contained in:
parent
c5fdd35078
commit
3ca9edeee6
13 changed files with 1409 additions and 0 deletions
19
lnbits/extensions/nostrnip5/README.md
Normal file
19
lnbits/extensions/nostrnip5/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Invoices
|
||||||
|
|
||||||
|
## Create invoices that you can send to your client to pay online over Lightning.
|
||||||
|
|
||||||
|
This extension allows users to create "traditional" invoices (not in the lightning sense) that contain one or more line items. Line items are denominated in a user-configurable fiat currency. Each invoice contains one or more payments up to the total of the invoice. Each invoice creates a public link that can be shared with a customer that they can use to (partially or in full) pay the invoice.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create an invoice by clicking "NEW INVOICE"\
|
||||||
|

|
||||||
|
2. Fill the options for your INVOICE
|
||||||
|
- select the wallet
|
||||||
|
- select the fiat currency the invoice will be denominated in
|
||||||
|
- select a status for the invoice (default is draft)
|
||||||
|
- enter a company name, first name, last name, email, phone & address (optional)
|
||||||
|
- add one or more line items
|
||||||
|
- enter a name & price for each line item
|
||||||
|
3. You can then use share your invoice link with your customer to receive payment\
|
||||||
|

|
||||||
36
lnbits/extensions/nostrnip5/__init__.py
Normal file
36
lnbits/extensions/nostrnip5/__init__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
||||||
|
from .views_api import * # noqa
|
||||||
6
lnbits/extensions/nostrnip5/config.json
Normal file
6
lnbits/extensions/nostrnip5/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Nostr NIP-5",
|
||||||
|
"short_description": "Verify addresses for Nostr NIP-5",
|
||||||
|
"icon": "request_quote",
|
||||||
|
"contributors": ["leesalminen"]
|
||||||
|
}
|
||||||
162
lnbits/extensions/nostrnip5/crud.py
Normal file
162
lnbits/extensions/nostrnip5/crud.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import (
|
||||||
|
CreateDomainData,
|
||||||
|
Domain,
|
||||||
|
Address,
|
||||||
|
CreateAddressData,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,)
|
||||||
|
)
|
||||||
|
return Address.from_row(row) if row else None
|
||||||
|
|
||||||
|
async def get_addresses(domain_id: str) -> List[Address]:
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"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_domain(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 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) -> bool:
|
||||||
|
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,
|
||||||
|
data.pubkey,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
address = await get_address(domain_id, address_id)
|
||||||
|
assert address, "Newly created address couldn't be retrieved"
|
||||||
|
return address
|
||||||
|
|
||||||
|
async def create_domain_internal(wallet_id: str, data: CreateDomainData) -> Domain:
|
||||||
|
domain_id = urlsafe_short_hash()
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostrnip5.domains (id, wallet, currency, amount, domain)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
domain_id,
|
||||||
|
wallet_id,
|
||||||
|
data.currency,
|
||||||
|
int(data.amount * 100),
|
||||||
|
data.domain
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
domain = await get_domain(domain_id)
|
||||||
|
assert domain, "Newly created domain couldn't be retrieved"
|
||||||
|
return domain
|
||||||
35
lnbits/extensions/nostrnip5/migrations.py
Normal file
35
lnbits/extensions/nostrnip5/migrations.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
42
lnbits/extensions/nostrnip5/models.py
Normal file
42
lnbits/extensions/nostrnip5/models.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from enum import Enum
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
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 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))
|
||||||
0
lnbits/extensions/nostrnip5/static/css/signup.css
Normal file
0
lnbits/extensions/nostrnip5/static/css/signup.css
Normal file
33
lnbits/extensions/nostrnip5/tasks.py
Normal file
33
lnbits/extensions/nostrnip5/tasks.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from lnbits.core.models import Payment
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
|
||||||
|
|
||||||
|
from .crud import (
|
||||||
|
get_domain,
|
||||||
|
activate_domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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":
|
||||||
|
# not relevant
|
||||||
|
return
|
||||||
|
|
||||||
|
domain_id = payment.extra.get("domain_id")
|
||||||
|
address_id = payment.extra.get("address_id")
|
||||||
|
|
||||||
|
active = await activate_domain(domain_id, address_id)
|
||||||
|
|
||||||
|
return
|
||||||
153
lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html
Normal file
153
lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<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="List Invoices">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">GET</span> /invoices/api/v1/invoices</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</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>[<invoice_object>, ...]</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X GET {{ request.base_url }}invoices/api/v1/invoices -H
|
||||||
|
"X-Api-Key: <invoice_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Fetch Invoice">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">GET</span>
|
||||||
|
/invoices/api/v1/invoice/{invoice_id}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</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>{invoice_object}</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
<code
|
||||||
|
>curl -X GET {{ request.base_url
|
||||||
|
}}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
|
||||||
|
<invoice_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Create Invoice">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">POST</span> /invoices/api/v1/invoice</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</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>{invoice_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 -H
|
||||||
|
"X-Api-Key: <invoice_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item group="api" dense expand-separator label="Update Invoice">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<code
|
||||||
|
><span class="text-blue">POST</span>
|
||||||
|
/invoices/api/v1/invoice/{invoice_id}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <invoice_key>}</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>{invoice_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} -H "X-Api-Key:
|
||||||
|
<invoice_key>"
|
||||||
|
</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:
|
||||||
|
<invoice_key>"
|
||||||
|
</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>
|
||||||
|
/invoices/api/v1/invoice/{invoice_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
|
||||||
|
}}invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash} -H
|
||||||
|
"X-Api-Key: <invoice_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-expansion-item>
|
||||||
535
lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
Normal file
535
lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
Normal file
|
|
@ -0,0 +1,535 @@
|
||||||
|
{% 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-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="delete"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
@click="deleteDomain(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="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>
|
||||||
|
</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="10.00"
|
||||||
|
></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"
|
||||||
|
v-if="typeof formDialog.data.id == 'undefined'"
|
||||||
|
>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>
|
||||||
|
var mapDomain = function (obj) {
|
||||||
|
obj.time = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
|
||||||
|
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
domains: [],
|
||||||
|
addresses: [],
|
||||||
|
currencyOptions: [
|
||||||
|
'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: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
169
lnbits/extensions/nostrnip5/templates/nostrnip5/signup.html
Normal file
169
lnbits/extensions/nostrnip5/templates/nostrnip5/signup.html
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
{% 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">
|
||||||
|
<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 <b>{{ "{:0,.2f}".format(domain.amount / 100) }} {{ domain.currency }}</b> for a <b>lifetime</b> account.</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 || '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
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</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 :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 {
|
||||||
|
domain: '{{ domain.domain }}',
|
||||||
|
domain_id: '{{ domain_id }}',
|
||||||
|
wallet: '{{ domain.wallet }}',
|
||||||
|
currency: '{{ domain.currency }}',
|
||||||
|
amount: '{{ domain.amount }}',
|
||||||
|
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
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
alert(`Success! Your username is now active at ${localPart}@${self.domain}. Please add this to your nostr profile accordingly.`)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
44
lnbits/extensions/nostrnip5/views.py
Normal file
44
lnbits/extensions/nostrnip5/views.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.params import Depends
|
||||||
|
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_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 index(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,
|
||||||
|
},
|
||||||
|
)
|
||||||
175
lnbits/extensions/nostrnip5/views_api.py
Normal file
175
lnbits/extensions/nostrnip5/views_api.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Query, Request, Response
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from loguru import logger
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
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 (
|
||||||
|
get_domains,
|
||||||
|
get_domain,
|
||||||
|
create_domain_internal,
|
||||||
|
create_address_internal,
|
||||||
|
delete_domain,
|
||||||
|
get_domain_by_name,
|
||||||
|
get_address_by_local_part,
|
||||||
|
get_addresses,
|
||||||
|
get_all_addresses,
|
||||||
|
delete_address,
|
||||||
|
)
|
||||||
|
from .models import CreateDomainData, CreateAddressData
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
||||||
|
wallet_ids = (await get_user(wallet.wallet.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:
|
||||||
|
wallet_ids = (await get_user(wallet.wallet.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)
|
||||||
|
async def api_invoice(domain_id: str, wallet: 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."
|
||||||
|
)
|
||||||
|
|
||||||
|
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.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", status_code=HTTPStatus.CREATED)
|
||||||
|
async def api_address_create(
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
|
||||||
|
exists = await get_address_by_local_part(domain_id, data.local_part)
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Local part already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(data.pubkey) != 64:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Pubkey must be in hex format."
|
||||||
|
)
|
||||||
|
|
||||||
|
address = await create_address_internal(domain_id=domain_id, data=data)
|
||||||
|
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 domain {domain_id}",
|
||||||
|
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}
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
||||||
|
if address.get("active") == False:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name and name != address.get("local_part"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
output[address.get("local_part")] = address.get("pubkey")
|
||||||
|
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"names": output
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue