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
|
||||
|
||||
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
|
||||
|
||||
black:
|
||||
|
|
@ -18,7 +18,7 @@ mypy:
|
|||
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
|
||||
|
||||
checkblack:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
};
|
||||
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
forSystems = systems: f:
|
||||
nixpkgs.lib.genAttrs systems
|
||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import hashlib
|
||||
import re
|
||||
import time
|
||||
from binascii import unhexlify
|
||||
from decimal import Decimal
|
||||
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()
|
||||
sig = signature[0:64]
|
||||
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)
|
||||
else:
|
||||
keys = VerifyingKey.from_public_key_recovery(
|
||||
|
|
@ -131,7 +130,7 @@ def encode(options):
|
|||
if options["timestamp"]:
|
||||
addr.date = int(options["timestamp"])
|
||||
|
||||
addr.paymenthash = unhexlify(options["paymenthash"])
|
||||
addr.paymenthash = bytes.fromhex(options["paymenthash"])
|
||||
|
||||
if options["description"]:
|
||||
addr.tags.append(("d", options["description"]))
|
||||
|
|
@ -149,8 +148,8 @@ def encode(options):
|
|||
while len(splits) >= 5:
|
||||
route.append(
|
||||
(
|
||||
unhexlify(splits[0]),
|
||||
unhexlify(splits[1]),
|
||||
bytes.fromhex(splits[0]),
|
||||
bytes.fromhex(splits[1]),
|
||||
int(splits[2]),
|
||||
int(splits[3]),
|
||||
int(splits[4]),
|
||||
|
|
@ -235,7 +234,7 @@ def lnencode(addr, privkey):
|
|||
raise ValueError("Must include either 'd' or 'h'")
|
||||
|
||||
# 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(
|
||||
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||
)
|
||||
|
|
@ -261,7 +260,7 @@ class LnAddr(object):
|
|||
|
||||
def __str__(self):
|
||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
||||
bytes.hex(self.pubkey.serialize()).decode("utf-8"),
|
||||
self.amount,
|
||||
self.currency,
|
||||
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
||||
|
|
|
|||
|
|
@ -454,6 +454,7 @@ async def update_payment_details(
|
|||
async def update_payment_extra(
|
||||
payment_hash: str,
|
||||
extra: dict,
|
||||
outgoing: bool = False,
|
||||
conn: Optional[Connection] = 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.
|
||||
"""
|
||||
|
||||
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
|
||||
|
||||
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,),
|
||||
)
|
||||
if not row:
|
||||
|
|
@ -471,10 +474,7 @@ async def update_payment_extra(
|
|||
db_extra.update(extra)
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ",
|
||||
(json.dumps(db_extra), payment_hash),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ async def m007_set_invoice_expiries(db):
|
|||
)
|
||||
).fetchall()
|
||||
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, (
|
||||
payment_request,
|
||||
checking_id,
|
||||
|
|
@ -238,7 +238,7 @@ async def m007_set_invoice_expiries(db):
|
|||
invoice.date + invoice.expiry
|
||||
)
|
||||
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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import hmac
|
|||
import json
|
||||
import time
|
||||
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 fastapi import Query
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Extra, validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection
|
||||
from lnbits.helpers import url_for
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
|
@ -13,12 +12,7 @@ from loguru import logger
|
|||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import (
|
||||
|
|
@ -308,7 +302,7 @@ async def perform_lnurlauth(
|
|||
) -> Optional[LnurlErrorResponse]:
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from typing import Dict
|
|||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||
|
||||
from . import db
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
<a :href="'lightning:' + props.row.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="props.row.bolt11"
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
@ -325,7 +325,7 @@
|
|||
</p>
|
||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||
<qrcode
|
||||
value="{{wallet.lnurlwithdraw_full}}"
|
||||
value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||
:options="{width:240}"
|
||||
></qrcode>
|
||||
</a>
|
||||
|
|
@ -524,7 +524,7 @@
|
|||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="receive.paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import asyncio
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
|
@ -38,7 +37,7 @@ from lnbits.decorators import (
|
|||
require_admin_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.utils.exchange_rates import (
|
||||
currencies,
|
||||
|
|
@ -48,14 +47,11 @@ from lnbits.utils.exchange_rates import (
|
|||
|
||||
from .. import core_app, db
|
||||
from ..crud import (
|
||||
create_payment,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_total_balance,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
update_payment_status,
|
||||
update_wallet,
|
||||
)
|
||||
from ..services import (
|
||||
|
|
@ -71,6 +67,11 @@ from ..services import (
|
|||
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")
|
||||
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
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:
|
||||
try:
|
||||
description_hash = (
|
||||
binascii.unhexlify(data.description_hash)
|
||||
if data.description_hash
|
||||
else b""
|
||||
bytes.fromhex(data.description_hash) if data.description_hash else b""
|
||||
)
|
||||
unhashed_description = (
|
||||
binascii.unhexlify(data.unhashed_description)
|
||||
bytes.fromhex(data.unhashed_description)
|
||||
if data.unhashed_description
|
||||
else b""
|
||||
)
|
||||
except binascii.Error:
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
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():
|
||||
WALLET = get_wallet_class()
|
||||
total_balance = await get_total_balance()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ async def favicon():
|
|||
|
||||
|
||||
@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(
|
||||
"core/index.html", {"request": request, "lnurl": lightning}
|
||||
)
|
||||
|
|
@ -124,12 +124,15 @@ async def wallet(
|
|||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
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(
|
||||
"error.html", {"request": request, "err": "User not authorized."}
|
||||
)
|
||||
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
||||
user.admin = True
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name: # type: ignore
|
||||
wallet = user.wallets[0] # type: ignore
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
|||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits import bolt11
|
||||
|
||||
|
|
|
|||
|
|
@ -236,8 +236,8 @@ async def check_user_exists(usr: UUID4) -> User:
|
|||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
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 != settings.super_user
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import base64
|
|||
import hashlib
|
||||
import hmac
|
||||
import urllib
|
||||
from binascii import unhexlify
|
||||
from http import HTTPStatus
|
||||
from typing import Dict
|
||||
|
||||
|
|
@ -19,7 +18,7 @@ def generate_bleskomat_lnurl_signature(
|
|||
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
|
||||
):
|
||||
if api_key_encoding == "hex":
|
||||
key = unhexlify(api_key_secret)
|
||||
key = bytes.fromhex(api_key_secret)
|
||||
elif api_key_encoding == "base64":
|
||||
key = base64.b64decode(api_key_secret)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@
|
|||
<p>
|
||||
This extension allows you to connect a Bleskomat ATM to an lnbits
|
||||
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
|
||||
>
|
||||
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>
|
||||
<h5 class="text-subtitle1 q-my-none">Connect Your Bleskomat ATM</h5>
|
||||
<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.
|
||||
|
||||
<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!***
|
||||
|
||||
|
|
@ -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!
|
||||
- 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)
|
||||
|
||||
Follow the guide.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
Manage your Bolt Cards self custodian way<br />
|
||||
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards"
|
||||
>More details</a
|
||||
>
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@
|
|||
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||
(QR for <strong>create</strong> the card in
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||
target="_blank"
|
||||
style="color: inherit"
|
||||
|
|
@ -395,6 +396,7 @@
|
|||
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||
(QR for <strong>wipe</strong> the card in
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||
target="_blank"
|
||||
style="color: inherit"
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ async def api_hits(
|
|||
|
||||
|
||||
@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)
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import os
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import sha256
|
||||
from typing import Awaitable, Union
|
||||
|
||||
|
|
@ -56,7 +55,7 @@ async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
|
|||
raise
|
||||
|
||||
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(
|
||||
"post",
|
||||
|
|
@ -121,7 +120,7 @@ async def create_reverse_swap(
|
|||
return False
|
||||
|
||||
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_hash = sha256(preimage).hexdigest()
|
||||
|
||||
|
|
@ -311,12 +310,12 @@ async def create_onchain_tx(
|
|||
sequence = 0xFFFFFFFE
|
||||
else:
|
||||
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
|
||||
preimage = unhexlify(swap.preimage)
|
||||
preimage = bytes.fromhex(swap.preimage)
|
||||
onchain_address = swap.onchain_address
|
||||
sequence = 0xFFFFFFFF
|
||||
|
||||
locktime = swap.timeout_block_height
|
||||
redeem_script = unhexlify(swap.redeem_script)
|
||||
redeem_script = bytes.fromhex(swap.redeem_script)
|
||||
|
||||
fees = get_fee_estimation()
|
||||
|
||||
|
|
@ -324,7 +323,7 @@ async def create_onchain_tx(
|
|||
|
||||
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)]
|
||||
tx = Transaction(vin=vin, vout=vout)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
from binascii import hexlify
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
|
|
@ -84,7 +83,7 @@ def get_mempool_blockheight() -> int:
|
|||
|
||||
|
||||
async def send_onchain_tx(tx: Transaction):
|
||||
raw = hexlify(tx.serialize())
|
||||
raw = bytes.hex(tx.serialize())
|
||||
logger.debug(f"Boltz - mempool sending onchain tx...")
|
||||
req_wrap(
|
||||
"post",
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@
|
|||
</p>
|
||||
<p>
|
||||
Link :
|
||||
<a target="_blank" href="https://boltz.exchange"
|
||||
<a class="text-secondary" target="_blank" href="https://boltz.exchange"
|
||||
>https://boltz.exchange
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
class="text-secondary"
|
||||
target="_blank"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||
>More details</a
|
||||
|
|
@ -38,7 +39,12 @@
|
|||
<p>
|
||||
<small
|
||||
>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>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import random
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
from cashu.core.base import MintKeyset
|
||||
|
|
|
|||
|
|
@ -4,9 +4,24 @@
|
|||
<p>Create Cashu ecash mints and wallets.</p>
|
||||
<small
|
||||
>Created by
|
||||
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,
|
||||
<a href="https://github.com/motorina0" target="_blank">vlad</a>,
|
||||
<a href="https://github.com/calle" target="_blank">calle</a>.</small
|
||||
<a
|
||||
class="text-secondary"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
></q-icon>
|
||||
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||
<a
|
||||
class="text-secondary"
|
||||
class="q-my-xl text-white"
|
||||
style="font-size: 1.5rem"
|
||||
href="../wallet?mint_id={{ mint_id }}"
|
||||
|
|
@ -24,7 +25,11 @@
|
|||
<h5 class="q-my-md">Read the following carefully!</h5>
|
||||
<p>
|
||||
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
|
||||
>
|
||||
mint. Cashu is an ecash system for Bitcoin.
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ page_container %}
|
|||
size="lg"
|
||||
color="secondary"
|
||||
class="q-mr-md cursor-pointer"
|
||||
@click="recheckInvoice(props.row.hash)"
|
||||
@click="checkInvoice(props.row.hash)"
|
||||
>
|
||||
Check
|
||||
</q-badge>
|
||||
|
|
@ -616,10 +616,10 @@ page_container %}
|
|||
></q-input>
|
||||
</div>
|
||||
<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">
|
||||
<qrcode
|
||||
:value="invoiceData.bolt11"
|
||||
:value="'lightning:' + invoiceData.bolt11.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
>
|
||||
|
|
@ -681,7 +681,7 @@ page_container %}
|
|||
</div>
|
||||
<div v-else 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">
|
||||
<qrcode
|
||||
: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)
|
||||
},
|
||||
|
||||
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 ///////////
|
||||
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.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)
|
||||
},
|
||||
// MINT
|
||||
|
||||
requestMintButton: async function () {
|
||||
await this.requestMint()
|
||||
|
|
@ -1586,8 +1546,12 @@ page_container %}
|
|||
await this.invoiceCheckWorker()
|
||||
},
|
||||
|
||||
// /mint
|
||||
|
||||
requestMint: async function () {
|
||||
// gets an invoice from the mint to get new tokens
|
||||
/*
|
||||
gets an invoice from the mint to get new tokens
|
||||
*/
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
|
|
@ -1611,7 +1575,14 @@ page_container %}
|
|||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /mint
|
||||
|
||||
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)
|
||||
try {
|
||||
let secrets = await this.generateSecrets(amounts)
|
||||
|
|
@ -1647,7 +1618,19 @@ page_container %}
|
|||
}
|
||||
this.proofs = this.proofs.concat(proofs)
|
||||
this.storeProofs()
|
||||
|
||||
// update UI
|
||||
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
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
|
@ -1657,62 +1640,20 @@ page_container %}
|
|||
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
|
||||
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
|
||||
|
||||
split: async function (proofs, amount) {
|
||||
/*
|
||||
supplies proofs and requests a split from the mint of these
|
||||
proofs at a specific amount
|
||||
*/
|
||||
try {
|
||||
if (proofs.length == 0) {
|
||||
throw new Error('no proofs provided.')
|
||||
}
|
||||
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
||||
// delete proofs from this.proofs
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||
this.deleteProofs(proofs)
|
||||
// add new fristProofs, scndProofs to this.proofs
|
||||
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
||||
this.storeProofs()
|
||||
|
|
@ -1723,6 +1664,9 @@ page_container %}
|
|||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /split
|
||||
|
||||
splitApi: async function (proofs, amount) {
|
||||
try {
|
||||
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 () {
|
||||
/*
|
||||
uses split to receive new tokens.
|
||||
*/
|
||||
this.showReceiveTokens = false
|
||||
console.log('### receive tokens', this.receiveData.tokensBase64)
|
||||
try {
|
||||
|
|
@ -1793,6 +1792,9 @@ page_container %}
|
|||
const proofs = JSON.parse(tokenJson)
|
||||
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
||||
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
||||
|
||||
// update UI
|
||||
|
||||
// HACK: we need to do this so the balance updates
|
||||
this.proofs = this.proofs.concat([])
|
||||
|
||||
|
|
@ -1827,13 +1829,18 @@ page_container %}
|
|||
},
|
||||
|
||||
sendTokens: async function () {
|
||||
/*
|
||||
calls splitToSend, displays token and kicks off the spendableWorker
|
||||
*/
|
||||
try {
|
||||
// keep firstProofs, send scndProofs
|
||||
// keep firstProofs, send scndProofs and delete them (invalidate=true)
|
||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||
this.proofs,
|
||||
this.sendData.amount,
|
||||
true
|
||||
)
|
||||
|
||||
// update UI
|
||||
this.sendData.tokens = scndProofs
|
||||
console.log('### this.sendData.tokens', this.sendData.tokens)
|
||||
this.sendData.tokensBase64 = btoa(
|
||||
|
|
@ -1846,33 +1853,19 @@ page_container %}
|
|||
date: currentDateStr(),
|
||||
token: this.sendData.tokensBase64
|
||||
})
|
||||
|
||||
// store "pending" outgoing tokens in history table
|
||||
this.storehistoryTokens()
|
||||
|
||||
this.checkTokenSpendableWorker()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
// /melt
|
||||
|
||||
melt: async function () {
|
||||
// todo: get fees from server and add to inputs
|
||||
this.payInvoiceData.blocking = true
|
||||
|
|
@ -1924,8 +1917,20 @@ page_container %}
|
|||
]
|
||||
})
|
||||
// delete spent tokens from db
|
||||
this.proofs = fristProofs
|
||||
this.storeProofs()
|
||||
this.deleteProofs(scndProofs)
|
||||
|
||||
// update UI
|
||||
|
||||
tokensBase64 = btoa(JSON.stringify(scndProofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: -amount,
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
|
||||
console.log({
|
||||
amount: -amount,
|
||||
bolt11: this.payInvoiceData.data.request,
|
||||
|
|
@ -1953,13 +1958,95 @@ page_container %}
|
|||
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) {
|
||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||
invoice.status = 'paid'
|
||||
this.storeinvoicesCashu()
|
||||
},
|
||||
recheckInvoice: async function (payment_hash, verbose = true) {
|
||||
console.log('### recheckInvoice.hash', payment_hash)
|
||||
checkInvoice: async function (payment_hash, verbose = true) {
|
||||
console.log('### checkInvoice.hash', payment_hash)
|
||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||
try {
|
||||
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
||||
|
|
@ -1969,15 +2056,15 @@ page_container %}
|
|||
throw error
|
||||
}
|
||||
},
|
||||
recheckPendingInvoices: async function () {
|
||||
checkPendingInvoices: async function () {
|
||||
for (const invoice of this.invoicesCashu) {
|
||||
if (invoice.status === 'pending' && invoice.sat > 0) {
|
||||
this.recheckInvoice(invoice.hash, false)
|
||||
if (invoice.status === 'pending' && invoice.amount > 0) {
|
||||
this.checkInvoice(invoice.hash, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
recheckPendingTokens: async function () {
|
||||
checkPendingTokens: async function () {
|
||||
for (const token of this.historyTokens) {
|
||||
if (token.status === 'pending' && token.amount < 0) {
|
||||
this.checkTokenSpendable(token.token, false)
|
||||
|
|
@ -1990,51 +2077,15 @@ page_container %}
|
|||
this.storehistoryTokens()
|
||||
},
|
||||
|
||||
checkTokenSpendableWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
this.tokensCheckSpendableListener = setInterval(async () => {
|
||||
try {
|
||||
nInterval += 1
|
||||
// exit loop after 2m
|
||||
if (nInterval > 24) {
|
||||
console.log('### stopping token check worker')
|
||||
this.clearAllWorkers()
|
||||
}
|
||||
console.log('### checkTokenSpendableWorker setInterval', nInterval)
|
||||
console.log(this.sendData)
|
||||
|
||||
// this will throw an error if the invoice is pending
|
||||
paid = await this.checkTokenSpendable(
|
||||
this.sendData.tokensBase64,
|
||||
false
|
||||
)
|
||||
if (paid) {
|
||||
console.log('### stopping token check worker')
|
||||
this.clearAllWorkers()
|
||||
this.sendData.tokens = ''
|
||||
this.showSendTokens = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('not paid yet')
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
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)
|
||||
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
|
||||
)
|
||||
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)) {
|
||||
|
|
@ -2079,23 +2130,89 @@ page_container %}
|
|||
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)
|
||||
////////////// 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 () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
this.tokensCheckSpendableListener = setInterval(async () => {
|
||||
try {
|
||||
nInterval += 1
|
||||
// exit loop after 2m
|
||||
if (nInterval > 24) {
|
||||
console.log('### stopping token check worker')
|
||||
this.clearAllWorkers()
|
||||
}
|
||||
console.log('### checkTokenSpendableWorker setInterval', nInterval)
|
||||
console.log(this.sendData)
|
||||
|
||||
// this will throw an error if the invoice is pending
|
||||
paid = await this.checkTokenSpendable(
|
||||
this.sendData.tokensBase64,
|
||||
false
|
||||
)
|
||||
if (paid) {
|
||||
console.log('### stopping token check worker')
|
||||
this.clearAllWorkers()
|
||||
this.sendData.tokens = ''
|
||||
this.showSendTokens = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('not paid yet')
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -2116,62 +2233,62 @@ page_container %}
|
|||
}
|
||||
},
|
||||
|
||||
checkInvoice: function () {
|
||||
console.log('#### checkInvoice')
|
||||
try {
|
||||
const invoice = decode(this.payInvoiceData.data.request)
|
||||
// checkInvoice: function () {
|
||||
// console.log('#### checkInvoice')
|
||||
// try {
|
||||
// const invoice = decode(this.payInvoiceData.data.request)
|
||||
|
||||
const cleanInvoice = {
|
||||
msat: invoice.human_readable_part.amount,
|
||||
sat: invoice.human_readable_part.amount / 1000,
|
||||
fsat: LNbits.utils.formatSat(
|
||||
invoice.human_readable_part.amount / 1000
|
||||
)
|
||||
}
|
||||
// const cleanInvoice = {
|
||||
// msat: invoice.human_readable_part.amount,
|
||||
// sat: invoice.human_readable_part.amount / 1000,
|
||||
// fsat: LNbits.utils.formatSat(
|
||||
// invoice.human_readable_part.amount / 1000
|
||||
// )
|
||||
// }
|
||||
|
||||
_.each(invoice.data.tags, tag => {
|
||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
if (tag.description === 'payment_hash') {
|
||||
cleanInvoice.hash = tag.value
|
||||
} else if (tag.description === 'description') {
|
||||
cleanInvoice.description = tag.value
|
||||
} else if (tag.description === 'expiry') {
|
||||
var expireDate = new Date(
|
||||
(invoice.data.time_stamp + tag.value) * 1000
|
||||
)
|
||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
expireDate,
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
)
|
||||
cleanInvoice.expired = false // TODO
|
||||
}
|
||||
}
|
||||
// _.each(invoice.data.tags, tag => {
|
||||
// if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
// if (tag.description === 'payment_hash') {
|
||||
// cleanInvoice.hash = tag.value
|
||||
// } else if (tag.description === 'description') {
|
||||
// cleanInvoice.description = tag.value
|
||||
// } else if (tag.description === 'expiry') {
|
||||
// var expireDate = new Date(
|
||||
// (invoice.data.time_stamp + tag.value) * 1000
|
||||
// )
|
||||
// cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
// expireDate,
|
||||
// 'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
// )
|
||||
// cleanInvoice.expired = false // TODO
|
||||
// }
|
||||
// }
|
||||
|
||||
this.payInvoiceData.invoice = cleanInvoice
|
||||
})
|
||||
// this.payInvoiceData.invoice = cleanInvoice
|
||||
// })
|
||||
|
||||
console.log(
|
||||
'#### this.payInvoiceData.invoice',
|
||||
this.payInvoiceData.invoice
|
||||
)
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Could not decode invoice',
|
||||
caption: error + '',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// console.log(
|
||||
// '#### this.payInvoiceData.invoice',
|
||||
// this.payInvoiceData.invoice
|
||||
// )
|
||||
// } catch (error) {
|
||||
// this.$q.notify({
|
||||
// timeout: 5000,
|
||||
// type: 'warning',
|
||||
// message: 'Could not decode invoice',
|
||||
// caption: error + '',
|
||||
// position: 'top',
|
||||
// actions: [
|
||||
// {
|
||||
// icon: 'close',
|
||||
// color: 'white',
|
||||
// handler: () => {}
|
||||
// }
|
||||
// ]
|
||||
// })
|
||||
// throw error
|
||||
// }
|
||||
// },
|
||||
|
||||
////////////// STORAGE /////////////
|
||||
|
||||
|
|
@ -2335,8 +2452,9 @@ page_container %}
|
|||
console.log('#### this.mintId', this.mintId)
|
||||
console.log('#### this.mintName', this.mintName)
|
||||
|
||||
this.recheckPendingInvoices()
|
||||
this.recheckPendingTokens()
|
||||
this.checkProofsSpendable(this.proofs, true)
|
||||
this.checkPendingInvoices()
|
||||
this.checkPendingTokens()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -46,9 +46,16 @@ from .models import Cashu
|
|||
|
||||
# --------- 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
|
||||
|
||||
if not LIGHTNING:
|
||||
logger.warning(
|
||||
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
||||
)
|
||||
|
||||
########################################
|
||||
############### 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)
|
||||
|
||||
|
||||
@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)
|
||||
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||
"""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")
|
||||
async def mint_coins(
|
||||
async def mint(
|
||||
data: MintRequest,
|
||||
cashu_id: 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."
|
||||
)
|
||||
|
||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||
|
||||
if LIGHTNING:
|
||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash
|
||||
|
|
@ -206,12 +237,22 @@ async def mint_coins(
|
|||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Mint does not know this invoice.",
|
||||
)
|
||||
if invoice.issued == True:
|
||||
if invoice.issued:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail="Tokens already issued for this invoice.",
|
||||
)
|
||||
|
||||
# set this invoice as issued
|
||||
await ledger.crud.update_lightning_invoice(
|
||||
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(
|
||||
|
|
@ -219,29 +260,32 @@ async def mint_coins(
|
|||
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:
|
||||
if not status.paid:
|
||||
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(
|
||||
db=ledger.db, hash=payment_hash, issued=True
|
||||
)
|
||||
|
||||
return promises
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
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
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||
|
|
@ -285,13 +329,17 @@ async def melt_coins(
|
|||
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")
|
||||
try:
|
||||
await pay_invoice(
|
||||
wallet_id=cashu.wallet,
|
||||
payment_request=invoice,
|
||||
description=f"Pay cashu invoice",
|
||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
||||
raise e
|
||||
finally:
|
||||
logger.debug(
|
||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||
)
|
||||
|
|
@ -299,14 +347,20 @@ async def melt_coins(
|
|||
cashu.wallet, invoice_obj.payment_hash
|
||||
)
|
||||
if status.paid == True:
|
||||
logger.debug("Cashu: Payment successful, invalidating proofs")
|
||||
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:
|
||||
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Cashu: {str(e)}",
|
||||
)
|
||||
finally:
|
||||
logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}")
|
||||
# delete proofs from pending list
|
||||
await ledger._unset_proofs_pending(proofs)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
||||
animation<br />
|
||||
<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>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
>
|
||||
<div class="col">
|
||||
<qrcode
|
||||
:value="copilot.lnurl"
|
||||
:value="'lightning:' + copilot.lnurl"
|
||||
:options="{width:250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -11,18 +11,24 @@
|
|||
</h5>
|
||||
<p>
|
||||
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
|
||||
>
|
||||
leveraging LNbits as a community based lightning node.<br />
|
||||
<small>
|
||||
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 />
|
||||
<small>
|
||||
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>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
This extension is designed to be used through its API by a Discord Bot,
|
||||
currently you have to install the bot
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/chrislennon/lnbits-discord-bot/#installation"
|
||||
>yourself</a
|
||||
><br />
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
Events comes with a shareable ticket scanner, which can be used to
|
||||
register attendees.<br />
|
||||
<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>
|
||||
</p>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@
|
|||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></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>
|
||||
|
||||
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.
|
||||
</h5>
|
||||
<p>
|
||||
<a href="https://bitcoinhivemind.com/">Hivemind</a> is a Bitcoin sidechain
|
||||
project for a peer-to-peer oracle protocol that absorbs accurate data into
|
||||
a blockchain so that Bitcoin users can speculate in prediction markets.
|
||||
<a class="text-secondary" href="https://bitcoinhivemind.com/">Hivemind</a>
|
||||
is a Bitcoin sidechain project for a peer-to-peer oracle protocol that
|
||||
absorbs accurate data into a blockchain so that Bitcoin users can
|
||||
speculate in prediction markets.
|
||||
</p>
|
||||
<p>
|
||||
These markets have the potential to revolutionize the emergence of
|
||||
|
|
@ -17,8 +18,8 @@
|
|||
</p>
|
||||
<p>
|
||||
This extension will become fully operative when the
|
||||
<a href="https://drivechain.xyz/">BIP300</a> soft-fork gets activated and
|
||||
Bitcoin Hivemind is launched.
|
||||
<a class="text-secondary" href="https://drivechain.xyz/">BIP300</a>
|
||||
soft-fork gets activated and Bitcoin Hivemind is launched.
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
|
|||
|
|
@ -251,10 +251,13 @@ block page %}
|
|||
@hide="closeQrCodeDialog"
|
||||
>
|
||||
<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">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.payment_request"
|
||||
:value="'lightning:' + qrCodeDialog.data.payment_request.toUpperCase()"
|
||||
:options="{width: 400}"
|
||||
class="rounded-borders"
|
||||
></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)
|
||||
async def index(request: Request, invoice_id: str):
|
||||
async def pay(request: Request, invoice_id: str):
|
||||
invoice = await get_invoice(invoice_id)
|
||||
|
||||
if not invoice:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
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
|
||||
<a
|
||||
class="text-secondary"
|
||||
style="color: #43a047"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here
|
||||
|
|
@ -9,9 +10,14 @@
|
|||
<br /><br />Select the playlists you want people to be able to pay for, share
|
||||
the frontend page, profit :) <br /><br />
|
||||
Made by,
|
||||
<a style="color: #43a047" href="https://twitter.com/arcbtc">benarc</a>.
|
||||
Inspired by,
|
||||
<a
|
||||
class="text-secondary"
|
||||
style="color: #43a047"
|
||||
href="https://twitter.com/arcbtc"
|
||||
>benarc</a
|
||||
>. Inspired by,
|
||||
<a
|
||||
class="text-secondary"
|
||||
style="color: #43a047"
|
||||
href="https://twitter.com/pirosb3/status/1056263089128161280"
|
||||
>pirosb3</a
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="'lightning:' + receive.paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@
|
|||
<q-td auto-width>{{ props.row.name }}</q-td>
|
||||
<q-td class="text-center" auto-width>
|
||||
<a
|
||||
class="text-secondary"
|
||||
target="_blank"
|
||||
:href="'/wallet?usr=' + props.row.user + '&wal=' + props.row.wallet"
|
||||
>
|
||||
|
|
@ -191,7 +192,7 @@
|
|||
</q-select>
|
||||
</q-form>
|
||||
|
||||
<a :href="'lightning:' + livestream.lnurl">
|
||||
<a class="text-secondary" :href="'lightning:' + livestream.lnurl">
|
||||
<q-responsive :ratio="1" class="q-mx-sm">
|
||||
<qrcode
|
||||
:value="livestream.lnurl"
|
||||
|
|
@ -235,10 +236,10 @@
|
|||
<p class="text-subtitle1 q-my-none">
|
||||
Standalone QR Code for this track
|
||||
</p>
|
||||
<a :href="'lightning:' + trackDialog.data.lnurl">
|
||||
<a class="text-secondary" :href="'lightning:' + trackDialog.data.lnurl">
|
||||
<q-responsive :ratio="1" class="q-mx-sm">
|
||||
<qrcode
|
||||
:value="trackDialog.data.lnurl"
|
||||
:value="'lightning:' + trackDialog.data.lnurl.toUpperCase()"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@
|
|||
Charge people for using your domain name...<br />
|
||||
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/lnaddress"
|
||||
>More details</a
|
||||
>
|
||||
<br />
|
||||
<small>
|
||||
Created by,
|
||||
<a href="https://twitter.com/talvasconcelos">talvasconcelos</a></small
|
||||
<a class="text-secondary" href="https://twitter.com/talvasconcelos"
|
||||
>talvasconcelos</a
|
||||
></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -184,10 +184,10 @@
|
|||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:value="'lightning:' + paymentReq.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@
|
|||
<template v-slot:hint>
|
||||
Check extension
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits-legend/blob/main/lnbits/extensions/lnaddress/README.md"
|
||||
>documentation!</a
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
To access an LNbits wallet from a mobile phone,
|
||||
<ol>
|
||||
<li>
|
||||
Install either <a href="https://zeusln.app">Zeus</a> or
|
||||
<a href="https://bluewallet.io/">BlueWallet</a>;
|
||||
Install either
|
||||
<a class="text-secondary" href="https://zeusln.app">Zeus</a> or
|
||||
<a class="text-secondary" href="https://bluewallet.io/">BlueWallet</a
|
||||
>;
|
||||
</li>
|
||||
<li>
|
||||
Go to <code>Add a wallet / Import wallet</code> on BlueWallet or
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
<q-card-section>
|
||||
<p>
|
||||
LndHub is a protocol invented by
|
||||
<a href="https://bluewallet.io/">BlueWallet</a> that allows mobile
|
||||
wallets to query payments and balances, generate invoices and make
|
||||
payments from accounts that exist on a server. The protocol is a
|
||||
collection of HTTP endpoints exposed through the internet.
|
||||
<a class="text-secondary" href="https://bluewallet.io/">BlueWallet</a>
|
||||
that allows mobile wallets to query payments and balances, generate
|
||||
invoices and make payments from accounts that exist on a server. The
|
||||
protocol is a collection of HTTP endpoints exposed through the internet.
|
||||
</p>
|
||||
<p>
|
||||
For a wallet that supports it, reading a QR code that contains the URL
|
||||
along with secret access credentials should enable access. Currently it
|
||||
is supported by <a href="https://zeusln.app">Zeus</a> and
|
||||
<a href="https://bluewallet.io/">BlueWallet</a>.
|
||||
is supported by
|
||||
<a class="text-secondary" href="https://zeusln.app">Zeus</a> and
|
||||
<a class="text-secondary" href="https://bluewallet.io/">BlueWallet</a>.
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
>
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a :href="selectedWallet[type]">
|
||||
<a class="text-secondary" :href="selectedWallet[type]">
|
||||
<q-responsive :ratio="1" class="q-mx-sm">
|
||||
<qrcode
|
||||
:value="selectedWallet[type]"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from binascii import unhexlify
|
||||
|
||||
from lnbits.bolt11 import Invoice
|
||||
|
||||
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@
|
|||
paid support ticketing, PAYG language services, contact spam
|
||||
protection.<br />
|
||||
<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>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@
|
|||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:value="'lightning:' + paymentReq.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices
|
|||
async def create_lnurldevice(
|
||||
data: createLnurldevice,
|
||||
) -> lnurldevices:
|
||||
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()
|
||||
await db.execute(
|
||||
|
|
@ -79,6 +82,17 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev
|
|||
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:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ class createLnurldevice(BaseModel):
|
|||
wallet: str
|
||||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
amount: int
|
||||
profit: float = 0
|
||||
amount: Optional[int] = 0
|
||||
pin: int = 0
|
||||
profit1: float = 0
|
||||
amount1: int = 0
|
||||
|
|
|
|||
|
|
@ -4,21 +4,25 @@
|
|||
For LNURL based Points of Sale, ATMs, and relay devices<br />
|
||||
Use with: <br />
|
||||
LNPoS
|
||||
<a href="https://lnbits.github.io/lnpos">
|
||||
<a class="text-secondary" href="https://lnbits.github.io/lnpos">
|
||||
https://lnbits.github.io/lnpos</a
|
||||
><br />
|
||||
bitcoinSwitch
|
||||
<a href="https://github.com/lnbits/bitcoinSwitch">
|
||||
<a class="text-secondary" href="https://github.com/lnbits/bitcoinSwitch">
|
||||
https://github.com/lnbits/bitcoinSwitch</a
|
||||
><br />
|
||||
FOSSA
|
||||
<a href="https://github.com/lnbits/fossa">
|
||||
<a class="text-secondary" href="https://github.com/lnbits/fossa">
|
||||
https://github.com/lnbits/fossa</a
|
||||
><br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>,
|
||||
<a href="https://github.com/blackcoffeexbt">BC</a>,
|
||||
<a href="https://github.com/motorina0">Vlad Stan</a></small
|
||||
Created by,
|
||||
<a class="text-secondary" href="https://github.com/benarc">Ben Arc</a>,
|
||||
<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>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@
|
|||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="lnurlValue"
|
||||
:value="'lightning:' + lnurlValue"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import SQLITE
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import CreatePayLinkData, 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"
|
||||
method = db.execute if db.type == SQLITE else db.fetchone
|
||||
|
||||
result = await (method)(
|
||||
result = await db.execute(
|
||||
f"""
|
||||
INSERT INTO lnurlp.pay_links (
|
||||
id,
|
||||
wallet,
|
||||
description,
|
||||
min,
|
||||
|
|
@ -29,10 +28,11 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|||
currency,
|
||||
fiat_base_multiplier
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
{returning}
|
||||
""",
|
||||
(
|
||||
link_id,
|
||||
wallet_id,
|
||||
data.description,
|
||||
data.min,
|
||||
|
|
@ -47,10 +47,6 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
|||
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)
|
||||
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_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):
|
||||
id: int
|
||||
id: str
|
||||
wallet: str
|
||||
description: str
|
||||
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)
|
||||
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:
|
||||
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(
|
||||
payment: Payment, status: int, is_success: bool, reason_phrase="", text=""
|
||||
payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
|
||||
) -> 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>
|
||||
<small
|
||||
>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
|
||||
>
|
||||
for further information.</small
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a href="lightning:{{ lnurl }}">
|
||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
value="{{ lnurl }}"
|
||||
value="lightning:{{ lnurl }}"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@
|
|||
{% raw %}
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.lnurl"
|
||||
:value="'lightning:' + qrCodeDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "print.html" %} {% block page %}
|
||||
<div class="row justify-center">
|
||||
<div class="qr">
|
||||
<qrcode value="{{ lnurl }}" :options="{width}"></qrcode>
|
||||
<qrcode value="lightning:{{ lnurl }}" :options="{width}"></qrcode>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block styles %}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@
|
|||
<template v-slot:body="props">
|
||||
<q-tr :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>{{
|
||||
col.value.substring(0, 40) }}...</a
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@
|
|||
Access this lnbits instance at the following url
|
||||
</h5>
|
||||
<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>
|
||||
</div>
|
||||
|
|
@ -29,7 +33,10 @@
|
|||
</p>
|
||||
<small
|
||||
>Created by
|
||||
<a href="https://github.com/supertestnet" target="_blank"
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/supertestnet"
|
||||
target="_blank"
|
||||
>Supertestnet</a
|
||||
>.</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">
|
||||
<qrcode
|
||||
:value="itemDialog.data.lnurl"
|
||||
:value="'lightning:' + itemDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
<div class="row justify-center">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@
|
|||
</div>
|
||||
</q-form>
|
||||
<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">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:value="'lightning:' + paymentReq.toUpperCase()"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@
|
|||
</p>
|
||||
<small
|
||||
>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
|
||||
>
|
||||
for further information.</small
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a href="lightning:{{ lnurl }}">
|
||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ lnurl }}'"
|
||||
:value="'lightning:{{ lnurl }}'"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a href="lightning:{{ lnurl }}">
|
||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ lnurl }}'"
|
||||
:value="'lightning:{{ lnurl }}'"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
|
|
|||
|
|
@ -7,11 +7,17 @@ context %}{% block page %}
|
|||
<center>
|
||||
{% if lost %}
|
||||
<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>
|
||||
{% endif %} {% if paid %}
|
||||
<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>
|
||||
{% endif %}
|
||||
<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