Merge branch 'main' into diagon-alley
This commit is contained in:
commit
87933d1f8b
137 changed files with 7739 additions and 618 deletions
4
Makefile
4
Makefile
|
|
@ -6,7 +6,7 @@ format: prettier isort black
|
||||||
|
|
||||||
check: mypy checkprettier checkisort checkblack
|
check: mypy checkprettier checkisort checkblack
|
||||||
|
|
||||||
prettier: $(shell find lnbits -name "*.js" -name ".html")
|
prettier: $(shell find lnbits -name "*.js" -o -name ".html")
|
||||||
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||||
|
|
||||||
black:
|
black:
|
||||||
|
|
@ -18,7 +18,7 @@ mypy:
|
||||||
isort:
|
isort:
|
||||||
poetry run isort .
|
poetry run isort .
|
||||||
|
|
||||||
checkprettier: $(shell find lnbits -name "*.js" -name ".html")
|
checkprettier: $(shell find lnbits -name "*.js" -o -name ".html")
|
||||||
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||||
|
|
||||||
checkblack:
|
checkblack:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
};
|
};
|
||||||
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
||||||
let
|
let
|
||||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
forSystems = systems: f:
|
forSystems = systems: f:
|
||||||
nixpkgs.lib.genAttrs systems
|
nixpkgs.lib.genAttrs systems
|
||||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
|
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from binascii import unhexlify
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import List, NamedTuple, Optional
|
from typing import List, NamedTuple, Optional
|
||||||
|
|
||||||
|
|
@ -108,7 +107,7 @@ def decode(pr: str) -> Invoice:
|
||||||
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
|
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||||
sig = signature[0:64]
|
sig = signature[0:64]
|
||||||
if invoice.payee:
|
if invoice.payee:
|
||||||
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
|
key = VerifyingKey.from_string(bytes.fromhex(invoice.payee), curve=SECP256k1)
|
||||||
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
|
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
|
||||||
else:
|
else:
|
||||||
keys = VerifyingKey.from_public_key_recovery(
|
keys = VerifyingKey.from_public_key_recovery(
|
||||||
|
|
@ -131,7 +130,7 @@ def encode(options):
|
||||||
if options["timestamp"]:
|
if options["timestamp"]:
|
||||||
addr.date = int(options["timestamp"])
|
addr.date = int(options["timestamp"])
|
||||||
|
|
||||||
addr.paymenthash = unhexlify(options["paymenthash"])
|
addr.paymenthash = bytes.fromhex(options["paymenthash"])
|
||||||
|
|
||||||
if options["description"]:
|
if options["description"]:
|
||||||
addr.tags.append(("d", options["description"]))
|
addr.tags.append(("d", options["description"]))
|
||||||
|
|
@ -149,8 +148,8 @@ def encode(options):
|
||||||
while len(splits) >= 5:
|
while len(splits) >= 5:
|
||||||
route.append(
|
route.append(
|
||||||
(
|
(
|
||||||
unhexlify(splits[0]),
|
bytes.fromhex(splits[0]),
|
||||||
unhexlify(splits[1]),
|
bytes.fromhex(splits[1]),
|
||||||
int(splits[2]),
|
int(splits[2]),
|
||||||
int(splits[3]),
|
int(splits[3]),
|
||||||
int(splits[4]),
|
int(splits[4]),
|
||||||
|
|
@ -235,7 +234,7 @@ def lnencode(addr, privkey):
|
||||||
raise ValueError("Must include either 'd' or 'h'")
|
raise ValueError("Must include either 'd' or 'h'")
|
||||||
|
|
||||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
||||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
|
privkey = secp256k1.PrivateKey(bytes.fromhex(privkey))
|
||||||
sig = privkey.ecdsa_sign_recoverable(
|
sig = privkey.ecdsa_sign_recoverable(
|
||||||
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||||
)
|
)
|
||||||
|
|
@ -261,7 +260,7 @@ class LnAddr(object):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||||
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
bytes.hex(self.pubkey.serialize()).decode("utf-8"),
|
||||||
self.amount,
|
self.amount,
|
||||||
self.currency,
|
self.currency,
|
||||||
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,7 @@ async def update_payment_details(
|
||||||
async def update_payment_extra(
|
async def update_payment_extra(
|
||||||
payment_hash: str,
|
payment_hash: str,
|
||||||
extra: dict,
|
extra: dict,
|
||||||
|
outgoing: bool = False,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -461,8 +462,10 @@ async def update_payment_extra(
|
||||||
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
|
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
|
||||||
|
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
"SELECT hash, extra from apipayments WHERE hash = ?",
|
f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}",
|
||||||
(payment_hash,),
|
(payment_hash,),
|
||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
|
|
@ -471,10 +474,7 @@ async def update_payment_extra(
|
||||||
db_extra.update(extra)
|
db_extra.update(extra)
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ",
|
||||||
UPDATE apipayments SET extra = ?
|
|
||||||
WHERE hash = ?
|
|
||||||
""",
|
|
||||||
(json.dumps(db_extra), payment_hash),
|
(json.dumps(db_extra), payment_hash),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ async def m007_set_invoice_expiries(db):
|
||||||
)
|
)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
if len(rows):
|
if len(rows):
|
||||||
logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices")
|
logger.info(f"Migration: Checking expiry of {len(rows)} invoices")
|
||||||
for i, (
|
for i, (
|
||||||
payment_request,
|
payment_request,
|
||||||
checking_id,
|
checking_id,
|
||||||
|
|
@ -238,7 +238,7 @@ async def m007_set_invoice_expiries(db):
|
||||||
invoice.date + invoice.expiry
|
invoice.date + invoice.expiry
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||||
)
|
)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import hmac
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Dict, List, NamedTuple, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Extra, validator
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from binascii import unhexlify
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
@ -13,12 +12,7 @@ from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||||
WalletTypeInfo,
|
|
||||||
get_key_type,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||||
from lnbits.requestvars import g
|
from lnbits.requestvars import g
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
|
|
@ -308,7 +302,7 @@ async def perform_lnurlauth(
|
||||||
) -> Optional[LnurlErrorResponse]:
|
) -> Optional[LnurlErrorResponse]:
|
||||||
cb = urlparse(callback)
|
cb = urlparse(callback)
|
||||||
|
|
||||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
||||||
|
|
||||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from typing import Dict
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@
|
||||||
<a :href="'lightning:' + props.row.bolt11">
|
<a :href="'lightning:' + props.row.bolt11">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="props.row.bolt11"
|
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
@ -325,7 +325,7 @@
|
||||||
</p>
|
</p>
|
||||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||||
<qrcode
|
<qrcode
|
||||||
value="{{wallet.lnurlwithdraw_full}}"
|
value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||||
:options="{width:240}"
|
:options="{width:240}"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -524,7 +524,7 @@
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a :href="'lightning:' + receive.paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="receive.paymentReq"
|
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import binascii
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
@ -38,7 +37,7 @@ from lnbits.decorators import (
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
from lnbits.helpers import url_for
|
||||||
from lnbits.settings import get_wallet_class, settings
|
from lnbits.settings import get_wallet_class, settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
currencies,
|
currencies,
|
||||||
|
|
@ -48,14 +47,11 @@ from lnbits.utils.exchange_rates import (
|
||||||
|
|
||||||
from .. import core_app, db
|
from .. import core_app, db
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_payment,
|
|
||||||
get_payments,
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
get_total_balance,
|
get_total_balance,
|
||||||
get_wallet,
|
|
||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
save_balance_check,
|
save_balance_check,
|
||||||
update_payment_status,
|
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
|
|
@ -71,6 +67,11 @@ from ..services import (
|
||||||
from ..tasks import api_invoice_listeners
|
from ..tasks import api_invoice_listeners
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.get("/api/v1/health", status_code=HTTPStatus.OK)
|
||||||
|
async def health():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/wallet")
|
@core_app.get("/api/v1/wallet")
|
||||||
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
if wallet.wallet_type == 0:
|
if wallet.wallet_type == 0:
|
||||||
|
|
@ -140,16 +141,14 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
if data.description_hash or data.unhashed_description:
|
if data.description_hash or data.unhashed_description:
|
||||||
try:
|
try:
|
||||||
description_hash = (
|
description_hash = (
|
||||||
binascii.unhexlify(data.description_hash)
|
bytes.fromhex(data.description_hash) if data.description_hash else b""
|
||||||
if data.description_hash
|
|
||||||
else b""
|
|
||||||
)
|
)
|
||||||
unhashed_description = (
|
unhashed_description = (
|
||||||
binascii.unhexlify(data.unhashed_description)
|
bytes.fromhex(data.unhashed_description)
|
||||||
if data.unhashed_description
|
if data.unhashed_description
|
||||||
else b""
|
else b""
|
||||||
)
|
)
|
||||||
except binascii.Error:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
||||||
|
|
@ -659,7 +658,7 @@ async def img(request: Request, data):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/audit/", dependencies=[Depends(check_admin)])
|
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
|
||||||
async def api_auditor():
|
async def api_auditor():
|
||||||
WALLET = get_wallet_class()
|
WALLET = get_wallet_class()
|
||||||
total_balance = await get_total_balance()
|
total_balance = await get_total_balance()
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ async def favicon():
|
||||||
|
|
||||||
|
|
||||||
@core_html_routes.get("/", response_class=HTMLResponse)
|
@core_html_routes.get("/", response_class=HTMLResponse)
|
||||||
async def home(request: Request, lightning: str = None):
|
async def home(request: Request, lightning: str = ""):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"core/index.html", {"request": request, "lnurl": lightning}
|
"core/index.html", {"request": request, "lnurl": lightning}
|
||||||
)
|
)
|
||||||
|
|
@ -124,12 +124,15 @@ async def wallet(
|
||||||
if (
|
if (
|
||||||
len(settings.lnbits_allowed_users) > 0
|
len(settings.lnbits_allowed_users) > 0
|
||||||
and user_id not in settings.lnbits_allowed_users
|
and user_id not in settings.lnbits_allowed_users
|
||||||
|
and user_id not in settings.lnbits_admin_users
|
||||||
|
and user_id != settings.super_user
|
||||||
):
|
):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"error.html", {"request": request, "err": "User not authorized."}
|
"error.html", {"request": request, "err": "User not authorized."}
|
||||||
)
|
)
|
||||||
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
||||||
user.admin = True
|
user.admin = True
|
||||||
|
|
||||||
if not wallet_id:
|
if not wallet_id:
|
||||||
if user.wallets and not wallet_name: # type: ignore
|
if user.wallets and not wallet_name: # type: ignore
|
||||||
wallet = user.wallets[0] # type: ignore
|
wallet = user.wallets[0] # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -236,8 +236,8 @@ async def check_user_exists(usr: UUID4) -> User:
|
||||||
if (
|
if (
|
||||||
len(settings.lnbits_allowed_users) > 0
|
len(settings.lnbits_allowed_users) > 0
|
||||||
and g().user.id not in settings.lnbits_allowed_users
|
and g().user.id not in settings.lnbits_allowed_users
|
||||||
and g().user.id != settings.super_user
|
|
||||||
and g().user.id not in settings.lnbits_admin_users
|
and g().user.id not in settings.lnbits_admin_users
|
||||||
|
and g().user.id != settings.super_user
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import urllib
|
import urllib
|
||||||
from binascii import unhexlify
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
|
@ -19,7 +18,7 @@ def generate_bleskomat_lnurl_signature(
|
||||||
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
|
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
|
||||||
):
|
):
|
||||||
if api_key_encoding == "hex":
|
if api_key_encoding == "hex":
|
||||||
key = unhexlify(api_key_secret)
|
key = bytes.fromhex(api_key_secret)
|
||||||
elif api_key_encoding == "base64":
|
elif api_key_encoding == "base64":
|
||||||
key = base64.b64decode(api_key_secret)
|
key = base64.b64decode(api_key_secret)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@
|
||||||
<p>
|
<p>
|
||||||
This extension allows you to connect a Bleskomat ATM to an lnbits
|
This extension allows you to connect a Bleskomat ATM to an lnbits
|
||||||
wallet. It will work with both the
|
wallet. It will work with both the
|
||||||
<a href="https://github.com/samotari/bleskomat"
|
<a class="text-secondary" href="https://github.com/samotari/bleskomat"
|
||||||
>open-source DIY Bleskomat ATM project</a
|
>open-source DIY Bleskomat ATM project</a
|
||||||
>
|
>
|
||||||
as well as the
|
as well as the
|
||||||
<a href="https://www.bleskomat.com/">commercial Bleskomat ATM</a>.
|
<a class="text-secondary" href="https://www.bleskomat.com/"
|
||||||
|
>commercial Bleskomat ATM</a
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<h5 class="text-subtitle1 q-my-none">Connect Your Bleskomat ATM</h5>
|
<h5 class="text-subtitle1 q-my-none">Connect Your Bleskomat ATM</h5>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
|
This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
|
||||||
|
|
||||||
<a href="https://www.youtube.com/watch?v=wJ7QLFTRjK0">Tutorial</a>
|
<a class="text-secondary" href="https://www.youtube.com/watch?v=wJ7QLFTRjK0">Tutorial</a>
|
||||||
|
|
||||||
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
||||||
|
|
||||||
|
|
@ -55,6 +55,8 @@ Since v0.1.2 of Boltcard NFC Card Creator it is possible not only reset the keys
|
||||||
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
||||||
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
||||||
|
|
||||||
|
If you somehow find yourself in some non-standard state (for instance only k3 and k4 remains filled after previous unsuccessful reset), then you need edit the key fields manually (for instance leave k0-k2 to zeroes and provide the right k3 and k4).
|
||||||
|
|
||||||
## Setting the card - computer (hard way)
|
## Setting the card - computer (hard way)
|
||||||
|
|
||||||
Follow the guide.
|
Follow the guide.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
Manage your Bolt Cards self custodian way<br />
|
Manage your Bolt Cards self custodian way<br />
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards"
|
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards"
|
||||||
>More details</a
|
>More details</a
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,7 @@
|
||||||
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||||
(QR for <strong>create</strong> the card in
|
(QR for <strong>create</strong> the card in
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="color: inherit"
|
style="color: inherit"
|
||||||
|
|
@ -395,6 +396,7 @@
|
||||||
<p class="text-center" v-show="qrCodeDialog.wipe">
|
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||||
(QR for <strong>wipe</strong> the card in
|
(QR for <strong>wipe</strong> the card in
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="color: inherit"
|
style="color: inherit"
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ async def api_hits(
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.get("/api/v1/refunds")
|
@boltcards_ext.get("/api/v1/refunds")
|
||||||
async def api_hits(
|
async def api_refunds(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from binascii import hexlify, unhexlify
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Awaitable, Union
|
from typing import Awaitable, Union
|
||||||
|
|
||||||
|
|
@ -56,7 +55,7 @@ async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
||||||
refund_pubkey_hex = hexlify(refund_privkey.sec()).decode("UTF-8")
|
refund_pubkey_hex = bytes.hex(refund_privkey.sec()).decode("UTF-8")
|
||||||
|
|
||||||
res = req_wrap(
|
res = req_wrap(
|
||||||
"post",
|
"post",
|
||||||
|
|
@ -121,7 +120,7 @@ async def create_reverse_swap(
|
||||||
return False
|
return False
|
||||||
|
|
||||||
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
||||||
claim_pubkey_hex = hexlify(claim_privkey.sec()).decode("UTF-8")
|
claim_pubkey_hex = bytes.hex(claim_privkey.sec()).decode("UTF-8")
|
||||||
preimage = os.urandom(32)
|
preimage = os.urandom(32)
|
||||||
preimage_hash = sha256(preimage).hexdigest()
|
preimage_hash = sha256(preimage).hexdigest()
|
||||||
|
|
||||||
|
|
@ -311,12 +310,12 @@ async def create_onchain_tx(
|
||||||
sequence = 0xFFFFFFFE
|
sequence = 0xFFFFFFFE
|
||||||
else:
|
else:
|
||||||
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
|
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
|
||||||
preimage = unhexlify(swap.preimage)
|
preimage = bytes.fromhex(swap.preimage)
|
||||||
onchain_address = swap.onchain_address
|
onchain_address = swap.onchain_address
|
||||||
sequence = 0xFFFFFFFF
|
sequence = 0xFFFFFFFF
|
||||||
|
|
||||||
locktime = swap.timeout_block_height
|
locktime = swap.timeout_block_height
|
||||||
redeem_script = unhexlify(swap.redeem_script)
|
redeem_script = bytes.fromhex(swap.redeem_script)
|
||||||
|
|
||||||
fees = get_fee_estimation()
|
fees = get_fee_estimation()
|
||||||
|
|
||||||
|
|
@ -324,7 +323,7 @@ async def create_onchain_tx(
|
||||||
|
|
||||||
script_pubkey = script.address_to_scriptpubkey(onchain_address)
|
script_pubkey = script.address_to_scriptpubkey(onchain_address)
|
||||||
|
|
||||||
vin = [TransactionInput(unhexlify(txid), vout_cnt, sequence=sequence)]
|
vin = [TransactionInput(bytes.fromhex(txid), vout_cnt, sequence=sequence)]
|
||||||
vout = [TransactionOutput(vout_amount - fees, script_pubkey)]
|
vout = [TransactionOutput(vout_amount - fees, script_pubkey)]
|
||||||
tx = Transaction(vin=vin, vout=vout)
|
tx = Transaction(vin=vin, vout=vout)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from binascii import hexlify
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import websockets
|
import websockets
|
||||||
|
|
@ -84,7 +83,7 @@ def get_mempool_blockheight() -> int:
|
||||||
|
|
||||||
|
|
||||||
async def send_onchain_tx(tx: Transaction):
|
async def send_onchain_tx(tx: Transaction):
|
||||||
raw = hexlify(tx.serialize())
|
raw = bytes.hex(tx.serialize())
|
||||||
logger.debug(f"Boltz - mempool sending onchain tx...")
|
logger.debug(f"Boltz - mempool sending onchain tx...")
|
||||||
req_wrap(
|
req_wrap(
|
||||||
"post",
|
"post",
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,13 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Link :
|
Link :
|
||||||
<a target="_blank" href="https://boltz.exchange"
|
<a class="text-secondary" target="_blank" href="https://boltz.exchange"
|
||||||
>https://boltz.exchange
|
>https://boltz.exchange
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||||
>More details</a
|
>More details</a
|
||||||
|
|
@ -38,7 +39,12 @@
|
||||||
<p>
|
<p>
|
||||||
<small
|
<small
|
||||||
>Created by,
|
>Created by,
|
||||||
<a target="_blank" href="https://github.com/dni">dni</a></small
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/dni"
|
||||||
|
>dni</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from binascii import hexlify, unhexlify
|
|
||||||
from typing import Any, List, Optional, Union
|
from typing import Any, List, Optional, Union
|
||||||
|
|
||||||
from cashu.core.base import MintKeyset
|
from cashu.core.base import MintKeyset
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,24 @@
|
||||||
<p>Create Cashu ecash mints and wallets.</p>
|
<p>Create Cashu ecash mints and wallets.</p>
|
||||||
<small
|
<small
|
||||||
>Created by
|
>Created by
|
||||||
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,
|
<a
|
||||||
<a href="https://github.com/motorina0" target="_blank">vlad</a>,
|
class="text-secondary"
|
||||||
<a href="https://github.com/calle" target="_blank">calle</a>.</small
|
href="https://github.com/arcbtc"
|
||||||
|
target="_blank"
|
||||||
|
>arcbtc</a
|
||||||
|
>,
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/motorina0"
|
||||||
|
target="_blank"
|
||||||
|
>vlad</a
|
||||||
|
>,
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/calle"
|
||||||
|
target="_blank"
|
||||||
|
>calle</a
|
||||||
|
>.</small
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
></q-icon>
|
></q-icon>
|
||||||
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
class="q-my-xl text-white"
|
class="q-my-xl text-white"
|
||||||
style="font-size: 1.5rem"
|
style="font-size: 1.5rem"
|
||||||
href="../wallet?mint_id={{ mint_id }}"
|
href="../wallet?mint_id={{ mint_id }}"
|
||||||
|
|
@ -24,7 +25,11 @@
|
||||||
<h5 class="q-my-md">Read the following carefully!</h5>
|
<h5 class="q-my-md">Read the following carefully!</h5>
|
||||||
<p>
|
<p>
|
||||||
This is a
|
This is a
|
||||||
<a href="https://cashu.space/" style="color: white" target="”_blank”"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://cashu.space/"
|
||||||
|
style="color: white"
|
||||||
|
target="”_blank”"
|
||||||
>Cashu</a
|
>Cashu</a
|
||||||
>
|
>
|
||||||
mint. Cashu is an ecash system for Bitcoin.
|
mint. Cashu is an ecash system for Bitcoin.
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ page_container %}
|
||||||
size="lg"
|
size="lg"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="q-mr-md cursor-pointer"
|
class="q-mr-md cursor-pointer"
|
||||||
@click="recheckInvoice(props.row.hash)"
|
@click="checkInvoice(props.row.hash)"
|
||||||
>
|
>
|
||||||
Check
|
Check
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
@ -616,10 +616,10 @@ page_container %}
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center q-mb-lg">
|
<div v-else class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + invoiceData.bolt11">
|
<a class="text-secondary" :href="'lightning:' + invoiceData.bolt11">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="invoiceData.bolt11"
|
:value="'lightning:' + invoiceData.bolt11.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
>
|
>
|
||||||
|
|
@ -681,7 +681,7 @@ page_container %}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center q-mb-lg">
|
<div v-else class="text-center q-mb-lg">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<!-- <a :href="'cashu:' + sendData.tokensBase64"> -->
|
<!-- <a class="text-secondary" :href="'cashu:' + sendData.tokensBase64"> -->
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="disclaimerDialog.base_url + '?mint_id=' + mintId + '&recv_token=' + sendData.tokensBase64"
|
:value="disclaimerDialog.base_url + '?mint_id=' + mintId + '&recv_token=' + sendData.tokensBase64"
|
||||||
|
|
@ -1528,57 +1528,17 @@ page_container %}
|
||||||
return proofs.reduce((s, t) => (s += t.amount), 0)
|
return proofs.reduce((s, t) => (s += t.amount), 0)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteProofs: function (proofs) {
|
||||||
|
// delete proofs from this.proofs
|
||||||
|
const usedSecrets = proofs.map(p => p.secret)
|
||||||
|
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||||
|
this.storeProofs()
|
||||||
|
return this.proofs
|
||||||
|
},
|
||||||
|
|
||||||
//////////// API ///////////
|
//////////// API ///////////
|
||||||
clearAllWorkers: function () {
|
|
||||||
if (this.invoiceCheckListener) {
|
|
||||||
clearInterval(this.invoiceCheckListener)
|
|
||||||
}
|
|
||||||
if (this.tokensCheckSpendableListener) {
|
|
||||||
clearInterval(this.tokensCheckSpendableListener)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
invoiceCheckWorker: async function () {
|
|
||||||
let nInterval = 0
|
|
||||||
this.clearAllWorkers()
|
|
||||||
this.invoiceCheckListener = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
nInterval += 1
|
|
||||||
|
|
||||||
// exit loop after 2m
|
// MINT
|
||||||
if (nInterval > 40) {
|
|
||||||
console.log('### stopping invoice check worker')
|
|
||||||
this.clearAllWorkers()
|
|
||||||
}
|
|
||||||
console.log('### invoiceCheckWorker setInterval', nInterval)
|
|
||||||
console.log(this.invoiceData)
|
|
||||||
|
|
||||||
// this will throw an error if the invoice is pending
|
|
||||||
await this.recheckInvoice(this.invoiceData.hash, false)
|
|
||||||
|
|
||||||
// only without error (invoice paid) will we reach here
|
|
||||||
console.log('### stopping invoice check worker')
|
|
||||||
this.clearAllWorkers()
|
|
||||||
this.invoiceData.bolt11 = ''
|
|
||||||
this.showInvoiceDetails = false
|
|
||||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Payment received',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log('not paid yet')
|
|
||||||
}
|
|
||||||
}, 3000)
|
|
||||||
},
|
|
||||||
|
|
||||||
requestMintButton: async function () {
|
requestMintButton: async function () {
|
||||||
await this.requestMint()
|
await this.requestMint()
|
||||||
|
|
@ -1586,8 +1546,12 @@ page_container %}
|
||||||
await this.invoiceCheckWorker()
|
await this.invoiceCheckWorker()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /mint
|
||||||
|
|
||||||
requestMint: async function () {
|
requestMint: async function () {
|
||||||
// gets an invoice from the mint to get new tokens
|
/*
|
||||||
|
gets an invoice from the mint to get new tokens
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -1611,7 +1575,14 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /mint
|
||||||
|
|
||||||
mintApi: async function (amounts, payment_hash, verbose = true) {
|
mintApi: async function (amounts, payment_hash, verbose = true) {
|
||||||
|
/*
|
||||||
|
asks the mint to check whether the invoice with payment_hash has been paid
|
||||||
|
and requests signing of the attached outputs (blindedMessages)
|
||||||
|
*/
|
||||||
console.log('### promises', payment_hash)
|
console.log('### promises', payment_hash)
|
||||||
try {
|
try {
|
||||||
let secrets = await this.generateSecrets(amounts)
|
let secrets = await this.generateSecrets(amounts)
|
||||||
|
|
@ -1647,7 +1618,19 @@ page_container %}
|
||||||
}
|
}
|
||||||
this.proofs = this.proofs.concat(proofs)
|
this.proofs = this.proofs.concat(proofs)
|
||||||
this.storeProofs()
|
this.storeProofs()
|
||||||
|
|
||||||
|
// update UI
|
||||||
await this.setInvoicePaid(payment_hash)
|
await this.setInvoicePaid(payment_hash)
|
||||||
|
tokensBase64 = btoa(JSON.stringify(proofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: amount,
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
|
||||||
return proofs
|
return proofs
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
@ -1657,62 +1640,20 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitToSend: async function (proofs, amount, invlalidate = false) {
|
|
||||||
// splits proofs so the user can keep firstProofs, send scndProofs
|
|
||||||
try {
|
|
||||||
const spendableProofs = proofs.filter(p => !p.reserved)
|
|
||||||
if (this.sumProofs(spendableProofs) < amount) {
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Balance too low',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
throw Error('balance too low.')
|
|
||||||
}
|
|
||||||
let {fristProofs, scndProofs} = await this.split(
|
|
||||||
spendableProofs,
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
|
|
||||||
// set scndProofs in this.proofs as reserved
|
// SPLIT
|
||||||
const usedSecrets = proofs.map(p => p.secret)
|
|
||||||
for (let i = 0; i < this.proofs.length; i++) {
|
|
||||||
if (usedSecrets.includes(this.proofs[i].secret)) {
|
|
||||||
this.proofs[i].reserved = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (invlalidate) {
|
|
||||||
// delete tokens from db
|
|
||||||
this.proofs = fristProofs
|
|
||||||
// add new fristProofs, scndProofs to this.proofs
|
|
||||||
this.storeProofs()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {fristProofs, scndProofs}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
split: async function (proofs, amount) {
|
split: async function (proofs, amount) {
|
||||||
|
/*
|
||||||
|
supplies proofs and requests a split from the mint of these
|
||||||
|
proofs at a specific amount
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
if (proofs.length == 0) {
|
if (proofs.length == 0) {
|
||||||
throw new Error('no proofs provided.')
|
throw new Error('no proofs provided.')
|
||||||
}
|
}
|
||||||
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
||||||
// delete proofs from this.proofs
|
this.deleteProofs(proofs)
|
||||||
const usedSecrets = proofs.map(p => p.secret)
|
|
||||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
|
||||||
// add new fristProofs, scndProofs to this.proofs
|
// add new fristProofs, scndProofs to this.proofs
|
||||||
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
||||||
this.storeProofs()
|
this.storeProofs()
|
||||||
|
|
@ -1723,6 +1664,9 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /split
|
||||||
|
|
||||||
splitApi: async function (proofs, amount) {
|
splitApi: async function (proofs, amount) {
|
||||||
try {
|
try {
|
||||||
const total = this.sumProofs(proofs)
|
const total = this.sumProofs(proofs)
|
||||||
|
|
@ -1782,7 +1726,62 @@ page_container %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
splitToSend: async function (proofs, amount, invlalidate = false) {
|
||||||
|
/*
|
||||||
|
splits proofs so the user can keep firstProofs, send scndProofs.
|
||||||
|
then sets scndProofs as reserved.
|
||||||
|
|
||||||
|
if invalidate, scndProofs (the one to send) are invalidated
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
const spendableProofs = proofs.filter(p => !p.reserved)
|
||||||
|
if (this.sumProofs(spendableProofs) < amount) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Balance too low',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
throw Error('balance too low.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// call /split
|
||||||
|
|
||||||
|
let {fristProofs, scndProofs} = await this.split(
|
||||||
|
spendableProofs,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
// set scndProofs in this.proofs as reserved
|
||||||
|
const usedSecrets = proofs.map(p => p.secret)
|
||||||
|
for (let i = 0; i < this.proofs.length; i++) {
|
||||||
|
if (usedSecrets.includes(this.proofs[i].secret)) {
|
||||||
|
this.proofs[i].reserved = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (invlalidate) {
|
||||||
|
// delete scndProofs from db
|
||||||
|
this.deleteProofs(scndProofs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {fristProofs, scndProofs}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
redeem: async function () {
|
redeem: async function () {
|
||||||
|
/*
|
||||||
|
uses split to receive new tokens.
|
||||||
|
*/
|
||||||
this.showReceiveTokens = false
|
this.showReceiveTokens = false
|
||||||
console.log('### receive tokens', this.receiveData.tokensBase64)
|
console.log('### receive tokens', this.receiveData.tokensBase64)
|
||||||
try {
|
try {
|
||||||
|
|
@ -1793,6 +1792,9 @@ page_container %}
|
||||||
const proofs = JSON.parse(tokenJson)
|
const proofs = JSON.parse(tokenJson)
|
||||||
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
||||||
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
|
||||||
// HACK: we need to do this so the balance updates
|
// HACK: we need to do this so the balance updates
|
||||||
this.proofs = this.proofs.concat([])
|
this.proofs = this.proofs.concat([])
|
||||||
|
|
||||||
|
|
@ -1827,13 +1829,18 @@ page_container %}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendTokens: async function () {
|
sendTokens: async function () {
|
||||||
|
/*
|
||||||
|
calls splitToSend, displays token and kicks off the spendableWorker
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
// keep firstProofs, send scndProofs
|
// keep firstProofs, send scndProofs and delete them (invalidate=true)
|
||||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||||
this.proofs,
|
this.proofs,
|
||||||
this.sendData.amount,
|
this.sendData.amount,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// update UI
|
||||||
this.sendData.tokens = scndProofs
|
this.sendData.tokens = scndProofs
|
||||||
console.log('### this.sendData.tokens', this.sendData.tokens)
|
console.log('### this.sendData.tokens', this.sendData.tokens)
|
||||||
this.sendData.tokensBase64 = btoa(
|
this.sendData.tokensBase64 = btoa(
|
||||||
|
|
@ -1846,33 +1853,19 @@ page_container %}
|
||||||
date: currentDateStr(),
|
date: currentDateStr(),
|
||||||
token: this.sendData.tokensBase64
|
token: this.sendData.tokensBase64
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// store "pending" outgoing tokens in history table
|
||||||
this.storehistoryTokens()
|
this.storehistoryTokens()
|
||||||
|
|
||||||
this.checkTokenSpendableWorker()
|
this.checkTokenSpendableWorker()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkFees: async function (payment_request) {
|
|
||||||
const payload = {
|
// /melt
|
||||||
pr: payment_request
|
|
||||||
}
|
|
||||||
console.log('#### payload', JSON.stringify(payload))
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
`/cashu/api/v1/${this.mintId}/checkfees`,
|
|
||||||
'',
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
console.log('#### checkFees', payment_request, data.fee)
|
|
||||||
return data.fee
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
melt: async function () {
|
melt: async function () {
|
||||||
// todo: get fees from server and add to inputs
|
// todo: get fees from server and add to inputs
|
||||||
this.payInvoiceData.blocking = true
|
this.payInvoiceData.blocking = true
|
||||||
|
|
@ -1924,8 +1917,20 @@ page_container %}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
// delete spent tokens from db
|
// delete spent tokens from db
|
||||||
this.proofs = fristProofs
|
this.deleteProofs(scndProofs)
|
||||||
this.storeProofs()
|
|
||||||
|
// update UI
|
||||||
|
|
||||||
|
tokensBase64 = btoa(JSON.stringify(scndProofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: -amount,
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
amount: -amount,
|
amount: -amount,
|
||||||
bolt11: this.payInvoiceData.data.request,
|
bolt11: this.payInvoiceData.data.request,
|
||||||
|
|
@ -1953,13 +1958,95 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /check
|
||||||
|
|
||||||
|
checkProofsSpendable: async function (proofs, update_history = false) {
|
||||||
|
/*
|
||||||
|
checks with the mint whether an array of proofs is still
|
||||||
|
spendable or already invalidated
|
||||||
|
*/
|
||||||
|
const payload = {
|
||||||
|
proofs: proofs.flat()
|
||||||
|
}
|
||||||
|
console.log('#### payload', JSON.stringify(payload))
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/cashu/api/v1/${this.mintId}/check`,
|
||||||
|
'',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete proofs from database if it is spent
|
||||||
|
let spentProofs = proofs.filter((p, pidx) => !data[pidx])
|
||||||
|
if (spentProofs.length) {
|
||||||
|
this.deleteProofs(spentProofs)
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
if (update_history) {
|
||||||
|
tokensBase64 = btoa(JSON.stringify(spentProofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: -this.sumProofs(spentProofs),
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// /checkfees
|
||||||
|
checkFees: async function (payment_request) {
|
||||||
|
const payload = {
|
||||||
|
pr: payment_request
|
||||||
|
}
|
||||||
|
console.log('#### payload', JSON.stringify(payload))
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/cashu/api/v1/${this.mintId}/checkfees`,
|
||||||
|
'',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
console.log('#### checkFees', payment_request, data.fee)
|
||||||
|
return data.fee
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// /keys
|
||||||
|
|
||||||
|
fetchMintKeys: async function () {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/cashu/api/v1/${this.mintId}/keys`
|
||||||
|
)
|
||||||
|
this.keys = data
|
||||||
|
localStorage.setItem(
|
||||||
|
this.mintKey(this.mintId, 'keys'),
|
||||||
|
JSON.stringify(data)
|
||||||
|
)
|
||||||
|
},
|
||||||
setInvoicePaid: async function (payment_hash) {
|
setInvoicePaid: async function (payment_hash) {
|
||||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||||
invoice.status = 'paid'
|
invoice.status = 'paid'
|
||||||
this.storeinvoicesCashu()
|
this.storeinvoicesCashu()
|
||||||
},
|
},
|
||||||
recheckInvoice: async function (payment_hash, verbose = true) {
|
checkInvoice: async function (payment_hash, verbose = true) {
|
||||||
console.log('### recheckInvoice.hash', payment_hash)
|
console.log('### checkInvoice.hash', payment_hash)
|
||||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||||
try {
|
try {
|
||||||
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
||||||
|
|
@ -1969,15 +2056,15 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recheckPendingInvoices: async function () {
|
checkPendingInvoices: async function () {
|
||||||
for (const invoice of this.invoicesCashu) {
|
for (const invoice of this.invoicesCashu) {
|
||||||
if (invoice.status === 'pending' && invoice.sat > 0) {
|
if (invoice.status === 'pending' && invoice.amount > 0) {
|
||||||
this.recheckInvoice(invoice.hash, false)
|
this.checkInvoice(invoice.hash, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
recheckPendingTokens: async function () {
|
checkPendingTokens: async function () {
|
||||||
for (const token of this.historyTokens) {
|
for (const token of this.historyTokens) {
|
||||||
if (token.status === 'pending' && token.amount < 0) {
|
if (token.status === 'pending' && token.amount < 0) {
|
||||||
this.checkTokenSpendable(token.token, false)
|
this.checkTokenSpendable(token.token, false)
|
||||||
|
|
@ -1990,6 +2077,113 @@ page_container %}
|
||||||
this.storehistoryTokens()
|
this.storehistoryTokens()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkTokenSpendable: async function (token, verbose = true) {
|
||||||
|
/*
|
||||||
|
checks whether a base64-encoded token (from the history table) has been spent already.
|
||||||
|
if it is spent, the appropraite entry in the history table is set to paid.
|
||||||
|
*/
|
||||||
|
const tokenJson = atob(token)
|
||||||
|
const proofs = JSON.parse(tokenJson)
|
||||||
|
let data = await this.checkProofsSpendable(proofs)
|
||||||
|
|
||||||
|
// iterate through response of form {0: true, 1: false, ...}
|
||||||
|
let paid = false
|
||||||
|
for (const [key, spendable] of Object.entries(data)) {
|
||||||
|
if (!spendable) {
|
||||||
|
this.setTokenPaid(token)
|
||||||
|
paid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paid) {
|
||||||
|
console.log('### token paid')
|
||||||
|
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Token paid',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('### token not paid yet')
|
||||||
|
if (verbose) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
color: 'grey',
|
||||||
|
message: 'Token still pending',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.sendData.tokens = token
|
||||||
|
}
|
||||||
|
return paid
|
||||||
|
},
|
||||||
|
|
||||||
|
////////////// WORKERS //////////////
|
||||||
|
|
||||||
|
clearAllWorkers: function () {
|
||||||
|
if (this.invoiceCheckListener) {
|
||||||
|
clearInterval(this.invoiceCheckListener)
|
||||||
|
}
|
||||||
|
if (this.tokensCheckSpendableListener) {
|
||||||
|
clearInterval(this.tokensCheckSpendableListener)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invoiceCheckWorker: async function () {
|
||||||
|
let nInterval = 0
|
||||||
|
this.clearAllWorkers()
|
||||||
|
this.invoiceCheckListener = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
nInterval += 1
|
||||||
|
|
||||||
|
// exit loop after 2m
|
||||||
|
if (nInterval > 40) {
|
||||||
|
console.log('### stopping invoice check worker')
|
||||||
|
this.clearAllWorkers()
|
||||||
|
}
|
||||||
|
console.log('### invoiceCheckWorker setInterval', nInterval)
|
||||||
|
console.log(this.invoiceData)
|
||||||
|
|
||||||
|
// this will throw an error if the invoice is pending
|
||||||
|
await this.checkInvoice(this.invoiceData.hash, false)
|
||||||
|
|
||||||
|
// only without error (invoice paid) will we reach here
|
||||||
|
console.log('### stopping invoice check worker')
|
||||||
|
this.clearAllWorkers()
|
||||||
|
this.invoiceData.bolt11 = ''
|
||||||
|
this.showInvoiceDetails = false
|
||||||
|
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Payment received',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('not paid yet')
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
checkTokenSpendableWorker: async function () {
|
checkTokenSpendableWorker: async function () {
|
||||||
let nInterval = 0
|
let nInterval = 0
|
||||||
this.clearAllWorkers()
|
this.clearAllWorkers()
|
||||||
|
|
@ -2021,83 +2215,6 @@ page_container %}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkTokenSpendable: async function (token, verbose = true) {
|
|
||||||
const tokenJson = atob(token)
|
|
||||||
const proofs = JSON.parse(tokenJson)
|
|
||||||
const payload = {
|
|
||||||
proofs: proofs.flat()
|
|
||||||
}
|
|
||||||
console.log('#### payload', JSON.stringify(payload))
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
`/cashu/api/v1/${this.mintId}/check`,
|
|
||||||
'',
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
// iterate through response of form {0: true, 1: false, ...}
|
|
||||||
let paid = false
|
|
||||||
for (const [key, spendable] of Object.entries(data)) {
|
|
||||||
if (!spendable) {
|
|
||||||
this.setTokenPaid(token)
|
|
||||||
paid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paid) {
|
|
||||||
console.log('### token paid')
|
|
||||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Token paid',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('### token not paid yet')
|
|
||||||
if (verbose) {
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
color: 'grey',
|
|
||||||
message: 'Token still pending',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.sendData.tokens = token
|
|
||||||
}
|
|
||||||
return paid
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMintKeys: async function () {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/cashu/api/v1/${this.mintId}/keys`
|
|
||||||
)
|
|
||||||
this.keys = data
|
|
||||||
localStorage.setItem(
|
|
||||||
this.mintKey(this.mintId, 'keys'),
|
|
||||||
JSON.stringify(data)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -2116,62 +2233,62 @@ page_container %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkInvoice: function () {
|
// checkInvoice: function () {
|
||||||
console.log('#### checkInvoice')
|
// console.log('#### checkInvoice')
|
||||||
try {
|
// try {
|
||||||
const invoice = decode(this.payInvoiceData.data.request)
|
// const invoice = decode(this.payInvoiceData.data.request)
|
||||||
|
|
||||||
const cleanInvoice = {
|
// const cleanInvoice = {
|
||||||
msat: invoice.human_readable_part.amount,
|
// msat: invoice.human_readable_part.amount,
|
||||||
sat: invoice.human_readable_part.amount / 1000,
|
// sat: invoice.human_readable_part.amount / 1000,
|
||||||
fsat: LNbits.utils.formatSat(
|
// fsat: LNbits.utils.formatSat(
|
||||||
invoice.human_readable_part.amount / 1000
|
// invoice.human_readable_part.amount / 1000
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
_.each(invoice.data.tags, tag => {
|
// _.each(invoice.data.tags, tag => {
|
||||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
// if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||||
if (tag.description === 'payment_hash') {
|
// if (tag.description === 'payment_hash') {
|
||||||
cleanInvoice.hash = tag.value
|
// cleanInvoice.hash = tag.value
|
||||||
} else if (tag.description === 'description') {
|
// } else if (tag.description === 'description') {
|
||||||
cleanInvoice.description = tag.value
|
// cleanInvoice.description = tag.value
|
||||||
} else if (tag.description === 'expiry') {
|
// } else if (tag.description === 'expiry') {
|
||||||
var expireDate = new Date(
|
// var expireDate = new Date(
|
||||||
(invoice.data.time_stamp + tag.value) * 1000
|
// (invoice.data.time_stamp + tag.value) * 1000
|
||||||
)
|
// )
|
||||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
// cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||||
expireDate,
|
// expireDate,
|
||||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
// 'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
)
|
// )
|
||||||
cleanInvoice.expired = false // TODO
|
// cleanInvoice.expired = false // TODO
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.payInvoiceData.invoice = cleanInvoice
|
// this.payInvoiceData.invoice = cleanInvoice
|
||||||
})
|
// })
|
||||||
|
|
||||||
console.log(
|
// console.log(
|
||||||
'#### this.payInvoiceData.invoice',
|
// '#### this.payInvoiceData.invoice',
|
||||||
this.payInvoiceData.invoice
|
// this.payInvoiceData.invoice
|
||||||
)
|
// )
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
this.$q.notify({
|
// this.$q.notify({
|
||||||
timeout: 5000,
|
// timeout: 5000,
|
||||||
type: 'warning',
|
// type: 'warning',
|
||||||
message: 'Could not decode invoice',
|
// message: 'Could not decode invoice',
|
||||||
caption: error + '',
|
// caption: error + '',
|
||||||
position: 'top',
|
// position: 'top',
|
||||||
actions: [
|
// actions: [
|
||||||
{
|
// {
|
||||||
icon: 'close',
|
// icon: 'close',
|
||||||
color: 'white',
|
// color: 'white',
|
||||||
handler: () => {}
|
// handler: () => {}
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
})
|
// })
|
||||||
throw error
|
// throw error
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
|
|
||||||
////////////// STORAGE /////////////
|
////////////// STORAGE /////////////
|
||||||
|
|
||||||
|
|
@ -2335,8 +2452,9 @@ page_container %}
|
||||||
console.log('#### this.mintId', this.mintId)
|
console.log('#### this.mintId', this.mintId)
|
||||||
console.log('#### this.mintName', this.mintName)
|
console.log('#### this.mintName', this.mintName)
|
||||||
|
|
||||||
this.recheckPendingInvoices()
|
this.checkProofsSpendable(this.proofs, true)
|
||||||
this.recheckPendingTokens()
|
this.checkPendingInvoices()
|
||||||
|
this.checkPendingTokens()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,16 @@ from .models import Cashu
|
||||||
|
|
||||||
# --------- extension imports
|
# --------- extension imports
|
||||||
|
|
||||||
|
# WARNING: Do not set this to False in production! This will create
|
||||||
|
# tokens for free otherwise. This is for testing purposes only!
|
||||||
|
|
||||||
LIGHTNING = True
|
LIGHTNING = True
|
||||||
|
|
||||||
|
if not LIGHTNING:
|
||||||
|
logger.warning(
|
||||||
|
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
||||||
|
)
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
############### LNBITS MINTS ###########
|
############### LNBITS MINTS ###########
|
||||||
########################################
|
########################################
|
||||||
|
|
@ -130,6 +137,28 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||||
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}")
|
||||||
|
async def keyset_keys(
|
||||||
|
cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
|
||||||
|
) -> dict[int, str]:
|
||||||
|
"""
|
||||||
|
Get the public keys of the mint of a specificy keyset id.
|
||||||
|
The id is encoded in base64_urlsafe and needs to be converted back to
|
||||||
|
normal base64 before it can be processed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||||
|
keyset = ledger.get_keyset(keyset_id=id)
|
||||||
|
return keyset
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
||||||
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||||
"""Get the public keys of the mint"""
|
"""Get the public keys of the mint"""
|
||||||
|
|
@ -182,7 +211,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||||
async def mint_coins(
|
async def mint(
|
||||||
data: MintRequest,
|
data: MintRequest,
|
||||||
cashu_id: str = Query(None),
|
cashu_id: str = Query(None),
|
||||||
payment_hash: str = Query(None),
|
payment_hash: str = Query(None),
|
||||||
|
|
@ -197,6 +226,8 @@ async def mint_coins(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||||
|
|
||||||
if LIGHTNING:
|
if LIGHTNING:
|
||||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||||
db=ledger.db, hash=payment_hash
|
db=ledger.db, hash=payment_hash
|
||||||
|
|
@ -206,42 +237,55 @@ async def mint_coins(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Mint does not know this invoice.",
|
detail="Mint does not know this invoice.",
|
||||||
)
|
)
|
||||||
if invoice.issued == True:
|
if invoice.issued:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
detail="Tokens already issued for this invoice.",
|
detail="Tokens already issued for this invoice.",
|
||||||
)
|
)
|
||||||
|
|
||||||
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
# set this invoice as issued
|
||||||
if total_requested > invoice.amount:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
|
||||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
|
||||||
)
|
|
||||||
|
|
||||||
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
|
||||||
|
|
||||||
if LIGHTNING and status.paid != True:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
|
||||||
|
|
||||||
promises = await ledger._generate_promises(
|
|
||||||
B_s=data.blinded_messages, keyset=keyset
|
|
||||||
)
|
|
||||||
assert len(promises), HTTPException(
|
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned."
|
|
||||||
)
|
|
||||||
await ledger.crud.update_lightning_invoice(
|
await ledger.crud.update_lightning_invoice(
|
||||||
db=ledger.db, hash=payment_hash, issued=True
|
db=ledger.db, hash=payment_hash, issued=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status: PaymentStatus = await check_transaction_status(
|
||||||
|
cashu.wallet, payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||||
|
if total_requested > invoice.amount:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
|
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not status.paid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||||
|
)
|
||||||
|
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
|
return promises
|
||||||
|
except (Exception, HTTPException) as e:
|
||||||
|
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
||||||
|
# unset issued flag because something went wrong
|
||||||
|
await ledger.crud.update_lightning_invoice(
|
||||||
|
db=ledger.db, hash=payment_hash, issued=False
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=getattr(e, "status_code")
|
||||||
|
or HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail=str(e) or getattr(e, "detail"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# only used for testing when LIGHTNING=false
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
return promises
|
return promises
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||||
|
|
@ -285,28 +329,38 @@ async def melt_coins(
|
||||||
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||||
)
|
)
|
||||||
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
||||||
await pay_invoice(
|
try:
|
||||||
wallet_id=cashu.wallet,
|
await pay_invoice(
|
||||||
payment_request=invoice,
|
wallet_id=cashu.wallet,
|
||||||
description=f"Pay cashu invoice",
|
payment_request=invoice,
|
||||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
description=f"Pay cashu invoice",
|
||||||
)
|
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||||
|
)
|
||||||
logger.debug(
|
except Exception as e:
|
||||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
||||||
)
|
raise e
|
||||||
status: PaymentStatus = await check_transaction_status(
|
finally:
|
||||||
cashu.wallet, invoice_obj.payment_hash
|
logger.debug(
|
||||||
)
|
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||||
if status.paid == True:
|
)
|
||||||
logger.debug("Cashu: Payment successful, invalidating proofs")
|
status: PaymentStatus = await check_transaction_status(
|
||||||
await ledger._invalidate_proofs(proofs)
|
cashu.wallet, invoice_obj.payment_hash
|
||||||
|
)
|
||||||
|
if status.paid == True:
|
||||||
|
logger.debug(
|
||||||
|
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
||||||
|
)
|
||||||
|
await ledger._invalidate_proofs(proofs)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=f"Cashu: {str(e)}",
|
detail=f"Cashu: {str(e)}",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}")
|
||||||
# delete proofs from pending list
|
# delete proofs from pending list
|
||||||
await ledger._unset_proofs_pending(proofs)
|
await ledger._unset_proofs_pending(proofs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
||||||
animation<br />
|
animation<br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
>
|
>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="copilot.lnurl"
|
:value="'lightning:' + copilot.lnurl"
|
||||||
:options="{width:250}"
|
:options="{width:250}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,24 @@
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
Connect your LNbits instance to a
|
Connect your LNbits instance to a
|
||||||
<a href="https://github.com/chrislennon/lnbits-discord-bot"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/chrislennon/lnbits-discord-bot"
|
||||||
>Discord Bot</a
|
>Discord Bot</a
|
||||||
>
|
>
|
||||||
leveraging LNbits as a community based lightning node.<br />
|
leveraging LNbits as a community based lightning node.<br />
|
||||||
<small>
|
<small>
|
||||||
Created by,
|
Created by,
|
||||||
<a href="https://github.com/chrislennon">Chris Lennon</a></small
|
<a class="text-secondary" href="https://github.com/chrislennon"
|
||||||
|
>Chris Lennon</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
Based on User Manager, by
|
Based on User Manager, by
|
||||||
<a href="https://github.com/benarc">Ben Arc</a></small
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
This extension is designed to be used through its API by a Discord Bot,
|
This extension is designed to be used through its API by a Discord Bot,
|
||||||
currently you have to install the bot
|
currently you have to install the bot
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://github.com/chrislennon/lnbits-discord-bot/#installation"
|
href="https://github.com/chrislennon/lnbits-discord-bot/#installation"
|
||||||
>yourself</a
|
>yourself</a
|
||||||
><br />
|
><br />
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
Events comes with a shareable ticket scanner, which can be used to
|
Events comes with a shareable ticket scanner, which can be used to
|
||||||
register attendees.<br />
|
register attendees.<br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc">Ben Arc</a>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="paymentReq"
|
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
13
lnbits/extensions/gerty/README.md
Normal file
13
lnbits/extensions/gerty/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Gerty
|
||||||
|
|
||||||
|
## Your desktop bitcoin assistant
|
||||||
|
|
||||||
|
Buy here `<link>`
|
||||||
|
|
||||||
|
blah blah blah
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Enable extension
|
||||||
|
2. Fill out form
|
||||||
|
3. point gerty at the server and give it the Gerty ID
|
||||||
30
lnbits/extensions/gerty/__init__.py
Normal file
30
lnbits/extensions/gerty/__init__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.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_gerty")
|
||||||
|
|
||||||
|
|
||||||
|
gerty_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/gerty/static",
|
||||||
|
"app": StaticFiles(packages=[("lnbits", "extensions/gerty/static")]),
|
||||||
|
"name": "gerty_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
gerty_ext: APIRouter = APIRouter(prefix="/gerty", tags=["Gerty"])
|
||||||
|
|
||||||
|
|
||||||
|
def gerty_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/gerty/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
||||||
6
lnbits/extensions/gerty/config.json
Normal file
6
lnbits/extensions/gerty/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Gerty",
|
||||||
|
"short_description": "Desktop bitcoin Assistant",
|
||||||
|
"icon": "sentiment_satisfied",
|
||||||
|
"contributors": ["arcbtc", "blackcoffeebtc"]
|
||||||
|
}
|
||||||
137
lnbits/extensions/gerty/crud.py
Normal file
137
lnbits/extensions/gerty/crud.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import Gerty, Mempool, MempoolEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
|
||||||
|
gerty_id = urlsafe_short_hash()
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO gerty.gertys (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
utc_offset,
|
||||||
|
type,
|
||||||
|
wallet,
|
||||||
|
lnbits_wallets,
|
||||||
|
mempool_endpoint,
|
||||||
|
exchange,
|
||||||
|
display_preferences,
|
||||||
|
refresh_time,
|
||||||
|
urls
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
gerty_id,
|
||||||
|
data.name,
|
||||||
|
data.utc_offset,
|
||||||
|
data.type,
|
||||||
|
wallet_id,
|
||||||
|
data.lnbits_wallets,
|
||||||
|
data.mempool_endpoint,
|
||||||
|
data.exchange,
|
||||||
|
data.display_preferences,
|
||||||
|
data.refresh_time,
|
||||||
|
data.urls,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
assert gerty, "Newly created gerty couldn't be retrieved"
|
||||||
|
return gerty
|
||||||
|
|
||||||
|
|
||||||
|
async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
|
||||||
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE gerty.gertys SET {q} WHERE id = ?", (*kwargs.values(), gerty_id)
|
||||||
|
)
|
||||||
|
return await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_gerty(gerty_id: str) -> Optional[Gerty]:
|
||||||
|
row = await db.fetchone("SELECT * FROM gerty.gertys WHERE id = ?", (gerty_id,))
|
||||||
|
return Gerty(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_gertys(wallet_ids: Union[str, List[str]]) -> List[Gerty]:
|
||||||
|
if isinstance(wallet_ids, str):
|
||||||
|
wallet_ids = [wallet_ids]
|
||||||
|
|
||||||
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"SELECT * FROM gerty.gertys WHERE wallet IN ({q})", (*wallet_ids,)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Gerty(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_gerty(gerty_id: str) -> None:
|
||||||
|
await db.execute("DELETE FROM gerty.gertys WHERE id = ?", (gerty_id,))
|
||||||
|
|
||||||
|
|
||||||
|
#############MEMPOOL###########
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]:
|
||||||
|
logger.debug(endPoint)
|
||||||
|
endpoints = MempoolEndpoint()
|
||||||
|
url = ""
|
||||||
|
for endpoint in endpoints:
|
||||||
|
if endPoint == endpoint[0]:
|
||||||
|
url = endpoint[1]
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM gerty.mempool WHERE endpoint = ? AND mempool_endpoint = ?",
|
||||||
|
(
|
||||||
|
endPoint,
|
||||||
|
gerty.mempool_endpoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(gerty.mempool_endpoint + url)
|
||||||
|
logger.debug(gerty.mempool_endpoint + url)
|
||||||
|
mempool_id = urlsafe_short_hash()
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO gerty.mempool (
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
endpoint,
|
||||||
|
time,
|
||||||
|
mempool_endpoint
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
mempool_id,
|
||||||
|
json.dumps(response.json()),
|
||||||
|
endPoint,
|
||||||
|
int(time.time()),
|
||||||
|
gerty.mempool_endpoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
if int(time.time()) - row.time > 20:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(gerty.mempool_endpoint + url)
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE gerty.mempool SET data = ?, time = ? WHERE endpoint = ? AND mempool_endpoint = ?",
|
||||||
|
(
|
||||||
|
json.dumps(response.json()),
|
||||||
|
int(time.time()),
|
||||||
|
endPoint,
|
||||||
|
gerty.mempool_endpoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
return json.loads(row.data)
|
||||||
951
lnbits/extensions/gerty/helpers.py
Normal file
951
lnbits/extensions/gerty/helpers.py
Normal file
|
|
@ -0,0 +1,951 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import textwrap
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||||
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
||||||
|
|
||||||
|
from .crud import get_gerty, get_mempool_info
|
||||||
|
from .number_prefixer import *
|
||||||
|
|
||||||
|
|
||||||
|
def get_percent_difference(current, previous, precision=3):
|
||||||
|
difference = (current - previous) / current * 100
|
||||||
|
return "{0}{1}%".format("+" if difference > 0 else "", round(difference, precision))
|
||||||
|
|
||||||
|
|
||||||
|
# A helper function get a nicely formated dict for the text
|
||||||
|
def get_text_item_dict(
|
||||||
|
text: str,
|
||||||
|
font_size: int,
|
||||||
|
x_pos: int = None,
|
||||||
|
y_pos: int = None,
|
||||||
|
gerty_type: str = "Gerty",
|
||||||
|
):
|
||||||
|
# Get line size by font size
|
||||||
|
line_width = 20
|
||||||
|
if font_size <= 12:
|
||||||
|
line_width = 60
|
||||||
|
elif font_size <= 15:
|
||||||
|
line_width = 45
|
||||||
|
elif font_size <= 20:
|
||||||
|
line_width = 35
|
||||||
|
elif font_size <= 40:
|
||||||
|
line_width = 25
|
||||||
|
|
||||||
|
# Get font sizes for Gerty mini
|
||||||
|
if gerty_type.lower() == "mini gerty":
|
||||||
|
if font_size <= 12:
|
||||||
|
font_size = 1
|
||||||
|
if font_size <= 15:
|
||||||
|
font_size = 1
|
||||||
|
elif font_size <= 20:
|
||||||
|
font_size = 2
|
||||||
|
elif font_size <= 40:
|
||||||
|
font_size = 2
|
||||||
|
else:
|
||||||
|
font_size = 5
|
||||||
|
|
||||||
|
# wrap the text
|
||||||
|
wrapper = textwrap.TextWrapper(width=line_width)
|
||||||
|
word_list = wrapper.wrap(text=text)
|
||||||
|
# logger.debug("number of chars = {0}".format(len(text)))
|
||||||
|
|
||||||
|
multilineText = "\n".join(word_list)
|
||||||
|
# logger.debug("number of lines = {0}".format(len(word_list)))
|
||||||
|
|
||||||
|
# logger.debug('multilineText')
|
||||||
|
# logger.debug(multilineText)
|
||||||
|
|
||||||
|
text = {"value": multilineText, "size": font_size}
|
||||||
|
if x_pos is None and y_pos is None:
|
||||||
|
text["position"] = "center"
|
||||||
|
else:
|
||||||
|
text["x"] = x_pos
|
||||||
|
text["y"] = y_pos
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# format a number for nice display output
|
||||||
|
def format_number(number, precision=None):
|
||||||
|
return "{:,}".format(round(number, precision))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mining_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
# current hashrate
|
||||||
|
r = await get_mempool_info("hashrate_1w", gerty)
|
||||||
|
data = r
|
||||||
|
hashrateNow = data["currentHashrate"]
|
||||||
|
hashrateOneWeekAgo = data["hashrates"][6]["avgHashrate"]
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current mining hashrate", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}hash".format(si_format(hashrateNow, 6, True, " ")),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} vs 7 days ago".format(
|
||||||
|
get_percent_difference(hashrateNow, hashrateOneWeekAgo, 3)
|
||||||
|
),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
|
||||||
|
# timeAvg
|
||||||
|
text = []
|
||||||
|
progress = "{0}%".format(round(r["progressPercent"], 2))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Progress through current epoch",
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=progress, font_size=60, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficulty adjustment
|
||||||
|
text = []
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Time to next difficulty adjustment",
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=get_time_remaining(stat / 1000, 3),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficultyChange
|
||||||
|
text = []
|
||||||
|
difficultyChange = round(r["difficultyChange"], 2)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Estimated difficulty change",
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}{1}%".format(
|
||||||
|
"+" if difficultyChange > 0 else "", round(difficultyChange, 2)
|
||||||
|
),
|
||||||
|
font_size=60,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
r = await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
data = r
|
||||||
|
stat = {}
|
||||||
|
stat["current"] = data["currentDifficulty"]
|
||||||
|
stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2]["difficulty"]
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lightning_stats(gerty):
|
||||||
|
data = await get_mempool_info("statistics", gerty)
|
||||||
|
areas = []
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text="Channel Count", font_size=12, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(data["latest"]["channel_count"]),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["channel_count"],
|
||||||
|
previous=data["previous"]["channel_count"],
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text="Number of Nodes", font_size=12, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(data["latest"]["node_count"]),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["node_count"], previous=data["previous"]["node_count"]
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text="Total Capacity", font_size=12, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
avg_capacity = float(data["latest"]["total_capacity"]) / float(100000000)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} BTC".format(format_number(avg_capacity, 2)),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["total_capacity"],
|
||||||
|
previous=data["previous"]["total_capacity"],
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Average Channel Capacity", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} sats".format(format_number(data["latest"]["avg_capacity"])),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["avg_capacity"],
|
||||||
|
previous=data["previous"]["avg_capacity"],
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_update_time(sleep_time_seconds: int = 0, utc_offset: int = 0):
|
||||||
|
utc_now = datetime.utcnow()
|
||||||
|
next_refresh_time = utc_now + timedelta(0, sleep_time_seconds)
|
||||||
|
local_refresh_time = next_refresh_time + timedelta(hours=utc_offset)
|
||||||
|
return "{0} {1}".format(
|
||||||
|
"I'll wake up at" if gerty_should_sleep(utc_offset) else "Next update at",
|
||||||
|
local_refresh_time.strftime("%H:%M on %e %b %Y"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gerty_should_sleep(utc_offset: int = 0):
|
||||||
|
utc_now = datetime.utcnow()
|
||||||
|
local_time = utc_now + timedelta(hours=utc_offset)
|
||||||
|
hours = local_time.strftime("%H")
|
||||||
|
hours = int(hours)
|
||||||
|
if hours >= 22 and hours <= 23:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mining_stat(stat_slug: str, gerty):
|
||||||
|
text = []
|
||||||
|
if stat_slug == "mining_current_hash_rate":
|
||||||
|
stat = await api_get_mining_stat(stat_slug, gerty)
|
||||||
|
current = "{0}hash".format(si_format(stat["current"], 6, True, " "))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current Mining Hashrate", font_size=20, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=current, font_size=40, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
# compare vs previous time period
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=stat["current"], previous=stat["1w"]
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "mining_current_difficulty":
|
||||||
|
stat = await api_get_mining_stat(stat_slug, gerty)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current Mining Difficulty", font_size=20, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(stat["current"]), font_size=40, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=stat["current"], previous=stat["previous"]
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} since last adjustment".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# text.append(get_text_item_dict("Required threshold for mining proof-of-work", 12))
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def api_get_mining_stat(stat_slug: str, gerty):
|
||||||
|
stat = ""
|
||||||
|
if stat_slug == "mining_current_hash_rate":
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
data = r
|
||||||
|
stat = {}
|
||||||
|
stat["current"] = data["currentHashrate"]
|
||||||
|
stat["1w"] = data["hashrates"][len(data["hashrates"]) - 7]["avgHashrate"]
|
||||||
|
elif stat_slug == "mining_current_difficulty":
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
data = r
|
||||||
|
stat = {}
|
||||||
|
stat["current"] = data["currentDifficulty"]
|
||||||
|
stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2][
|
||||||
|
"difficulty"
|
||||||
|
]
|
||||||
|
return stat
|
||||||
|
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
|
||||||
|
async def get_satoshi():
|
||||||
|
maxQuoteLength = 186
|
||||||
|
with open(
|
||||||
|
os.path.join(settings.lnbits_path, "extensions/gerty/static/satoshi.json")
|
||||||
|
) as fd:
|
||||||
|
satoshiQuotes = json.load(fd)
|
||||||
|
quote = satoshiQuotes[random.randint(0, len(satoshiQuotes) - 1)]
|
||||||
|
# logger.debug(quote.text)
|
||||||
|
if len(quote["text"]) > maxQuoteLength:
|
||||||
|
logger.debug("Quote is too long, getting another")
|
||||||
|
return await get_satoshi()
|
||||||
|
else:
|
||||||
|
return quote
|
||||||
|
|
||||||
|
|
||||||
|
# Get a screen slug by its position in the screens_list
|
||||||
|
def get_screen_slug_by_index(index: int, screens_list):
|
||||||
|
if index <= len(screens_list) - 1:
|
||||||
|
return list(screens_list)[index - 1]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Get a list of text items for the screen number
|
||||||
|
async def get_screen_data(screen_num: int, screens_list: dict, gerty):
|
||||||
|
screen_slug = get_screen_slug_by_index(screen_num, screens_list)
|
||||||
|
# first get the relevant slug from the display_preferences
|
||||||
|
areas = []
|
||||||
|
title = ""
|
||||||
|
|
||||||
|
if screen_slug == "dashboard":
|
||||||
|
title = gerty.name
|
||||||
|
areas = await get_dashboard(gerty)
|
||||||
|
if screen_slug == "lnbits_wallets_balance":
|
||||||
|
wallets = await get_lnbits_wallet_balances(gerty)
|
||||||
|
|
||||||
|
for wallet in wallets:
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}'s Wallet".format(wallet["name"]),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} sats".format(format_number(wallet["balance"])),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
elif screen_slug == "url_checker":
|
||||||
|
for url in json.loads(gerty.urls):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
text = []
|
||||||
|
try:
|
||||||
|
response = await client.get(url)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=url,
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=str(response.status_code),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=url,
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=str("DOWN"),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
elif screen_slug == "fun_satoshi_quotes":
|
||||||
|
areas.append(await get_satoshi_quotes(gerty))
|
||||||
|
elif screen_slug == "fun_exchange_market_rate":
|
||||||
|
areas.append(await get_exchange_rate(gerty))
|
||||||
|
elif screen_slug == "onchain_difficulty_epoch_progress":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "onchain_block_height":
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(await get_mempool_info("tip_height", gerty)),
|
||||||
|
font_size=80,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
elif screen_slug == "onchain_difficulty_retarget_date":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "onchain_difficulty_blocks_remaining":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "onchain_difficulty_epoch_time_remaining":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "dashboard_onchain":
|
||||||
|
title = "Onchain Data"
|
||||||
|
areas = await get_onchain_dashboard(gerty)
|
||||||
|
elif screen_slug == "mempool_recommended_fees":
|
||||||
|
areas.append(await get_mempool_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "mempool_tx_count":
|
||||||
|
areas.append(await get_mempool_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "mining_current_hash_rate":
|
||||||
|
areas.append(await get_mining_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "mining_current_difficulty":
|
||||||
|
areas.append(await get_mining_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "dashboard_mining":
|
||||||
|
title = "Mining Data"
|
||||||
|
areas = await get_mining_dashboard(gerty)
|
||||||
|
elif screen_slug == "lightning_dashboard":
|
||||||
|
title = "Lightning Network"
|
||||||
|
areas = await get_lightning_stats(gerty)
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data["title"] = title
|
||||||
|
data["areas"] = areas
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# Get the dashboard screen
|
||||||
|
async def get_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
# XC rate
|
||||||
|
text = []
|
||||||
|
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(amount), font_size=40, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="BTC{0} price".format(gerty.exchange),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
# balance
|
||||||
|
text = []
|
||||||
|
wallets = await get_lnbits_wallet_balances(gerty)
|
||||||
|
text = []
|
||||||
|
for wallet in wallets:
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(wallet["name"]), font_size=15, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} sats".format(format_number(wallet["balance"])),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# Mempool fees
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(await get_mempool_info("tip_height", gerty)),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current block height", font_size=15, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficulty adjustment time
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=await get_time_remaining_next_difficulty_adjustment(gerty),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="until next difficulty adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lnbits_wallet_balances(gerty):
|
||||||
|
# Get Wallet info
|
||||||
|
wallets = []
|
||||||
|
if gerty.lnbits_wallets != "":
|
||||||
|
for lnbits_wallet in json.loads(gerty.lnbits_wallets):
|
||||||
|
wallet = await get_wallet_for_key(key=lnbits_wallet)
|
||||||
|
if wallet:
|
||||||
|
wallets.append(
|
||||||
|
{
|
||||||
|
"name": wallet.name,
|
||||||
|
"balance": wallet.balance_msat / 1000,
|
||||||
|
"inkey": wallet.inkey,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return wallets
|
||||||
|
|
||||||
|
|
||||||
|
async def get_placeholder_text():
|
||||||
|
return [
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Some placeholder text",
|
||||||
|
x_pos=15,
|
||||||
|
y_pos=10,
|
||||||
|
font_size=50,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
),
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Some placeholder text",
|
||||||
|
x_pos=15,
|
||||||
|
y_pos=10,
|
||||||
|
font_size=50,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_satoshi_quotes(gerty):
|
||||||
|
# Get Satoshi quotes
|
||||||
|
text = []
|
||||||
|
quote = await get_satoshi()
|
||||||
|
if quote:
|
||||||
|
if quote["text"]:
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=quote["text"], font_size=15, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if quote["date"]:
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Satoshi Nakamoto - {0}".format(quote["date"]),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# Get Exchange Value
|
||||||
|
async def get_exchange_rate(gerty):
|
||||||
|
text = []
|
||||||
|
if gerty.exchange != "":
|
||||||
|
try:
|
||||||
|
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
||||||
|
if amount:
|
||||||
|
price = format_number(amount)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current {0}/BTC price".format(gerty.exchange),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=price, font_size=80, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def get_onchain_stat(stat_slug: str, gerty):
|
||||||
|
text = []
|
||||||
|
if (
|
||||||
|
stat_slug == "onchain_difficulty_epoch_progress"
|
||||||
|
or stat_slug == "onchain_difficulty_retarget_date"
|
||||||
|
or stat_slug == "onchain_difficulty_blocks_remaining"
|
||||||
|
or stat_slug == "onchain_difficulty_epoch_time_remaining"
|
||||||
|
):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
if stat_slug == "onchain_difficulty_epoch_progress":
|
||||||
|
stat = round(r["progressPercent"])
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Progress through current difficulty epoch",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}%".format(stat), font_size=80, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "onchain_difficulty_retarget_date":
|
||||||
|
stat = r["estimatedRetargetDate"]
|
||||||
|
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Date of next difficulty adjustment",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=dt, font_size=40, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
elif stat_slug == "onchain_difficulty_blocks_remaining":
|
||||||
|
stat = r["remainingBlocks"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Blocks until next difficulty adjustment",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(format_number(stat)),
|
||||||
|
font_size=80,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "onchain_difficulty_epoch_time_remaining":
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Time until next difficulty adjustment",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=get_time_remaining(stat / 1000, 4),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def get_onchain_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
text = []
|
||||||
|
stat = round(r["progressPercent"])
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Progress through epoch", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}%".format(stat), font_size=60, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r["estimatedRetargetDate"]
|
||||||
|
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Date of next adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=dt, font_size=20, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r["remainingBlocks"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Blocks until adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(format_number(stat)),
|
||||||
|
font_size=60,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Time until adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=get_time_remaining(stat / 1000, 4),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_time_remaining_next_difficulty_adjustment(gerty):
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
time = get_time_remaining(stat / 1000, 3)
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mempool_stat(stat_slug: str, gerty):
|
||||||
|
text = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
if stat_slug == "mempool_tx_count":
|
||||||
|
r = get_mempool_info("mempool", gerty)
|
||||||
|
if stat_slug == "mempool_tx_count":
|
||||||
|
stat = round(r["count"])
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Transactions in the mempool",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(format_number(stat)),
|
||||||
|
font_size=80,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "mempool_recommended_fees":
|
||||||
|
y_offset = 60
|
||||||
|
fees = await get_mempool_info("fees_recommended", gerty)
|
||||||
|
pos_y = 80 + y_offset
|
||||||
|
text.append(get_text_item_dict("mempool.space", 40, 160, pos_y, gerty.type))
|
||||||
|
pos_y = 180 + y_offset
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("Recommended Tx Fees", 20, 240, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
pos_y = 280 + y_offset
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("None"), 15, 30, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("Low"), 15, 235, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("Medium"), 15, 460, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("High"), 15, 750, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
pos_y = 340 + y_offset
|
||||||
|
font_size = 15
|
||||||
|
fee_append = "/vB"
|
||||||
|
fee_rate = fees["economyFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=30,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["hourFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=235,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["halfHourFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=460,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["fastestFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=750,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_suffix(dayNumber):
|
||||||
|
if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
|
||||||
|
return "th"
|
||||||
|
else:
|
||||||
|
return ["st", "nd", "rd"][dayNumber % 10 - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_remaining(seconds, granularity=2):
|
||||||
|
intervals = (
|
||||||
|
# ('weeks', 604800), # 60 * 60 * 24 * 7
|
||||||
|
("days", 86400), # 60 * 60 * 24
|
||||||
|
("hours", 3600), # 60 * 60
|
||||||
|
("minutes", 60),
|
||||||
|
("seconds", 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for name, count in intervals:
|
||||||
|
value = seconds // count
|
||||||
|
if value:
|
||||||
|
seconds -= value * count
|
||||||
|
if value == 1:
|
||||||
|
name = name.rstrip("s")
|
||||||
|
result.append("{} {}".format(round(value), name))
|
||||||
|
return ", ".join(result[:granularity])
|
||||||
59
lnbits/extensions/gerty/migrations.py
Normal file
59
lnbits/extensions/gerty/migrations.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
async def m001_initial(db):
|
||||||
|
"""
|
||||||
|
Initial Gertys table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE gerty.gertys (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
refresh_time INT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
lnbits_wallets TEXT,
|
||||||
|
mempool_endpoint TEXT,
|
||||||
|
exchange TEXT,
|
||||||
|
display_preferences TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m002_add_utc_offset_col(db):
|
||||||
|
"""
|
||||||
|
support for UTC offset
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;")
|
||||||
|
|
||||||
|
|
||||||
|
async def m003_add_gerty_model_col(db):
|
||||||
|
"""
|
||||||
|
support for Gerty model col
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE gerty.gertys ADD COLUMN type TEXT;")
|
||||||
|
|
||||||
|
|
||||||
|
#########MEMPOOL MIGRATIONS########
|
||||||
|
|
||||||
|
|
||||||
|
async def m004_initial(db):
|
||||||
|
"""
|
||||||
|
Initial Gertys table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE gerty.mempool (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
mempool_endpoint TEXT NOT NULL,
|
||||||
|
endpoint TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
time TIMESTAMP
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m005_add_gerty_model_col(db):
|
||||||
|
"""
|
||||||
|
support for Gerty model col
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE gerty.gertys ADD COLUMN urls TEXT;")
|
||||||
48
lnbits/extensions/gerty/models.py
Normal file
48
lnbits/extensions/gerty/models.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Gerty(BaseModel):
|
||||||
|
id: str = Query(None)
|
||||||
|
name: str
|
||||||
|
refresh_time: int = Query(None)
|
||||||
|
utc_offset: int = Query(None)
|
||||||
|
wallet: str = Query(None)
|
||||||
|
type: str
|
||||||
|
lnbits_wallets: str = Query(
|
||||||
|
None
|
||||||
|
) # Wallets to keep an eye on, {"wallet-id": "wallet-read-key, etc"}
|
||||||
|
mempool_endpoint: str = Query(None) # Mempool endpoint to use
|
||||||
|
exchange: str = Query(
|
||||||
|
None
|
||||||
|
) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
|
||||||
|
display_preferences: str = Query(None)
|
||||||
|
urls: str = Query(None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "Gerty":
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
#########MEMPOOL MODELS###########
|
||||||
|
|
||||||
|
|
||||||
|
class MempoolEndpoint(BaseModel):
|
||||||
|
fees_recommended: str = "/api/v1/fees/recommended"
|
||||||
|
hashrate_1w: str = "/api/v1/mining/hashrate/1w"
|
||||||
|
hashrate_1m: str = "/api/v1/mining/hashrate/1m"
|
||||||
|
statistics: str = "/api/v1/lightning/statistics/latest"
|
||||||
|
difficulty_adjustment: str = "/api/v1/difficulty-adjustment"
|
||||||
|
tip_height: str = "/api/blocks/tip/height"
|
||||||
|
mempool: str = "/api/mempool"
|
||||||
|
|
||||||
|
|
||||||
|
class Mempool(BaseModel):
|
||||||
|
id: str = Query(None)
|
||||||
|
mempool_endpoint: str = Query(None)
|
||||||
|
endpoint: str = Query(None)
|
||||||
|
data: str = Query(None)
|
||||||
|
time: int = Query(None)
|
||||||
66
lnbits/extensions/gerty/number_prefixer.py
Normal file
66
lnbits/extensions/gerty/number_prefixer.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def si_classifier(val):
|
||||||
|
suffixes = {
|
||||||
|
24: {"long_suffix": "yotta", "short_suffix": "Y", "scalar": 10**24},
|
||||||
|
21: {"long_suffix": "zetta", "short_suffix": "Z", "scalar": 10**21},
|
||||||
|
18: {"long_suffix": "exa", "short_suffix": "E", "scalar": 10**18},
|
||||||
|
15: {"long_suffix": "peta", "short_suffix": "P", "scalar": 10**15},
|
||||||
|
12: {"long_suffix": "tera", "short_suffix": "T", "scalar": 10**12},
|
||||||
|
9: {"long_suffix": "giga", "short_suffix": "G", "scalar": 10**9},
|
||||||
|
6: {"long_suffix": "mega", "short_suffix": "M", "scalar": 10**6},
|
||||||
|
3: {"long_suffix": "kilo", "short_suffix": "k", "scalar": 10**3},
|
||||||
|
0: {"long_suffix": "", "short_suffix": "", "scalar": 10**0},
|
||||||
|
-3: {"long_suffix": "milli", "short_suffix": "m", "scalar": 10**-3},
|
||||||
|
-6: {"long_suffix": "micro", "short_suffix": "µ", "scalar": 10**-6},
|
||||||
|
-9: {"long_suffix": "nano", "short_suffix": "n", "scalar": 10**-9},
|
||||||
|
-12: {"long_suffix": "pico", "short_suffix": "p", "scalar": 10**-12},
|
||||||
|
-15: {"long_suffix": "femto", "short_suffix": "f", "scalar": 10**-15},
|
||||||
|
-18: {"long_suffix": "atto", "short_suffix": "a", "scalar": 10**-18},
|
||||||
|
-21: {"long_suffix": "zepto", "short_suffix": "z", "scalar": 10**-21},
|
||||||
|
-24: {"long_suffix": "yocto", "short_suffix": "y", "scalar": 10**-24},
|
||||||
|
}
|
||||||
|
exponent = int(math.floor(math.log10(abs(val)) / 3.0) * 3)
|
||||||
|
return suffixes.get(exponent, None)
|
||||||
|
|
||||||
|
|
||||||
|
def si_formatter(value):
|
||||||
|
"""
|
||||||
|
Return a triple of scaled value, short suffix, long suffix, or None if
|
||||||
|
the value cannot be classified.
|
||||||
|
"""
|
||||||
|
classifier = si_classifier(value)
|
||||||
|
if classifier == None:
|
||||||
|
# Don't know how to classify this value
|
||||||
|
return None
|
||||||
|
|
||||||
|
scaled = value / classifier["scalar"]
|
||||||
|
return (scaled, classifier["short_suffix"], classifier["long_suffix"])
|
||||||
|
|
||||||
|
|
||||||
|
def si_format(value, precision=4, long_form=False, separator=""):
|
||||||
|
"""
|
||||||
|
"SI prefix" formatted string: return a string with the given precision
|
||||||
|
and an appropriate order-of-3-magnitudes suffix, e.g.:
|
||||||
|
si_format(1001.0) => '1.00K'
|
||||||
|
si_format(0.00000000123, long_form=True, separator=' ') => '1.230 nano'
|
||||||
|
"""
|
||||||
|
scaled, short_suffix, long_suffix = si_formatter(value)
|
||||||
|
|
||||||
|
if scaled == None:
|
||||||
|
# Don't know how to format this value
|
||||||
|
return value
|
||||||
|
|
||||||
|
suffix = long_suffix if long_form else short_suffix
|
||||||
|
|
||||||
|
if abs(scaled) < 10:
|
||||||
|
precision = precision - 1
|
||||||
|
elif abs(scaled) < 100:
|
||||||
|
precision = precision - 2
|
||||||
|
else:
|
||||||
|
precision = precision - 3
|
||||||
|
|
||||||
|
return "{scaled:.{precision}f}{separator}{suffix}".format(
|
||||||
|
scaled=scaled, precision=precision, separator=separator, suffix=suffix
|
||||||
|
)
|
||||||
BIN
lnbits/extensions/gerty/static/gerty.jpg
Normal file
BIN
lnbits/extensions/gerty/static/gerty.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
1099
lnbits/extensions/gerty/static/satoshi.json
Normal file
1099
lnbits/extensions/gerty/static/satoshi.json
Normal file
File diff suppressed because it is too large
Load diff
1099
lnbits/extensions/gerty/static/satoshi_long.json
Normal file
1099
lnbits/extensions/gerty/static/satoshi_long.json
Normal file
File diff suppressed because it is too large
Load diff
25
lnbits/extensions/gerty/templates/gerty/_api_docs.html
Normal file
25
lnbits/extensions/gerty/templates/gerty/_api_docs.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
Gerty (your bitcoin assistant): Use the software Gerty or
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://shop.lnbits.com/product/gerty-a-bitcoin-assistant"
|
||||||
|
>hardware Gerty</a
|
||||||
|
><br />
|
||||||
|
<small>
|
||||||
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/blackcoffeexbt"
|
||||||
|
>Black Coffee</a
|
||||||
|
>,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://shop.lnbits.com/product/gerty-a-bitcoin-assistant"
|
||||||
|
><img src="/gerty/static/gerty.jpg" style="max-width: 100%"
|
||||||
|
/></a>
|
||||||
|
</q-card-section>
|
||||||
244
lnbits/extensions/gerty/templates/gerty/gerty.html
Normal file
244
lnbits/extensions/gerty/templates/gerty/gerty.html
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
{% extends "public.html" %} {% block toolbar_title %} Gerty: {% raw %}{{
|
||||||
|
gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="q-pa-md row items-start q-gutter-md"
|
||||||
|
v-if="fun_exchange_market_rate || fun_satoshi_quotes"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
v-if="fun_exchange_market_rate"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-sm"
|
||||||
|
style="background: none !important"
|
||||||
|
>
|
||||||
|
<q-card-section class="text-h1 q-pa-none">
|
||||||
|
<small> <b>{{fun_exchange_market_rate["amount"]}}</b></small>
|
||||||
|
<small class="text-h4"
|
||||||
|
>{{fun_exchange_market_rate["unit"].split(" ")[1]}}</small
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card
|
||||||
|
v-if="fun_satoshi_quotes['quote']"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-none text-body1 blockquote"
|
||||||
|
style="background: none !important"
|
||||||
|
>
|
||||||
|
<blockquote class="text-right" style="max-width: 900px">
|
||||||
|
<p>"{{fun_satoshi_quotes["quote"]}}"</p>
|
||||||
|
<small>~ Satoshi {{fun_satoshi_quotes["date"]}}</small>
|
||||||
|
</blockquote>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pa-md row items-start q-gutter-md" v-if="lnbits_wallets_balance">
|
||||||
|
<q-card
|
||||||
|
class="q-pa-sm"
|
||||||
|
v-for="(wallet, t) in lnbits_wallets_balance"
|
||||||
|
:style="`background-color: ${wallet.color1} !important`"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-none q-pa-sm"
|
||||||
|
>
|
||||||
|
<q-card-section class="text-h1 q-pa-none">
|
||||||
|
<small> <b>{{wallet["amount"]}}</b></small>
|
||||||
|
<small class="text-h4">({{wallet["name"]}})</small>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="q-pa-md row items-start q-gutter-md"
|
||||||
|
v-if="dashboard_onchain || dashboard_mining || lightning_dashboard"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="q-pa-sm"
|
||||||
|
v-if="dashboard_onchain[0]"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-sm"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Onchain</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<p v-for="(item, t) in dashboard_onchain">
|
||||||
|
<b>{{item[0].value}}: </b>{{item[1].value}}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-pa-sm" v-if="dashboard_mining" unelevated class="q-pa-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Mining</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<p v-for="(item, t) in dashboard_mining">
|
||||||
|
<b>{{item[0].value}}:</b> {{item[1].value}}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-pa-sm" v-if="lightning_dashboard" unelevated class="q-pa-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Lightning (Last 7 days)</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<p v-for="(item, t) in lightning_dashboard">
|
||||||
|
<b>{{item[0].value}}:</b> {{item[1].value}}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-pa-sm" v-if="url_checker" unelevated class="q-pa-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Servers to check</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<div class="row q-pb-md" v-for="(item, t) in url_checker">
|
||||||
|
<div class="col-8">
|
||||||
|
<small>
|
||||||
|
<b style="word-wrap: break-word; max-width: 230px; display: block">
|
||||||
|
<a class="text-secondary" class="text-primary">
|
||||||
|
{{item[0].value}}
|
||||||
|
</a>
|
||||||
|
</b>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-chip
|
||||||
|
v-if="item[1].value < 300"
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
color="green"
|
||||||
|
text-color="white"
|
||||||
|
icon="sentiment_satisfied"
|
||||||
|
>
|
||||||
|
{{item[1].value}}
|
||||||
|
</q-chip>
|
||||||
|
<q-chip
|
||||||
|
v-else-if="item[1].value >= 300"
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
color="yellow"
|
||||||
|
text-color="white"
|
||||||
|
icon="sentiment_dissatisfied"
|
||||||
|
>
|
||||||
|
{{item[1].value}}
|
||||||
|
</q-chip>
|
||||||
|
<q-chip
|
||||||
|
v-else
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
color="red"
|
||||||
|
text-color="white"
|
||||||
|
icon="sentiment_dissatisfied"
|
||||||
|
>
|
||||||
|
{{item[1].value}}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endraw %} {% endblock %} {% block scripts %}
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
lnbits_wallets_balance: {},
|
||||||
|
dashboard_onchain: {},
|
||||||
|
fun_satoshi_quotes: {},
|
||||||
|
fun_exchange_market_rate: {},
|
||||||
|
gerty: [],
|
||||||
|
gerty_id: `{{gerty}}`,
|
||||||
|
gertyname: '',
|
||||||
|
walletColors: [
|
||||||
|
{first: '#3f51b5', second: '#1a237e'},
|
||||||
|
{first: '#9c27b0', second: '#4a148c'},
|
||||||
|
{first: '#e91e63', second: '#880e4f'},
|
||||||
|
{first: '#009688', second: '#004d40'},
|
||||||
|
{first: '#ff9800', second: '#e65100'},
|
||||||
|
{first: '#2196f3', second: '#0d47a1'},
|
||||||
|
{first: '#4caf50', second: '#1b5e20'}
|
||||||
|
],
|
||||||
|
gertywallets: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getGertyInfo: async function () {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/gerty/api/v1/gerty/pages/${this.gerty_id}/${i}`
|
||||||
|
)
|
||||||
|
this.gerty[i] = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(this.gerty)
|
||||||
|
for (let i = 0; i < this.gerty.length; i++) {
|
||||||
|
if (this.gerty[i].screen.group == 'lnbits_wallets_balance') {
|
||||||
|
for (let q = 0; q < this.gerty[i].screen.areas.length; q++) {
|
||||||
|
this.lnbits_wallets_balance[q] = {
|
||||||
|
name: this.gerty[i].screen.areas[q][0].value,
|
||||||
|
amount: this.gerty[i].screen.areas[q][1].value,
|
||||||
|
color1: this.walletColors[q].first,
|
||||||
|
color2: this.walletColors[q].second
|
||||||
|
}
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'url_checker') {
|
||||||
|
this.url_checker = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'dashboard_onchain') {
|
||||||
|
this.dashboard_onchain = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'dashboard_mining') {
|
||||||
|
this.dashboard_mining = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'lightning_dashboard') {
|
||||||
|
this.lightning_dashboard = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'fun_satoshi_quotes') {
|
||||||
|
this.fun_satoshi_quotes['quote'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][0].value
|
||||||
|
this.fun_satoshi_quotes['date'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][1].value
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'fun_exchange_market_rate') {
|
||||||
|
this.fun_exchange_market_rate['unit'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][0].value
|
||||||
|
this.fun_exchange_market_rate['amount'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][1].value
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(this.getGertyInfo, 20000)
|
||||||
|
this.$forceUpdate()
|
||||||
|
return this.gerty
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getGertyInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
798
lnbits/extensions/gerty/templates/gerty/index.html
Normal file
798
lnbits/extensions/gerty/templates/gerty/index.html
Normal file
|
|
@ -0,0 +1,798 @@
|
||||||
|
{% 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 Gerty
|
||||||
|
</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">Gerty</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="gertys"
|
||||||
|
row-key="id"
|
||||||
|
:columns="gertysTable.columns"
|
||||||
|
:pagination.sync="gertysTable.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"
|
||||||
|
:class="`col__${col.name} text-truncate elipsis`"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
<q-th auto-width></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="sentiment_satisfied"
|
||||||
|
color="green"
|
||||||
|
type="a"
|
||||||
|
:href="props.row.gerty"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<q-tooltip>Launch software Gerty</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="code"
|
||||||
|
color="pink"
|
||||||
|
type="a"
|
||||||
|
:href="props.row.gertyJson"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<q-tooltip>View Gerty API</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ (col.name == 'tip_options' && col.value ?
|
||||||
|
JSON.parse(col.value).join(", ") : col.value) }}
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="updateformDialog(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteGerty(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
<h6 class="text-subtitle1 q-my-none">
|
||||||
|
{{ SITE_TITLE }} Gerty extension
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
label="Swagger API"
|
||||||
|
type="a"
|
||||||
|
href="../docs#/gerty"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list> {% include "gerty/_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="sendFormDataGerty" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Son of Gerty"
|
||||||
|
></q-input>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.fun_satoshi_quotes"
|
||||||
|
val="xs"
|
||||||
|
label="Satoshi Quotes"
|
||||||
|
><q-tooltip
|
||||||
|
>Displays random quotes from Satoshi</q-tooltip
|
||||||
|
></q-checkbox
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.fun_exchange_market_rate"
|
||||||
|
val="xs"
|
||||||
|
label="Fiat to BTC price"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.lnbits_wallets_balance"
|
||||||
|
val="xs"
|
||||||
|
label="LNbits"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.dashboard_onchain"
|
||||||
|
val="xs"
|
||||||
|
label="Onchain"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.dashboard_mining"
|
||||||
|
val="xs"
|
||||||
|
label="Mining"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.lightning_dashboard"
|
||||||
|
val="xs"
|
||||||
|
label="Lightning"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.url_checker"
|
||||||
|
val="xs"
|
||||||
|
label="URL Checker"
|
||||||
|
></q-checkbox>
|
||||||
|
<br />
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.data.display_preferences.fun_exchange_market_rate"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.exchange"
|
||||||
|
:options="currencyOptions"
|
||||||
|
label="Exchange rate"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.data.display_preferences.lnbits_wallets_balance"
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.lnbits_wallets"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
label="Invoice keys of wallets to watch"
|
||||||
|
>
|
||||||
|
<q-tooltip>Hit enter to add values</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.data.display_preferences.url_checker"
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.urls"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
label="Urls to watch."
|
||||||
|
>
|
||||||
|
<q-tooltip>Hit enter to add values</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-toggle
|
||||||
|
label="*Advanced"
|
||||||
|
v-model="toggleStates.advanced"
|
||||||
|
@input="setAdvanced"
|
||||||
|
></q-toggle>
|
||||||
|
<br />
|
||||||
|
<q-input
|
||||||
|
v-if="toggleStates.advanced"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.mempool_endpoint"
|
||||||
|
label="Mempool link"
|
||||||
|
class="q-pb-sm"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="toggleStates.advanced"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.refresh_time"
|
||||||
|
label="Refresh time in seconds"
|
||||||
|
class="q-pb-md"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
>The amount of time in seconds between screen updates
|
||||||
|
</q-tooltip>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="formDialog.data.name == null"
|
||||||
|
type="submit"
|
||||||
|
class="q-mr-md"
|
||||||
|
v-if="!formDialog.data.id"
|
||||||
|
>Create Gerty
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="formDialog.data.name == null"
|
||||||
|
type="submit"
|
||||||
|
>Update Gerty
|
||||||
|
</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 mapGerty = function (obj) {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||||
|
obj.gerty = ['/gerty/', obj.id].join('')
|
||||||
|
obj.gertyJson = ['/gerty/api/v1/gerty/pages/', obj.id, '/0'].join('')
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
toggleStates: {
|
||||||
|
fun: false,
|
||||||
|
onchain: false,
|
||||||
|
mempool: false,
|
||||||
|
mining: false,
|
||||||
|
lightning: false,
|
||||||
|
advanced: false
|
||||||
|
},
|
||||||
|
oldToggleStates: {},
|
||||||
|
gertys: [],
|
||||||
|
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'
|
||||||
|
],
|
||||||
|
gertysTable: {
|
||||||
|
columns: [
|
||||||
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
|
{
|
||||||
|
name: 'exchange',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Exchange',
|
||||||
|
field: 'exchange'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mempool_endpoint',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Mempool Endpoint',
|
||||||
|
field: 'mempool_endpoint'
|
||||||
|
},
|
||||||
|
{name: 'id', align: 'left', label: 'Gerty ID', field: 'id'}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
type: 'Mini Gerty',
|
||||||
|
exchange: 'USD',
|
||||||
|
utc_offset: new Date().getTimezoneOffset(),
|
||||||
|
display_preferences: {
|
||||||
|
dashboard: false,
|
||||||
|
fun_satoshi_quotes: false,
|
||||||
|
fun_exchange_market_rate: false,
|
||||||
|
dashboard_onchain: false,
|
||||||
|
mempool_recommended_fees: false,
|
||||||
|
dashboard_mining: false,
|
||||||
|
lightning_dashboard: false,
|
||||||
|
onchain: false,
|
||||||
|
onchain_difficulty_epoch_progress: false,
|
||||||
|
onchain_difficulty_retarget_date: false,
|
||||||
|
onchain_difficulty_blocks_remaining: false,
|
||||||
|
onchain_difficulty_epoch_time_remaining: false,
|
||||||
|
onchain_block_height: false,
|
||||||
|
mempool_tx_count: false,
|
||||||
|
mining_current_hash_rate: false,
|
||||||
|
mining_current_difficulty: false,
|
||||||
|
lnbits_wallets_balance: false,
|
||||||
|
url_checker: false
|
||||||
|
},
|
||||||
|
lnbits_wallets: [],
|
||||||
|
urls: [],
|
||||||
|
mempool_endpoint: 'https://mempool.space',
|
||||||
|
refresh_time: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setAdvanced: function () {
|
||||||
|
self = this
|
||||||
|
self.formDialog.data.mempool_endpoint = 'https://mempool.space'
|
||||||
|
self.formDialog.data.refresh_time = 300
|
||||||
|
},
|
||||||
|
setWallets: function () {
|
||||||
|
self = this
|
||||||
|
if (!self.formDialog.data.display_preferences.lnbits_wallets_balance) {
|
||||||
|
self.formDialog.data.lnbits_wallets = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setUrls: function () {
|
||||||
|
self = this
|
||||||
|
if (!self.formDialog.data.display_preferences.url_checker) {
|
||||||
|
self.formDialog.data.urls = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setOnchain: function () {
|
||||||
|
self = this
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_epoch_progress =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_retarget_date =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_blocks_remaining = !self
|
||||||
|
.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_epoch_time_remaining =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_block_height =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
},
|
||||||
|
setMining: function () {
|
||||||
|
self = this
|
||||||
|
self.formDialog.data.display_preferences.mining_current_hash_rate =
|
||||||
|
self.toggleStates.mining
|
||||||
|
self.formDialog.data.display_preferences.mining_current_difficulty =
|
||||||
|
self.toggleStates.mining
|
||||||
|
},
|
||||||
|
closeFormDialog: function () {
|
||||||
|
this.formDialog.data = {
|
||||||
|
utc_offset: 0,
|
||||||
|
lnbits_wallets: [],
|
||||||
|
urls: [],
|
||||||
|
mempool_endpoint: 'https://mempool.space',
|
||||||
|
refresh_time: 300,
|
||||||
|
display_preferences: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getGertys: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/gerty/api/v1/gerty?all_wallets=true',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.gertys = response.data.map(function (obj) {
|
||||||
|
return mapGerty(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateformDialog: function (formId) {
|
||||||
|
var gerty = _.findWhere(this.gertys, {id: formId})
|
||||||
|
this.formDialog.data.id = gerty.id
|
||||||
|
this.formDialog.data.name = gerty.name
|
||||||
|
this.formDialog.data.type = gerty.type
|
||||||
|
this.formDialog.data.utc_offset = gerty.utc_offset
|
||||||
|
this.formDialog.data.lnbits_wallets = JSON.parse(gerty.lnbits_wallets)
|
||||||
|
this.formDialog.data.urls = JSON.parse(gerty.urls)
|
||||||
|
;(this.formDialog.data.exchange = gerty.exchange),
|
||||||
|
(this.formDialog.data.mempool_endpoint = gerty.mempool_endpoint),
|
||||||
|
(this.formDialog.data.refresh_time = gerty.refresh_time),
|
||||||
|
(this.formDialog.data.display_preferences = JSON.parse(
|
||||||
|
gerty.display_preferences
|
||||||
|
)),
|
||||||
|
(this.formDialog.show = true)
|
||||||
|
},
|
||||||
|
sendFormDataGerty: function () {
|
||||||
|
if (this.formDialog.data.id) {
|
||||||
|
this.updateGerty(
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
this.formDialog.data
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.createGerty(
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
this.formDialog.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createGerty: function () {
|
||||||
|
if (
|
||||||
|
this.formDialog.data.display_preferences.dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.lightning_dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.url_checker
|
||||||
|
) {
|
||||||
|
this.formDialog.data.type = 'Gerty'
|
||||||
|
}
|
||||||
|
var data = {
|
||||||
|
name: this.formDialog.data.name,
|
||||||
|
utc_offset: this.formDialog.data.utc_offset,
|
||||||
|
type: this.formDialog.data.type,
|
||||||
|
lnbits_wallets: JSON.stringify(this.formDialog.data.lnbits_wallets),
|
||||||
|
urls: JSON.stringify(this.formDialog.data.urls),
|
||||||
|
exchange: this.formDialog.data.exchange,
|
||||||
|
mempool_endpoint: this.formDialog.data.mempool_endpoint,
|
||||||
|
refresh_time: this.formDialog.data.refresh_time,
|
||||||
|
display_preferences: JSON.stringify(
|
||||||
|
this.formDialog.data.display_preferences
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/gerty/api/v1/gerty',
|
||||||
|
this.g.user.wallets[0].inkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.formDialog.show = false
|
||||||
|
self.gertys.push(mapGerty(response.data))
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateGerty: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
if (
|
||||||
|
this.formDialog.data.display_preferences.dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.lightning_dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.url_checker
|
||||||
|
) {
|
||||||
|
this.formDialog.data.type = 'Gerty'
|
||||||
|
}
|
||||||
|
data.utc_offset = this.formDialog.data.utc_offset
|
||||||
|
data.type = this.formDialog.data.type
|
||||||
|
data.lnbits_wallets = JSON.stringify(
|
||||||
|
this.formDialog.data.lnbits_wallets
|
||||||
|
)
|
||||||
|
data.urls = JSON.stringify(this.formDialog.data.urls)
|
||||||
|
data.display_preferences = JSON.stringify(
|
||||||
|
this.formDialog.data.display_preferences
|
||||||
|
)
|
||||||
|
LNbits.api
|
||||||
|
.request('PUT', '/gerty/api/v1/gerty/' + data.id, wallet, data)
|
||||||
|
.then(function (response) {
|
||||||
|
self.gertys = _.reject(self.gertys, function (obj) {
|
||||||
|
return obj.id == data.id
|
||||||
|
})
|
||||||
|
self.formDialog.show = false
|
||||||
|
self.gertys.push(mapGerty(response.data))
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteGerty: function (gertyId) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
var gerty = _.findWhere(self.gertys, {id: gertyId})
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this Gerty?')
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/gerty/api/v1/gerty/' + gertyId,
|
||||||
|
_.findWhere(self.g.user.wallets, {id: gerty.wallet}).adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.gertys = _.reject(self.gertys, function (obj) {
|
||||||
|
return obj.id == gertyId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportCSV: function () {
|
||||||
|
LNbits.utils.exportCSV(this.gertysTable.columns, this.gertys)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMiniGerty() {
|
||||||
|
return this.formDialog.data.type == 'Mini Gerty'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
if (this.g.user.wallets.length) {
|
||||||
|
this.getGertys()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'formDialog.data.type': {
|
||||||
|
handler(value) {
|
||||||
|
if (value == 'Mini Gerty') {
|
||||||
|
this.formDialog.data.display_preferences.dashboard = false
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain = false
|
||||||
|
this.formDialog.data.display_preferences.dashboard_mining = false
|
||||||
|
this.formDialog.data.display_preferences.lightning_dashboard = false
|
||||||
|
this.formDialog.data.display_preferences.fun_satoshi_quotes = false
|
||||||
|
this.formDialog.data.display_preferences.mempool_recommended_fees = false
|
||||||
|
this.formDialog.data.display_preferences.onchain = false
|
||||||
|
this.formDialog.data.display_preferences.url_checker = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStates: {
|
||||||
|
handler(toggleStatesValue) {
|
||||||
|
// Switch all the toggles in each section to the relevant state
|
||||||
|
for (const [toggleKey, toggleValue] of Object.entries(
|
||||||
|
toggleStatesValue
|
||||||
|
)) {
|
||||||
|
if (this.oldToggleStates[toggleKey] !== toggleValue) {
|
||||||
|
for (const [dpKey, dpValue] of Object.entries(
|
||||||
|
this.formDialog.data.display_preferences
|
||||||
|
)) {
|
||||||
|
if (dpKey.indexOf(toggleKey) === 0) {
|
||||||
|
this.formDialog.data.display_preferences[dpKey] = toggleValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is a weird hack we have to use to get VueJS to persist the previous toggle state between
|
||||||
|
// watches. VueJS passes the old and new values by reference so when comparing objects they
|
||||||
|
// will have the same values unless we do this
|
||||||
|
this.oldToggleStates = JSON.parse(JSON.stringify(toggleStatesValue))
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %} {% block styles %}
|
||||||
|
<style>
|
||||||
|
.col__display_preferences {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
37
lnbits/extensions/gerty/views.py
Normal file
37
lnbits/extensions/gerty/views.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import json
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from loguru import logger
|
||||||
|
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 gerty_ext, gerty_renderer
|
||||||
|
from .crud import get_gerty
|
||||||
|
from .views_api import api_gerty_json
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
|
return gerty_renderer().TemplateResponse(
|
||||||
|
"gerty/index.html", {"request": request, "user": user.dict()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/{gerty_id}", response_class=HTMLResponse)
|
||||||
|
async def display(request: Request, gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
|
)
|
||||||
|
return gerty_renderer().TemplateResponse(
|
||||||
|
"gerty/gerty.html", {"request": request, "gerty": gerty_id}
|
||||||
|
)
|
||||||
191
lnbits/extensions/gerty/views_api.py
Normal file
191
lnbits/extensions/gerty/views_api.py
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import Query
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from lnurl import decode as decode_lnurl
|
||||||
|
from loguru import logger
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||||
|
from lnbits.core.services import create_invoice
|
||||||
|
from lnbits.core.views.api import api_payment, api_wallet
|
||||||
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
||||||
|
|
||||||
|
from . import gerty_ext
|
||||||
|
from .crud import (
|
||||||
|
create_gerty,
|
||||||
|
delete_gerty,
|
||||||
|
get_gerty,
|
||||||
|
get_gertys,
|
||||||
|
get_mempool_info,
|
||||||
|
update_gerty,
|
||||||
|
)
|
||||||
|
from .helpers import *
|
||||||
|
from .models import Gerty, MempoolEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty", status_code=HTTPStatus.OK)
|
||||||
|
async def api_gertys(
|
||||||
|
all_wallets: bool = Query(False), 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 [gerty.dict() for gerty in await get_gertys(wallet_ids)]
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.post("/api/v1/gerty", status_code=HTTPStatus.CREATED)
|
||||||
|
@gerty_ext.put("/api/v1/gerty/{gerty_id}", status_code=HTTPStatus.OK)
|
||||||
|
async def api_link_create_or_update(
|
||||||
|
data: Gerty,
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
|
gerty_id: str = Query(None),
|
||||||
|
):
|
||||||
|
logger.debug(data)
|
||||||
|
if gerty_id:
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
if gerty.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Come on, seriously, this isn't your Gerty!",
|
||||||
|
)
|
||||||
|
|
||||||
|
data.wallet = wallet.wallet.id
|
||||||
|
gerty = await update_gerty(gerty_id, **data.dict())
|
||||||
|
else:
|
||||||
|
gerty = await create_gerty(wallet_id=wallet.wallet.id, data=data)
|
||||||
|
|
||||||
|
return {**gerty.dict()}
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.delete("/api/v1/gerty/{gerty_id}")
|
||||||
|
async def api_gerty_delete(
|
||||||
|
gerty_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
if gerty.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Gerty.")
|
||||||
|
|
||||||
|
await delete_gerty(gerty_id)
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK)
|
||||||
|
async def api_gerty_satoshi():
|
||||||
|
return await get_satoshi
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/pages/{gerty_id}/{p}")
|
||||||
|
async def api_gerty_json(gerty_id: str, p: int = None): # page number
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
display_preferences = json.loads(gerty.display_preferences)
|
||||||
|
|
||||||
|
enabled_screen_count = 0
|
||||||
|
|
||||||
|
enabled_screens = []
|
||||||
|
|
||||||
|
for screen_slug in display_preferences:
|
||||||
|
is_screen_enabled = display_preferences[screen_slug]
|
||||||
|
if is_screen_enabled:
|
||||||
|
enabled_screen_count += 1
|
||||||
|
enabled_screens.append(screen_slug)
|
||||||
|
|
||||||
|
logger.debug("Screeens " + str(enabled_screens))
|
||||||
|
data = await get_screen_data(p, enabled_screens, gerty)
|
||||||
|
|
||||||
|
next_screen_number = 0 if ((p + 1) >= enabled_screen_count) else p + 1
|
||||||
|
|
||||||
|
# get the sleep time
|
||||||
|
sleep_time = gerty.refresh_time if gerty.refresh_time else 300
|
||||||
|
utc_offset = gerty.utc_offset if gerty.utc_offset else 0
|
||||||
|
if gerty_should_sleep(utc_offset):
|
||||||
|
sleep_time_hours = 8
|
||||||
|
sleep_time = 60 * 60 * sleep_time_hours
|
||||||
|
|
||||||
|
return {
|
||||||
|
"settings": {
|
||||||
|
"refreshTime": sleep_time,
|
||||||
|
"requestTimestamp": get_next_update_time(sleep_time, utc_offset),
|
||||||
|
"nextScreenNumber": next_screen_number,
|
||||||
|
"showTextBoundRect": False,
|
||||||
|
"name": gerty.name,
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"slug": get_screen_slug_by_index(p, enabled_screens),
|
||||||
|
"group": get_screen_slug_by_index(p, enabled_screens),
|
||||||
|
"title": data["title"],
|
||||||
|
"areas": data["areas"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
###########CACHED MEMPOOL##############
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/fees-recommended/{gerty_id}")
|
||||||
|
async def api_gerty_get_fees_recommended(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("fees_recommended", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/hashrate-1w/{gerty_id}")
|
||||||
|
async def api_gerty_get_hashrate_1w(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("hashrate_1w", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/hashrate-1m/{gerty_id}")
|
||||||
|
async def api_gerty_get_hashrate_1m(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/statistics/{gerty_id}")
|
||||||
|
async def api_gerty_get_statistics(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("statistics", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/difficulty-adjustment/{gerty_id}")
|
||||||
|
async def api_gerty_get_difficulty_adjustment(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/tip-height/{gerty_id}")
|
||||||
|
async def api_gerty_get_tip_height(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("tip_height", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/mempool/{gerty_id}")
|
||||||
|
async def api_gerty_get_mempool(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("mempool", gerty)
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<h1>Hivemind</h1>
|
<h1>Hivemind</h1>
|
||||||
|
|
||||||
Placeholder for a future <a href="https://bitcoinhivemind.com/">Bitcoin Hivemind</a> extension.
|
Placeholder for a future <a class="text-secondary" href="https://bitcoinhivemind.com/">Bitcoin Hivemind</a> extension.
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@
|
||||||
This extension is just a placeholder for now.
|
This extension is just a placeholder for now.
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://bitcoinhivemind.com/">Hivemind</a> is a Bitcoin sidechain
|
<a class="text-secondary" href="https://bitcoinhivemind.com/">Hivemind</a>
|
||||||
project for a peer-to-peer oracle protocol that absorbs accurate data into
|
is a Bitcoin sidechain project for a peer-to-peer oracle protocol that
|
||||||
a blockchain so that Bitcoin users can speculate in prediction markets.
|
absorbs accurate data into a blockchain so that Bitcoin users can
|
||||||
|
speculate in prediction markets.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
These markets have the potential to revolutionize the emergence of
|
These markets have the potential to revolutionize the emergence of
|
||||||
|
|
@ -17,8 +18,8 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This extension will become fully operative when the
|
This extension will become fully operative when the
|
||||||
<a href="https://drivechain.xyz/">BIP300</a> soft-fork gets activated and
|
<a class="text-secondary" href="https://drivechain.xyz/">BIP300</a>
|
||||||
Bitcoin Hivemind is launched.
|
soft-fork gets activated and Bitcoin Hivemind is launched.
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -251,10 +251,13 @@ block page %}
|
||||||
@hide="closeQrCodeDialog"
|
@hide="closeQrCodeDialog"
|
||||||
>
|
>
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card text-center">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card text-center">
|
||||||
<a :href="'lightning:' + qrCodeDialog.data.payment_request">
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
:href="'lightning:' + qrCodeDialog.data.payment_request"
|
||||||
|
>
|
||||||
<q-responsive :ratio="1" class="q-mx-xs">
|
<q-responsive :ratio="1" class="q-mx-xs">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="qrCodeDialog.data.payment_request"
|
:value="'lightning:' + qrCodeDialog.data.payment_request.toUpperCase()"
|
||||||
:options="{width: 400}"
|
:options="{width: 400}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
|
|
||||||
|
|
||||||
@invoices_ext.get("/pay/{invoice_id}", response_class=HTMLResponse)
|
@invoices_ext.get("/pay/{invoice_id}", response_class=HTMLResponse)
|
||||||
async def index(request: Request, invoice_id: str):
|
async def pay(request: Request, invoice_id: str):
|
||||||
invoice = await get_invoice(invoice_id)
|
invoice = await get_invoice(invoice_id)
|
||||||
|
|
||||||
if not invoice:
|
if not invoice:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
To use this extension you need a Spotify client ID and client secret. You get
|
To use this extension you need a Spotify client ID and client secret. You get
|
||||||
these by creating an app in the Spotify developers dashboard
|
these by creating an app in the Spotify developers dashboard
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
style="color: #43a047"
|
style="color: #43a047"
|
||||||
href="https://developer.spotify.com/dashboard/applications"
|
href="https://developer.spotify.com/dashboard/applications"
|
||||||
>here
|
>here
|
||||||
|
|
@ -9,9 +10,14 @@
|
||||||
<br /><br />Select the playlists you want people to be able to pay for, share
|
<br /><br />Select the playlists you want people to be able to pay for, share
|
||||||
the frontend page, profit :) <br /><br />
|
the frontend page, profit :) <br /><br />
|
||||||
Made by,
|
Made by,
|
||||||
<a style="color: #43a047" href="https://twitter.com/arcbtc">benarc</a>.
|
|
||||||
Inspired by,
|
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
style="color: #43a047"
|
||||||
|
href="https://twitter.com/arcbtc"
|
||||||
|
>benarc</a
|
||||||
|
>. Inspired by,
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
style="color: #43a047"
|
style="color: #43a047"
|
||||||
href="https://twitter.com/pirosb3/status/1056263089128161280"
|
href="https://twitter.com/pirosb3/status/1056263089128161280"
|
||||||
>pirosb3</a
|
>pirosb3</a
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="'lightning:' + receive.paymentReq"
|
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@
|
||||||
<q-td auto-width>{{ props.row.name }}</q-td>
|
<q-td auto-width>{{ props.row.name }}</q-td>
|
||||||
<q-td class="text-center" auto-width>
|
<q-td class="text-center" auto-width>
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:href="'/wallet?usr=' + props.row.user + '&wal=' + props.row.wallet"
|
:href="'/wallet?usr=' + props.row.user + '&wal=' + props.row.wallet"
|
||||||
>
|
>
|
||||||
|
|
@ -191,7 +192,7 @@
|
||||||
</q-select>
|
</q-select>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
|
||||||
<a :href="'lightning:' + livestream.lnurl">
|
<a class="text-secondary" :href="'lightning:' + livestream.lnurl">
|
||||||
<q-responsive :ratio="1" class="q-mx-sm">
|
<q-responsive :ratio="1" class="q-mx-sm">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="livestream.lnurl"
|
:value="livestream.lnurl"
|
||||||
|
|
@ -235,10 +236,10 @@
|
||||||
<p class="text-subtitle1 q-my-none">
|
<p class="text-subtitle1 q-my-none">
|
||||||
Standalone QR Code for this track
|
Standalone QR Code for this track
|
||||||
</p>
|
</p>
|
||||||
<a :href="'lightning:' + trackDialog.data.lnurl">
|
<a class="text-secondary" :href="'lightning:' + trackDialog.data.lnurl">
|
||||||
<q-responsive :ratio="1" class="q-mx-sm">
|
<q-responsive :ratio="1" class="q-mx-sm">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="trackDialog.data.lnurl"
|
:value="'lightning:' + trackDialog.data.lnurl.toUpperCase()"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,16 @@
|
||||||
Charge people for using your domain name...<br />
|
Charge people for using your domain name...<br />
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/lnaddress"
|
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/lnaddress"
|
||||||
>More details</a
|
>More details</a
|
||||||
>
|
>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
Created by,
|
Created by,
|
||||||
<a href="https://twitter.com/talvasconcelos">talvasconcelos</a></small
|
<a class="text-secondary" href="https://twitter.com/talvasconcelos"
|
||||||
|
>talvasconcelos</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -184,10 +184,10 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="paymentReq"
|
:value="'lightning:' + paymentReq.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,7 @@
|
||||||
<template v-slot:hint>
|
<template v-slot:hint>
|
||||||
Check extension
|
Check extension
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://github.com/lnbits/lnbits-legend/blob/main/lnbits/extensions/lnaddress/README.md"
|
href="https://github.com/lnbits/lnbits-legend/blob/main/lnbits/extensions/lnaddress/README.md"
|
||||||
>documentation!</a
|
>documentation!</a
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@
|
||||||
To access an LNbits wallet from a mobile phone,
|
To access an LNbits wallet from a mobile phone,
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
Install either <a href="https://zeusln.app">Zeus</a> or
|
Install either
|
||||||
<a href="https://bluewallet.io/">BlueWallet</a>;
|
<a class="text-secondary" href="https://zeusln.app">Zeus</a> or
|
||||||
|
<a class="text-secondary" href="https://bluewallet.io/">BlueWallet</a
|
||||||
|
>;
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Go to <code>Add a wallet / Import wallet</code> on BlueWallet or
|
Go to <code>Add a wallet / Import wallet</code> on BlueWallet or
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<p>
|
<p>
|
||||||
LndHub is a protocol invented by
|
LndHub is a protocol invented by
|
||||||
<a href="https://bluewallet.io/">BlueWallet</a> that allows mobile
|
<a class="text-secondary" href="https://bluewallet.io/">BlueWallet</a>
|
||||||
wallets to query payments and balances, generate invoices and make
|
that allows mobile wallets to query payments and balances, generate
|
||||||
payments from accounts that exist on a server. The protocol is a
|
invoices and make payments from accounts that exist on a server. The
|
||||||
collection of HTTP endpoints exposed through the internet.
|
protocol is a collection of HTTP endpoints exposed through the internet.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For a wallet that supports it, reading a QR code that contains the URL
|
For a wallet that supports it, reading a QR code that contains the URL
|
||||||
along with secret access credentials should enable access. Currently it
|
along with secret access credentials should enable access. Currently it
|
||||||
is supported by <a href="https://zeusln.app">Zeus</a> and
|
is supported by
|
||||||
<a href="https://bluewallet.io/">BlueWallet</a>.
|
<a class="text-secondary" href="https://zeusln.app">Zeus</a> and
|
||||||
|
<a class="text-secondary" href="https://bluewallet.io/">BlueWallet</a>.
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
>
|
>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a :href="selectedWallet[type]">
|
<a class="text-secondary" :href="selectedWallet[type]">
|
||||||
<q-responsive :ratio="1" class="q-mx-sm">
|
<q-responsive :ratio="1" class="q-mx-sm">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="selectedWallet[type]"
|
:value="selectedWallet[type]"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
from binascii import unhexlify
|
|
||||||
|
|
||||||
from lnbits.bolt11 import Invoice
|
from lnbits.bolt11 import Invoice
|
||||||
|
|
||||||
|
|
||||||
def to_buffer(payment_hash: str):
|
def to_buffer(payment_hash: str):
|
||||||
return {"type": "Buffer", "data": [b for b in unhexlify(payment_hash)]}
|
return {"type": "Buffer", "data": [b for b in bytes.fromhex(payment_hash)]}
|
||||||
|
|
||||||
|
|
||||||
def decoded_as_lndhub(invoice: Invoice):
|
def decoded_as_lndhub(invoice: Invoice):
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@
|
||||||
paid support ticketing, PAYG language services, contact spam
|
paid support ticketing, PAYG language services, contact spam
|
||||||
protection.<br />
|
protection.<br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="paymentReq"
|
:value="'lightning:' + paymentReq.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices
|
||||||
async def create_lnurldevice(
|
async def create_lnurldevice(
|
||||||
data: createLnurldevice,
|
data: createLnurldevice,
|
||||||
) -> lnurldevices:
|
) -> lnurldevices:
|
||||||
lnurldevice_id = urlsafe_short_hash()
|
if data.device == "pos" or data.device == "atm":
|
||||||
|
lnurldevice_id = str(await get_lnurldeviceposcount())
|
||||||
|
else:
|
||||||
|
lnurldevice_id = urlsafe_short_hash()
|
||||||
lnurldevice_key = urlsafe_short_hash()
|
lnurldevice_key = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -79,6 +82,17 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev
|
||||||
return lnurldevices(**row) if row else None
|
return lnurldevices(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lnurldeviceposcount() -> int:
|
||||||
|
row = await db.fetchall(
|
||||||
|
"SELECT * FROM lnurldevice.lnurldevices WHERE device = ? OR device = ?",
|
||||||
|
(
|
||||||
|
"pos",
|
||||||
|
"atm",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return len(row) + 1
|
||||||
|
|
||||||
|
|
||||||
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
|
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ class createLnurldevice(BaseModel):
|
||||||
wallet: str
|
wallet: str
|
||||||
currency: str
|
currency: str
|
||||||
device: str
|
device: str
|
||||||
profit: float
|
profit: float = 0
|
||||||
amount: int
|
amount: Optional[int] = 0
|
||||||
pin: int = 0
|
pin: int = 0
|
||||||
profit1: float = 0
|
profit1: float = 0
|
||||||
amount1: int = 0
|
amount1: int = 0
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,25 @@
|
||||||
For LNURL based Points of Sale, ATMs, and relay devices<br />
|
For LNURL based Points of Sale, ATMs, and relay devices<br />
|
||||||
Use with: <br />
|
Use with: <br />
|
||||||
LNPoS
|
LNPoS
|
||||||
<a href="https://lnbits.github.io/lnpos">
|
<a class="text-secondary" href="https://lnbits.github.io/lnpos">
|
||||||
https://lnbits.github.io/lnpos</a
|
https://lnbits.github.io/lnpos</a
|
||||||
><br />
|
><br />
|
||||||
bitcoinSwitch
|
bitcoinSwitch
|
||||||
<a href="https://github.com/lnbits/bitcoinSwitch">
|
<a class="text-secondary" href="https://github.com/lnbits/bitcoinSwitch">
|
||||||
https://github.com/lnbits/bitcoinSwitch</a
|
https://github.com/lnbits/bitcoinSwitch</a
|
||||||
><br />
|
><br />
|
||||||
FOSSA
|
FOSSA
|
||||||
<a href="https://github.com/lnbits/fossa">
|
<a class="text-secondary" href="https://github.com/lnbits/fossa">
|
||||||
https://github.com/lnbits/fossa</a
|
https://github.com/lnbits/fossa</a
|
||||||
><br />
|
><br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>,
|
Created by,
|
||||||
<a href="https://github.com/blackcoffeexbt">BC</a>,
|
<a class="text-secondary" href="https://github.com/benarc">Ben Arc</a>,
|
||||||
<a href="https://github.com/motorina0">Vlad Stan</a></small
|
<a class="text-secondary" href="https://github.com/blackcoffeexbt">BC</a
|
||||||
|
>,
|
||||||
|
<a class="text-secondary" href="https://github.com/motorina0"
|
||||||
|
>Vlad Stan</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -476,7 +476,7 @@
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="lnurlValue"
|
:value="'lightning:' + lnurlValue"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from lnbits.db import SQLITE
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import CreatePayLinkData, PayLink
|
from .models import CreatePayLinkData, PayLink
|
||||||
|
|
||||||
|
|
||||||
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||||
|
link_id = urlsafe_short_hash()
|
||||||
|
|
||||||
returning = "" if db.type == SQLITE else "RETURNING ID"
|
result = await db.execute(
|
||||||
method = db.execute if db.type == SQLITE else db.fetchone
|
|
||||||
|
|
||||||
result = await (method)(
|
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO lnurlp.pay_links (
|
INSERT INTO lnurlp.pay_links (
|
||||||
|
id,
|
||||||
wallet,
|
wallet,
|
||||||
description,
|
description,
|
||||||
min,
|
min,
|
||||||
|
|
@ -29,10 +28,11 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||||
currency,
|
currency,
|
||||||
fiat_base_multiplier
|
fiat_base_multiplier
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
{returning}
|
{returning}
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
link_id,
|
||||||
wallet_id,
|
wallet_id,
|
||||||
data.description,
|
data.description,
|
||||||
data.min,
|
data.min,
|
||||||
|
|
@ -47,10 +47,6 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||||
data.fiat_base_multiplier,
|
data.fiat_base_multiplier,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if db.type == SQLITE:
|
|
||||||
link_id = result._result_proxy.lastrowid
|
|
||||||
else:
|
|
||||||
link_id = result[0]
|
|
||||||
|
|
||||||
link = await get_pay_link(link_id)
|
link = await get_pay_link(link_id)
|
||||||
assert link, "Newly created link couldn't be retrieved"
|
assert link, "Newly created link couldn't be retrieved"
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,76 @@ async def m005_webhook_headers_and_body(db):
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
|
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
|
||||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
|
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
|
||||||
|
|
||||||
|
|
||||||
|
async def m006_redux(db):
|
||||||
|
"""
|
||||||
|
Add UUID ID's to links and migrates existing data
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE lnurlp.pay_links (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
min INTEGER NOT NULL,
|
||||||
|
max INTEGER,
|
||||||
|
currency TEXT,
|
||||||
|
fiat_base_multiplier INTEGER DEFAULT 1,
|
||||||
|
served_meta INTEGER NOT NULL,
|
||||||
|
served_pr INTEGER NOT NULL,
|
||||||
|
webhook_url TEXT,
|
||||||
|
success_text TEXT,
|
||||||
|
success_url TEXT,
|
||||||
|
comment_chars INTEGER DEFAULT 0,
|
||||||
|
webhook_headers TEXT,
|
||||||
|
webhook_body TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in [
|
||||||
|
list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
|
||||||
|
]:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO lnurlp.pay_links (
|
||||||
|
id,
|
||||||
|
wallet,
|
||||||
|
description,
|
||||||
|
min,
|
||||||
|
served_meta,
|
||||||
|
served_pr,
|
||||||
|
webhook_url,
|
||||||
|
success_text,
|
||||||
|
success_url,
|
||||||
|
currency,
|
||||||
|
comment_chars,
|
||||||
|
max,
|
||||||
|
fiat_base_multiplier,
|
||||||
|
webhook_headers,
|
||||||
|
webhook_body
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
row[0],
|
||||||
|
row[1],
|
||||||
|
row[2],
|
||||||
|
row[3],
|
||||||
|
row[4],
|
||||||
|
row[5],
|
||||||
|
row[6],
|
||||||
|
row[7],
|
||||||
|
row[8],
|
||||||
|
row[9],
|
||||||
|
row[10],
|
||||||
|
row[11],
|
||||||
|
row[12],
|
||||||
|
row[13],
|
||||||
|
row[14],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute("DROP TABLE lnurlp.pay_links_old")
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class CreatePayLinkData(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class PayLink(BaseModel):
|
class PayLink(BaseModel):
|
||||||
id: int
|
id: str
|
||||||
wallet: str
|
wallet: str
|
||||||
description: str
|
description: str
|
||||||
min: float
|
min: float
|
||||||
|
|
|
||||||
|
|
@ -52,19 +52,29 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
|
||||||
r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs)
|
r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs)
|
||||||
await mark_webhook_sent(
|
await mark_webhook_sent(
|
||||||
payment, r.status_code, r.is_success, r.reason_phrase, r.text
|
payment.payment_hash,
|
||||||
|
r.status_code,
|
||||||
|
r.is_success,
|
||||||
|
r.reason_phrase,
|
||||||
|
r.text,
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(ex)
|
logger.error(ex)
|
||||||
await mark_webhook_sent(payment, -1, False, "Unexpected Error", str(ex))
|
await mark_webhook_sent(
|
||||||
|
payment.payment_hash, -1, False, "Unexpected Error", str(ex)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(
|
async def mark_webhook_sent(
|
||||||
payment: Payment, status: int, is_success: bool, reason_phrase="", text=""
|
payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
|
||||||
) -> None:
|
) -> None:
|
||||||
payment.extra["wh_status"] = status # keep for backwards compability
|
|
||||||
payment.extra["wh_success"] = is_success
|
|
||||||
payment.extra["wh_message"] = reason_phrase
|
|
||||||
payment.extra["wh_response"] = text
|
|
||||||
|
|
||||||
await update_payment_extra(payment.payment_hash, payment.extra)
|
await update_payment_extra(
|
||||||
|
payment_hash,
|
||||||
|
{
|
||||||
|
"wh_status": status, # keep for backwards compability
|
||||||
|
"wh_success": is_success,
|
||||||
|
"wh_message": reason_phrase,
|
||||||
|
"wh_response": text,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@
|
||||||
</p>
|
</p>
|
||||||
<small
|
<small
|
||||||
>Check
|
>Check
|
||||||
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/fiatjaf/awesome-lnurl"
|
||||||
|
target="_blank"
|
||||||
>Awesome LNURL</a
|
>Awesome LNURL</a
|
||||||
>
|
>
|
||||||
for further information.</small
|
for further information.</small
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="lightning:{{ lnurl }}">
|
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
value="{{ lnurl }}"
|
value="lightning:{{ lnurl }}"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="qrCodeDialog.data.lnurl"
|
:value="'lightning:' + qrCodeDialog.data.lnurl"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "print.html" %} {% block page %}
|
{% extends "print.html" %} {% block page %}
|
||||||
<div class="row justify-center">
|
<div class="row justify-center">
|
||||||
<div class="qr">
|
<div class="qr">
|
||||||
<qrcode value="{{ lnurl }}" :options="{width}"></qrcode>
|
<qrcode value="lightning:{{ lnurl }}" :options="{width}"></qrcode>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block styles %}
|
{% endblock %} {% block styles %}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,10 @@
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
<a v-if="col.label == 'LNURLPay'" @click="copyText(col.value)"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
v-if="col.label == 'LNURLPay'"
|
||||||
|
@click="copyText(col.value)"
|
||||||
><q-tooltip>Click to copy LNURL</q-tooltip>{{
|
><q-tooltip>Click to copy LNURL</q-tooltip>{{
|
||||||
col.value.substring(0, 40) }}...</a
|
col.value.substring(0, 40) }}...</a
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@
|
||||||
Access this lnbits instance at the following url
|
Access this lnbits instance at the following url
|
||||||
</h5>
|
</h5>
|
||||||
<q-separator class="q-my-lg"></q-separator>
|
<q-separator class="q-my-lg"></q-separator>
|
||||||
<p><a href="{{ ngrok }}" target="_blank">{{ ngrok }}</a></p>
|
<p>
|
||||||
|
<a class="text-secondary" href="{{ ngrok }}" target="_blank"
|
||||||
|
>{{ ngrok }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -29,7 +33,10 @@
|
||||||
</p>
|
</p>
|
||||||
<small
|
<small
|
||||||
>Created by
|
>Created by
|
||||||
<a href="https://github.com/supertestnet" target="_blank"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/supertestnet"
|
||||||
|
target="_blank"
|
||||||
>Supertestnet</a
|
>Supertestnet</a
|
||||||
>.</small
|
>.</small
|
||||||
>
|
>
|
||||||
|
|
|
||||||
44
lnbits/extensions/nostrnip5/README.md
Normal file
44
lnbits/extensions/nostrnip5/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
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"]
|
||||||
|
}
|
||||||
186
lnbits/extensions/nostrnip5/crud.py
Normal file
186
lnbits/extensions/nostrnip5/crud.py
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import Address, CreateAddressData, CreateDomainData, Domain
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
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_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 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)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
50
lnbits/extensions/nostrnip5/models.py
Normal file
50
lnbits/extensions/nostrnip5/models.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
from enum import Enum
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
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 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
35
lnbits/extensions/nostrnip5/tasks.py
Normal file
35
lnbits/extensions/nostrnip5/tasks.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
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 not payment.extra:
|
||||||
|
return
|
||||||
|
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
|
||||||
238
lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html
Normal file
238
lnbits/extensions/nostrnip5/templates/nostrnip5/_api_docs.html
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
<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 />
|
||||||
|
proxy_pass
|
||||||
|
https://{your_lnbits}/nostrnip5/api/v1/domain/{domain_id}/nostr.json;<br />
|
||||||
|
proxy_set_header Host {your_lnbits};<br />
|
||||||
|
proxy_ssl_server_name on;<br /><br />
|
||||||
|
|
||||||
|
expires 5m;<br />
|
||||||
|
add_header Cache-Control "public,
|
||||||
|
no-transform";<br /><br />
|
||||||
|
|
||||||
|
proxy_cache nip5_cache;<br />
|
||||||
|
proxy_cache_lock on;<br />
|
||||||
|
proxy_cache_valid 200 300s;<br />
|
||||||
|
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": <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>[<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/domains -H
|
||||||
|
"X-Api-Key: <invoice_key>"
|
||||||
|
</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": <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>[<address_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/addresses -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 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": <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>{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: <invoice_key>"
|
||||||
|
</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": <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>{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: <invoice_key>"
|
||||||
|
</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": <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>{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:
|
||||||
|
<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>
|
||||||
|
/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: <invoice_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-expansion-item>
|
||||||
709
lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
Normal file
709
lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
Normal file
|
|
@ -0,0 +1,709 @@
|
||||||
|
{% 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-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="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"
|
||||||
|
>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="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'
|
||||||
|
)
|
||||||
|
|
||||||
|
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: {}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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 %}
|
||||||
88
lnbits/extensions/nostrnip5/templates/nostrnip5/rotate.html
Normal file
88
lnbits/extensions/nostrnip5/templates/nostrnip5/rotate.html
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
{% 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 %}
|
||||||
207
lnbits/extensions/nostrnip5/templates/nostrnip5/signup.html
Normal file
207
lnbits/extensions/nostrnip5/templates/nostrnip5/signup.html
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
{% 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
|
||||||
|
<b
|
||||||
|
>{{ "{:0,.2f}".format(domain.amount / 100) }} {{ domain.currency }}</b
|
||||||
|
>
|
||||||
|
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 %}
|
||||||
68
lnbits/extensions/nostrnip5/views.py
Normal file
68
lnbits/extensions/nostrnip5/views.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from datetime import datetime
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
263
lnbits/extensions/nostrnip5/views_api.py
Normal file
263
lnbits/extensions/nostrnip5/views_api.py
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
from .models import CreateAddressData, CreateDomainData, 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.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)
|
||||||
|
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") == 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}
|
||||||
|
|
@ -236,7 +236,7 @@
|
||||||
|
|
||||||
<q-responsive v-if="itemDialog.data.id" :ratio="1">
|
<q-responsive v-if="itemDialog.data.id" :ratio="1">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="itemDialog.data.lnurl"
|
:value="'lightning:' + itemDialog.data.lnurl"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
<div class="row justify-center">
|
<div class="row justify-center">
|
||||||
<div v-for="item in items" class="q-my-sm q-mx-lg">
|
<div v-for="item in items" class="q-my-sm q-mx-lg">
|
||||||
<div class="text-center q-ma-none q-mb-sm">{{ item.name }}</div>
|
<div class="text-center q-ma-none q-mb-sm">{{ item.name }}</div>
|
||||||
<qrcode :value="item.lnurl" :options="{margin: 0, width: 250}"></qrcode>
|
<qrcode
|
||||||
|
:value="'lightning:' + item.lnurl"
|
||||||
|
:options="{margin: 0, width: 250}"
|
||||||
|
></qrcode>
|
||||||
<div class="text-center q-ma-none q-mt-sm">{{ item.price }}</div>
|
<div class="text-center q-ma-none q-mt-sm">{{ item.price }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,10 @@
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
<div v-if="paymentReq" class="q-mt-lg">
|
<div v-if="paymentReq" class="q-mt-lg">
|
||||||
<a :href="'lightning:' + paymentReq">
|
<a class="text-secondary" :href="'lightning:' + paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="paymentReq"
|
:value="'lightning:' + paymentReq.toUpperCase()"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@
|
||||||
</p>
|
</p>
|
||||||
<small
|
<small
|
||||||
>Check
|
>Check
|
||||||
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/fiatjaf/awesome-lnurl"
|
||||||
|
target="_blank"
|
||||||
>Awesome LNURL</a
|
>Awesome LNURL</a
|
||||||
>
|
>
|
||||||
for further information.</small
|
for further information.</small
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="lightning:{{ lnurl }}">
|
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="'{{ lnurl }}'"
|
:value="'lightning:{{ lnurl }}'"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="lightning:{{ lnurl }}">
|
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
<q-responsive :ratio="1" class="q-mx-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="'{{ lnurl }}'"
|
:value="'lightning:{{ lnurl }}'"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,17 @@ context %}{% block page %}
|
||||||
<center>
|
<center>
|
||||||
{% if lost %}
|
{% if lost %}
|
||||||
<h5 class="q-my-none">
|
<h5 class="q-my-none">
|
||||||
You lost. <a href="/satsdice/{{ link }}">Play again?</a>
|
You lost.
|
||||||
|
<a class="text-secondary" href="/satsdice/{{ link }}"
|
||||||
|
>Play again?</a
|
||||||
|
>
|
||||||
</h5>
|
</h5>
|
||||||
{% endif %} {% if paid %}
|
{% endif %} {% if paid %}
|
||||||
<h5 class="q-my-none">
|
<h5 class="q-my-none">
|
||||||
Winnings spent. <a href="/satsdice/{{ link }}">Play again?</a>
|
Winnings spent.
|
||||||
|
<a class="text-secondary" href="/satsdice/{{ link }}"
|
||||||
|
>Play again?</a
|
||||||
|
>
|
||||||
</h5>
|
</h5>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br />
|
<br />
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue