refactor: decorators, models and more broken bits
@ -5,8 +5,6 @@ charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{html,js,json}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
4
.gitignore
vendored
@ -11,8 +11,10 @@ __pycache__
|
||||
*.egg-info
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.webassets-cache
|
||||
htmlcov
|
||||
Pipfile.lock
|
||||
test-reports
|
||||
|
||||
*.swo
|
||||
*.swp
|
||||
@ -24,3 +26,5 @@ venv
|
||||
|
||||
*.sqlite3
|
||||
.pyre*
|
||||
|
||||
__bundle__
|
||||
|
6
Pipfile
@ -10,7 +10,13 @@ python_version = "3.7"
|
||||
bitstring = "*"
|
||||
lnurl = "*"
|
||||
flask = "*"
|
||||
flask-assets = "*"
|
||||
flask-compress = "*"
|
||||
flask-talisman = "*"
|
||||
gevent = "*"
|
||||
greenlet = "*"
|
||||
gunicorn = "*"
|
||||
pyscss = "*"
|
||||
requests = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
0
docs/README.md
Normal file
@ -3,12 +3,15 @@ import json
|
||||
import requests
|
||||
import uuid
|
||||
|
||||
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
||||
from flask import g, Flask, jsonify, redirect, render_template, request, url_for
|
||||
from flask_assets import Environment, Bundle
|
||||
from flask_compress import Compress
|
||||
from flask_talisman import Talisman
|
||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||
|
||||
from . import bolt11
|
||||
from .core import core_app
|
||||
from .decorators import api_validate_post_request
|
||||
from .db import init_databases, open_db
|
||||
from .helpers import ExtensionManager, megajson
|
||||
from .settings import WALLET, DEFAULT_USER_WALLET_NAME, FEE_RESERVE
|
||||
@ -21,6 +24,7 @@ valid_extensions = [ext for ext in ExtensionManager().extensions if ext.is_valid
|
||||
# optimization & security
|
||||
# -----------------------
|
||||
|
||||
Compress(app)
|
||||
Talisman(
|
||||
app,
|
||||
content_security_policy={
|
||||
@ -34,6 +38,8 @@ Talisman(
|
||||
"fonts.googleapis.com",
|
||||
"fonts.gstatic.com",
|
||||
"maxcdn.bootstrapcdn.com",
|
||||
"github.com",
|
||||
"avatars2.githubusercontent.com",
|
||||
]
|
||||
},
|
||||
)
|
||||
@ -55,13 +61,23 @@ for ext in valid_extensions:
|
||||
# filters
|
||||
# -------
|
||||
|
||||
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = valid_extensions
|
||||
app.jinja_env.filters["megajson"] = megajson
|
||||
|
||||
|
||||
# assets
|
||||
# ------
|
||||
|
||||
assets = Environment(app)
|
||||
assets.url = app.static_url_path
|
||||
assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css"))
|
||||
|
||||
|
||||
# init
|
||||
# ----
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
def init():
|
||||
init_databases()
|
||||
@ -73,32 +89,6 @@ def init():
|
||||
# vvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
|
||||
|
||||
@app.route("/deletewallet")
|
||||
def deletewallet():
|
||||
user_id = request.args.get("usr")
|
||||
wallet_id = request.args.get("wal")
|
||||
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE wallets AS w
|
||||
SET
|
||||
user = 'del:' || w.user,
|
||||
adminkey = 'del:' || w.adminkey,
|
||||
inkey = 'del:' || w.inkey
|
||||
WHERE id = ? AND user = ?
|
||||
""",
|
||||
(wallet_id, user_id),
|
||||
)
|
||||
|
||||
next_wallet = db.fetchone("SELECT id FROM wallets WHERE user = ?", (user_id,))
|
||||
|
||||
if next_wallet:
|
||||
return redirect(url_for("wallet", usr=user_id, wal=next_wallet[0]))
|
||||
|
||||
return redirect(url_for("home"))
|
||||
|
||||
|
||||
@app.route("/lnurl")
|
||||
def lnurl():
|
||||
lnurl = request.args.get("lightning")
|
||||
@ -157,188 +147,20 @@ def lnurlwallet():
|
||||
return redirect(url_for("wallet", usr=user_id, wal=wallet_id))
|
||||
|
||||
|
||||
@app.route("/wallet")
|
||||
def wallet():
|
||||
usr = request.args.get("usr")
|
||||
wallet_id = request.args.get("wal")
|
||||
wallet_name = request.args.get("nme")
|
||||
|
||||
if usr:
|
||||
if not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
if wallet_id:
|
||||
if not len(wallet_id) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# just usr: return a the first user wallet or create one if none found
|
||||
# usr and wallet_id: return that wallet or create it if it doesn't exist
|
||||
# usr, wallet_id and wallet_name: same as above, but use the specified name
|
||||
# usr and wallet_name: generate a wallet_id and create
|
||||
# wallet_id and wallet_name: create a user, then move an existing wallet or create
|
||||
# just wallet_name: create a user, then generate a wallet_id and create
|
||||
# nothing: create everything
|
||||
|
||||
with open_db() as db:
|
||||
# ensure this user exists
|
||||
# -------------------------------
|
||||
|
||||
if not usr:
|
||||
usr = uuid.uuid4().hex
|
||||
return redirect(url_for("wallet", usr=usr, wal=wallet_id, nme=wallet_name))
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO accounts (id) VALUES (?)
|
||||
""",
|
||||
(usr,),
|
||||
)
|
||||
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
|
||||
if not wallet_id:
|
||||
if user_wallets and not wallet_name:
|
||||
# fetch the first wallet from this user
|
||||
# -------------------------------------
|
||||
wallet_id = user_wallets[0]["id"]
|
||||
else:
|
||||
# create for this user
|
||||
# --------------------
|
||||
wallet_name = wallet_name or DEFAULT_USER_WALLET_NAME
|
||||
wallet_id = uuid.uuid4().hex
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, name, user, adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet_id, wallet_name, usr, uuid.uuid4().hex, uuid.uuid4().hex),
|
||||
)
|
||||
|
||||
return redirect(url_for("wallet", usr=usr, wal=wallet_id, nme=wallet_name))
|
||||
|
||||
# if wallet_id is given, try to move it to this user or create
|
||||
# ------------------------------------------------------------
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO wallets (id, user, name, adminkey, inkey)
|
||||
VALUES (?, ?,
|
||||
coalesce((SELECT name FROM wallets WHERE id = ?), ?),
|
||||
coalesce((SELECT adminkey FROM wallets WHERE id = ?), ?),
|
||||
coalesce((SELECT inkey FROM wallets WHERE id = ?), ?)
|
||||
)
|
||||
""",
|
||||
(
|
||||
wallet_id,
|
||||
usr,
|
||||
wallet_id,
|
||||
wallet_name or DEFAULT_USER_WALLET_NAME,
|
||||
wallet_id,
|
||||
uuid.uuid4().hex,
|
||||
wallet_id,
|
||||
uuid.uuid4().hex,
|
||||
),
|
||||
)
|
||||
|
||||
# finally, get the wallet with balance and transactions
|
||||
# -----------------------------------------------------
|
||||
|
||||
wallet = db.fetchone(
|
||||
"""
|
||||
SELECT
|
||||
coalesce((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance,
|
||||
*
|
||||
FROM wallets
|
||||
WHERE user = ? AND id = ?
|
||||
""",
|
||||
(1 - FEE_RESERVE, usr, wallet_id),
|
||||
)
|
||||
|
||||
transactions = db.fetchall(
|
||||
"""
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE wallet = ? AND pending = 0
|
||||
ORDER BY time
|
||||
""",
|
||||
(wallet_id,),
|
||||
)
|
||||
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
return render_template(
|
||||
"wallet.html", user_wallets=user_wallets, wallet=wallet, user=usr, transactions=transactions, user_ext=user_ext
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/v1/invoices", methods=["GET", "POST"])
|
||||
def api_invoices():
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
postedjson = request.json
|
||||
|
||||
# Form validation
|
||||
if int(postedjson["value"]) < 0 or not postedjson["memo"].replace(" ", "").isalnum():
|
||||
return jsonify({"ERROR": "FORM ERROR"}), 401
|
||||
|
||||
if "value" not in postedjson:
|
||||
return jsonify({"ERROR": "NO VALUE"}), 400
|
||||
|
||||
if not postedjson["value"].isdigit():
|
||||
return jsonify({"ERROR": "VALUE MUST BE A NUMBER"}), 400
|
||||
|
||||
if int(postedjson["value"]) < 0:
|
||||
return jsonify({"ERROR": "AMOUNTLESS INVOICES NOT SUPPORTED"}), 400
|
||||
|
||||
if "memo" not in postedjson:
|
||||
return jsonify({"ERROR": "NO MEMO"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
wallet = db.fetchone(
|
||||
"SELECT id FROM wallets WHERE inkey = ? OR adminkey = ?",
|
||||
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"],),
|
||||
)
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"ERROR": "NO KEY"}), 200
|
||||
|
||||
r, pay_hash, pay_req = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
|
||||
|
||||
if not r.ok or "error" in r.json():
|
||||
return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500
|
||||
|
||||
amount_msat = int(postedjson["value"]) * 1000
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 1, ?)",
|
||||
(pay_hash, amount_msat, wallet["id"], postedjson["memo"],),
|
||||
)
|
||||
|
||||
return jsonify({"pay_req": pay_req, "payment_hash": pay_hash}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/channels/transactions", methods=["GET", "POST"])
|
||||
@api_validate_post_request(required_params=["payment_request"])
|
||||
def api_transactions():
|
||||
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
data = request.json
|
||||
|
||||
print(data)
|
||||
if "payment_request" not in data:
|
||||
return jsonify({"ERROR": "NO PAY REQ"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"ERROR": "BAD AUTH"}), 401
|
||||
return jsonify({"message": "BAD AUTH"}), 401
|
||||
|
||||
# decode the invoice
|
||||
invoice = bolt11.decode(data["payment_request"])
|
||||
invoice = bolt11.decode(g.data["payment_request"])
|
||||
if invoice.amount_msat == 0:
|
||||
return jsonify({"ERROR": "AMOUNTLESS INVOICES NOT SUPPORTED"}), 400
|
||||
return jsonify({"message": "AMOUNTLESS INVOICES NOT SUPPORTED"}), 400
|
||||
|
||||
# insert the payment
|
||||
db.execute(
|
||||
@ -356,7 +178,7 @@ def api_transactions():
|
||||
balance = db.fetchone("SELECT balance/1000 FROM balances WHERE wallet = ?", (wallet["id"],))[0]
|
||||
if balance < 0:
|
||||
db.execute("DELETE FROM apipayments WHERE payhash = ? AND wallet = ?", (invoice.payment_hash, wallet["id"]))
|
||||
return jsonify({"ERROR": "INSUFFICIENT BALANCE"}), 403
|
||||
return jsonify({"message": "INSUFFICIENT BALANCE"}), 403
|
||||
|
||||
# check if the invoice is an internal one
|
||||
if db.fetchone("SELECT count(*) FROM apipayments WHERE payhash = ?", (invoice.payment_hash,))[0] == 2:
|
||||
@ -364,10 +186,10 @@ def api_transactions():
|
||||
db.execute("UPDATE apipayments SET pending = 0, fee = 0 WHERE payhash = ?", (invoice.payment_hash,))
|
||||
else:
|
||||
# actually send the payment
|
||||
r = WALLET.pay_invoice(data["payment_request"])
|
||||
r = WALLET.pay_invoice(g.data["payment_request"])
|
||||
|
||||
if not r.raw_response.ok or r.failed:
|
||||
return jsonify({"ERROR": "UNEXPECTED PAYMENT ERROR"}), 500
|
||||
return jsonify({"message": "UNEXPECTED PAYMENT ERROR"}), 500
|
||||
|
||||
# payment went through, not pending anymore, save actual fees
|
||||
db.execute(
|
||||
@ -378,58 +200,6 @@ def api_transactions():
|
||||
return jsonify({"PAID": "TRUE", "payment_hash": invoice.payment_hash}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/invoice/<payhash>", methods=["GET"])
|
||||
def api_checkinvoice(payhash):
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
|
||||
with open_db() as db:
|
||||
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (payhash,))
|
||||
|
||||
if not payment:
|
||||
return jsonify({"ERROR": "NO INVOICE"}), 404
|
||||
|
||||
if not payment[0][4]: # pending
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
if not WALLET.get_invoice_status(payhash).settled:
|
||||
return jsonify({"PAID": "FALSE"}), 200
|
||||
|
||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/payment/<payhash>", methods=["GET"])
|
||||
def api_checkpayment(payhash):
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
payment = db.fetchone(
|
||||
"""
|
||||
SELECT pending
|
||||
FROM apipayments
|
||||
INNER JOIN wallets AS w ON apipayments.wallet = w.id
|
||||
WHERE payhash = ?
|
||||
AND (w.adminkey = ? OR w.inkey = ?)
|
||||
""",
|
||||
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
|
||||
)
|
||||
|
||||
if not payment:
|
||||
return jsonify({"ERROR": "NO INVOICE"}), 404
|
||||
|
||||
if not payment["pending"]: # pending
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
if not WALLET.get_payment_status(payhash).settled:
|
||||
return jsonify({"PAID": "FALSE"}), 200
|
||||
|
||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/checkpending", methods=["POST"])
|
||||
def api_checkpending():
|
||||
with open_db() as db:
|
||||
@ -465,39 +235,5 @@ def api_checkpending():
|
||||
return ""
|
||||
|
||||
|
||||
# Checks DB to see if the extensions are activated or not activated for the user
|
||||
@app.route("/extensions")
|
||||
def extensions():
|
||||
usr = request.args.get("usr")
|
||||
enable = request.args.get("enable")
|
||||
disable = request.args.get("disable")
|
||||
ext = None
|
||||
|
||||
if usr and not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
if enable and disable:
|
||||
# TODO: show some kind of error
|
||||
return redirect(url_for("extensions"))
|
||||
|
||||
with open_db() as db:
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
|
||||
if enable:
|
||||
ext, value = enable, 1
|
||||
if disable:
|
||||
ext, value = disable, 0
|
||||
|
||||
if ext:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO extensions (user, extension, active)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(usr, ext, value),
|
||||
)
|
||||
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
return render_template("extensions.html", user_wallets=user_wallets, user=usr, user_ext=user_ext)
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
|
@ -1,7 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
core_app = Blueprint("core", __name__, template_folder="templates")
|
||||
core_app = Blueprint("core", __name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
|
180
lnbits/core/crud.py
Normal file
@ -0,0 +1,180 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.db import open_db
|
||||
from lnbits.settings import DEFAULT_USER_WALLET_NAME, FEE_RESERVE
|
||||
from typing import List, Optional
|
||||
|
||||
from .models import User, Transaction, Wallet
|
||||
|
||||
|
||||
# accounts
|
||||
# --------
|
||||
|
||||
|
||||
def create_account() -> User:
|
||||
with open_db() as db:
|
||||
user_id = uuid4().hex
|
||||
db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
|
||||
|
||||
return get_account(user_id=user_id)
|
||||
|
||||
|
||||
def get_account(user_id: str) -> Optional[User]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone("SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,))
|
||||
|
||||
return User(**row) if row else None
|
||||
|
||||
|
||||
def get_user(user_id: str) -> Optional[User]:
|
||||
with open_db() as db:
|
||||
user = db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,))
|
||||
|
||||
if user:
|
||||
extensions = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
|
||||
wallets = db.fetchall(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance
|
||||
FROM wallets
|
||||
WHERE user = ?
|
||||
""",
|
||||
(1 - FEE_RESERVE, user_id),
|
||||
)
|
||||
|
||||
return (
|
||||
User(**{**user, **{"extensions": [e[0] for e in extensions], "wallets": [Wallet(**w) for w in wallets]}})
|
||||
if user
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
def update_user_extension(*, user_id: str, extension: str, active: int) -> None:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO extensions (user, extension, active)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(user_id, extension, active),
|
||||
)
|
||||
|
||||
|
||||
# wallets
|
||||
# -------
|
||||
|
||||
|
||||
def create_wallet(*, user_id: str, wallet_name: Optional[str]) -> Wallet:
|
||||
with open_db() as db:
|
||||
wallet_id = uuid4().hex
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, name, user, adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet_id, wallet_name or DEFAULT_USER_WALLET_NAME, user_id, uuid4().hex, uuid4().hex),
|
||||
)
|
||||
|
||||
return get_wallet(wallet_id=wallet_id)
|
||||
|
||||
|
||||
def delete_wallet(*, user_id: str, wallet_id: str) -> None:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE wallets AS w
|
||||
SET
|
||||
user = 'del:' || w.user,
|
||||
adminkey = 'del:' || w.adminkey,
|
||||
inkey = 'del:' || w.inkey
|
||||
WHERE id = ? AND user = ?
|
||||
""",
|
||||
(wallet_id, user_id),
|
||||
)
|
||||
|
||||
|
||||
def get_wallet(wallet_id: str) -> Optional[Wallet]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance
|
||||
FROM wallets
|
||||
WHERE id = ?
|
||||
""",
|
||||
(1 - FEE_RESERVE, wallet_id),
|
||||
)
|
||||
|
||||
return Wallet(**row) if row else None
|
||||
|
||||
|
||||
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||
with open_db() as db:
|
||||
check_field = "adminkey" if key_type == "admin" else "inkey"
|
||||
row = db.fetchone(
|
||||
f"""
|
||||
SELECT *, COALESCE((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance
|
||||
FROM wallets
|
||||
WHERE {check_field} = ?
|
||||
""",
|
||||
(1 - FEE_RESERVE, key),
|
||||
)
|
||||
|
||||
return Wallet(**row) if row else None
|
||||
|
||||
|
||||
# wallet transactions
|
||||
# -------------------
|
||||
|
||||
|
||||
def get_wallet_transaction(wallet_id: str, payhash: str) -> Optional[Transaction]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone(
|
||||
"""
|
||||
SELECT payhash, amount, fee, pending, memo, time
|
||||
FROM apipayments
|
||||
WHERE wallet = ? AND payhash = ?
|
||||
""",
|
||||
(wallet_id, payhash),
|
||||
)
|
||||
|
||||
return Transaction(**row) if row else None
|
||||
|
||||
|
||||
def get_wallet_transactions(wallet_id: str, *, pending: bool = False) -> List[Transaction]:
|
||||
with open_db() as db:
|
||||
rows = db.fetchall(
|
||||
"""
|
||||
SELECT payhash, amount, fee, pending, memo, time
|
||||
FROM apipayments
|
||||
WHERE wallet = ? AND pending = ?
|
||||
ORDER BY time DESC
|
||||
""",
|
||||
(wallet_id, int(pending)),
|
||||
)
|
||||
|
||||
return [Transaction(**row) for row in rows]
|
||||
|
||||
|
||||
# transactions
|
||||
# ------------
|
||||
|
||||
|
||||
def create_transaction(*, wallet_id: str, payhash: str, amount: str, memo: str) -> Transaction:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO apipayments (wallet, payhash, amount, pending, memo)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet_id, payhash, amount, 1, memo),
|
||||
)
|
||||
|
||||
return get_wallet_transaction(wallet_id, payhash)
|
||||
|
||||
|
||||
def update_transaction_status(payhash: str, pending: bool) -> None:
|
||||
with open_db() as db:
|
||||
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), payhash,))
|
||||
|
||||
|
||||
def check_pending_transactions(wallet_id: str) -> None:
|
||||
pass
|
62
lnbits/core/models.py
Normal file
@ -0,0 +1,62 @@
|
||||
from decimal import Decimal
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
|
||||
class User(NamedTuple):
|
||||
id: str
|
||||
email: str
|
||||
extensions: Optional[List[str]] = []
|
||||
wallets: Optional[List["Wallet"]] = []
|
||||
password: Optional[str] = None
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> List[str]:
|
||||
return [wallet.id for wallet in self.wallets]
|
||||
|
||||
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
|
||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||
return w[0] if w else None
|
||||
|
||||
|
||||
class Wallet(NamedTuple):
|
||||
id: str
|
||||
name: str
|
||||
user: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
balance: Decimal
|
||||
|
||||
def get_transaction(self, payhash: str) -> "Transaction":
|
||||
from .crud import get_wallet_transaction
|
||||
|
||||
return get_wallet_transaction(self.id, payhash)
|
||||
|
||||
def get_transactions(self) -> List["Transaction"]:
|
||||
from .crud import get_wallet_transactions
|
||||
|
||||
return get_wallet_transactions(self.id)
|
||||
|
||||
|
||||
class Transaction(NamedTuple):
|
||||
payhash: str
|
||||
pending: bool
|
||||
amount: int
|
||||
fee: int
|
||||
memo: str
|
||||
time: int
|
||||
|
||||
@property
|
||||
def msat(self) -> int:
|
||||
return self.amount
|
||||
|
||||
@property
|
||||
def sat(self) -> int:
|
||||
return self.amount / 1000
|
||||
|
||||
@property
|
||||
def tx_type(self) -> str:
|
||||
return "payment" if self.amount < 0 else "invoice"
|
||||
|
||||
def set_pending(self, pending: bool) -> None:
|
||||
from .crud import update_transaction_status
|
||||
update_transaction_status(self.payhash, pending)
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
4
lnbits/core/static/js/extensions.js
Normal file
@ -0,0 +1,4 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin]
|
||||
});
|
14
lnbits/core/static/js/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
walletName: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
createWallet: function () {
|
||||
LNbits.href.createWallet(this.walletName);
|
||||
}
|
||||
}
|
||||
});
|
137
lnbits/core/static/js/wallet.js
Normal file
@ -0,0 +1,137 @@
|
||||
Vue.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
txUpdate: null,
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null,
|
||||
data: {
|
||||
amount: null,
|
||||
memo: ''
|
||||
}
|
||||
},
|
||||
send: {
|
||||
show: false,
|
||||
invoice: null,
|
||||
data: {
|
||||
bolt11: ''
|
||||
}
|
||||
},
|
||||
transactionsTable: {
|
||||
columns: [
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
||||
{name: 'sat', align: 'right', label: 'Amount (sat)', field: 'sat', sortable: true}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canPay: function () {
|
||||
if (!this.send.invoice) return false;
|
||||
return this.send.invoice.sat < this.w.wallet.balance;
|
||||
},
|
||||
transactions: function () {
|
||||
var data = (this.txUpdate) ? this.txUpdate : this.w.transactions;
|
||||
return data.sort(function (a, b) {
|
||||
return b.time - a.time;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openReceiveDialog: function () {
|
||||
this.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: null,
|
||||
data: {
|
||||
amount: null,
|
||||
memo: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
openSendDialog: function () {
|
||||
this.send = {
|
||||
show: true,
|
||||
invoice: null,
|
||||
data: {
|
||||
bolt11: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
createInvoice: function () {
|
||||
var self = this;
|
||||
this.receive.status = 'loading';
|
||||
LNbits.api.createInvoice(this.w.wallet, this.receive.data.amount, this.receive.data.memo)
|
||||
.then(function (response) {
|
||||
self.receive.status = 'success';
|
||||
self.receive.paymentReq = response.data.payment_request;
|
||||
|
||||
var check_invoice = setInterval(function () {
|
||||
LNbits.api.getInvoice(self.w.wallet, response.data.payment_hash).then(function (response) {
|
||||
if (response.data.paid) {
|
||||
self.refreshTransactions();
|
||||
self.receive.show = false;
|
||||
clearInterval(check_invoice);
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
self.receive.status = 'pending';
|
||||
});
|
||||
},
|
||||
decodeInvoice: function () {
|
||||
try {
|
||||
var invoice = decode(this.send.data.bolt11);
|
||||
} catch (err) {
|
||||
this.$q.notify({type: 'warning', message: err});
|
||||
return;
|
||||
}
|
||||
|
||||
var 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, function (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.send.invoice = Object.freeze(cleanInvoice);
|
||||
},
|
||||
payInvoice: function () {
|
||||
alert('pay!');
|
||||
},
|
||||
deleteWallet: function (walletId, user) {
|
||||
LNbits.href.deleteWallet(walletId, user);
|
||||
},
|
||||
refreshTransactions: function (notify) {
|
||||
var self = this;
|
||||
|
||||
LNbits.api.getTransactions(this.w.wallet).then(function (response) {
|
||||
self.txUpdate = response.data.map(function (obj) {
|
||||
return LNbits.map.transaction(obj);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
42
lnbits/core/templates/core/extensions.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/extensions.js',
|
||||
'core/js/extensions.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-6 col-md-4 col-lg-3" v-for="extension in w.extensions" :key="extension.code">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-icon :name="extension.icon" color="grey-5" style="font-size: 4rem;"></q-icon>
|
||||
{% raw %}
|
||||
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
|
||||
{{ extension.shortDescription }}
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-actions>
|
||||
<div v-if="extension.isEnabled">
|
||||
<q-btn flat color="deep-purple"
|
||||
type="a" :href="[extension.url, '?usr=', w.user.id].join('')">Open</q-btn>
|
||||
<q-btn flat color="grey-5"
|
||||
type="a"
|
||||
:href="['{{ url_for('core.extensions') }}', '?usr=', w.user.id, '&disable=', extension.code].join('')"> Disable</q-btn>
|
||||
</div>
|
||||
<q-btn v-else flat color="deep-purple"
|
||||
type="a"
|
||||
:href="['{{ url_for('core.extensions') }}', '?usr=', w.user.id, '&enable=', extension.code].join('')">
|
||||
Enable</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
87
lnbits/core/templates/core/index.html
Normal file
@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/index.js',
|
||||
'core/js/index.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block drawer %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md justify-between">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model="walletName"
|
||||
label="Name your LNbits wallet *"
|
||||
></q-input>
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="walletName == ''"
|
||||
@click="createWallet">Add a new wallet</q-btn>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none"><strong>LN</strong>bits</h3>
|
||||
<h5 class="q-my-md">Free and open-source lightning wallet</h5>
|
||||
<p>LNbits is a simple, free and open-source lightning-network
|
||||
wallet for bits and bobs. You can run it on your own server,
|
||||
or use it at lnbits.com.</p>
|
||||
<p>The wallet can be used in a variety of ways: an instant wallet for
|
||||
LN demonstrations, a fallback wallet for the LNURL scheme, an
|
||||
accounts system to mitigate the risk of exposing applications to
|
||||
your full balance.</p>
|
||||
<p>The wallet can run on top of LND, LNPay, @lntxbot or OpenNode.</p>
|
||||
<p>Please note that although one of the aims of this wallet is to
|
||||
mitigate exposure of all your funds, it’s still very BETA and may,
|
||||
in fact, do the opposite!</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat
|
||||
color="deep-purple"
|
||||
type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
|
||||
<q-btn flat
|
||||
color="deep-purple"
|
||||
type="a" href="https://paywall.link/to/f4e4e" target="_blank" rel="noopener">Donate</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Ads -->
|
||||
<div class="col-12 col-md-3 col-lg-3 q-gutter-y-md">
|
||||
<q-btn flat color="deep-purple" label="Advertise here!" type="a" href="mailto:ben@arc.wales" class="full-width"></q-btn>
|
||||
<div>
|
||||
<a href="https://where39.com/">
|
||||
<q-img :ratio="16/9" src="{{ url_for('static', filename='images/where39.png') }}" class="rounded-borders">
|
||||
<div class="absolute-top text-center">Where39 anon locations</div>
|
||||
</q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/arcbtc/Quickening">
|
||||
<q-img :ratio="16/9" src="{{ url_for('static', filename='images/quick.gif') }}" class="rounded-borders">
|
||||
<div class="absolute-top text-center">The Quickening <$8 PoS</div>
|
||||
</q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://jigawatt.co/">
|
||||
<q-img :ratio="16/9" src="{{ url_for('static', filename='images/stamps.jpg') }}" class="rounded-borders">
|
||||
<div class="absolute-top text-center">Buy BTC stamps + electronics</div>
|
||||
</q-img>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
252
lnbits/core/templates/core/wallet.html
Normal file
@ -0,0 +1,252 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user, wallet) }}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/wallet.js',
|
||||
'vendor/bolt11/utils.js',
|
||||
'vendor/bolt11/decoder.js',
|
||||
'vendor/vue-qrcode@1.0.2/vue-qrcode.min.js',
|
||||
'core/js/wallet.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% 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>
|
||||
<h4 class="q-my-none"><strong>{% raw %}{{ w.wallet.fsat }}{% endraw %}</strong> sat</h4>
|
||||
</q-card-section>
|
||||
<div class="row q-pb-md q-px-md q-col-gutter-md">
|
||||
<div class="col">
|
||||
<q-btn unelevated
|
||||
color="purple"
|
||||
class="full-width"
|
||||
@click="openSendDialog">Send</q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
class="full-width"
|
||||
@click="openReceiveDialog">Receive</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</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">Transactions</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" onclick="exportbut()">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table dense flat
|
||||
:data="transactions"
|
||||
row-key="payhash"
|
||||
:columns="transactionsTable.columns"
|
||||
:pagination.sync="transactionsTable.pagination">
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props">{{ col.label }}</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="lnbits__q-table__icon-td">
|
||||
<q-icon size="14px"
|
||||
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'"
|
||||
:color="(props.row.sat < 0) ? 'purple-5' : 'green'"></q-icon>
|
||||
</q-td>
|
||||
<q-td key="memo" :props="props">
|
||||
{{ props.row.memo }}
|
||||
</q-td>
|
||||
<q-td auto-width key="date" :props="props">
|
||||
{{ props.row.date }}
|
||||
</q-td>
|
||||
<q-td auto-width key="sat" :props="props">
|
||||
{{ props.row.fsat }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div id="satschart"></div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br>
|
||||
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br>
|
||||
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br>
|
||||
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" expand-separator label="Create an invoice">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
Generate an invoice:<br /><code>POST /api/v1/invoices</code
|
||||
><br />Header
|
||||
<code
|
||||
>{"Grpc-Metadata-macaroon": "<i>{{ wallet.inkey }}</i
|
||||
>"}</code
|
||||
><br />
|
||||
Body <code>{"value": "200","memo": "beer"} </code><br />
|
||||
Returns
|
||||
<code>{"pay_req": string,"pay_id": string} </code><br />
|
||||
*payment will not register in the wallet until the "check
|
||||
invoice" endpoint is used<br /><br />
|
||||
|
||||
Check invoice:<br />
|
||||
Check an invoice:<br /><code
|
||||
>GET /api/v1/invoice/*payment_hash*</code
|
||||
><br />Header
|
||||
<code
|
||||
>{"Grpc-Metadata-macaroon": "<i>{{ wallet.inkey }}</i
|
||||
>"}</code
|
||||
><br />
|
||||
|
||||
Returns
|
||||
<code>{"PAID": "TRUE"}/{"PAID": "FALSE"} </code><br />
|
||||
*if using LNTXBOT return will hang until paid<br /><br />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" expand-separator label="Get an invoice">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
This whole wallet will be deleted, the funds will be <strong>UNRECOVERABLE</strong>.
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="remove_circle"
|
||||
label="Delete wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>This whole wallet will be deleted, the funds will be <strong>UNRECOVERABLE</strong>.</p>
|
||||
<q-btn unelevated
|
||||
color="red-10"
|
||||
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')">Delete wallet</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" :position="($q.screen.gt.sm) ? 'standard' : 'top'">
|
||||
<q-card class="q-pa-md" style="width: 500px">
|
||||
<q-form v-if="!receive.paymentReq" class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model.number="receive.data.amount"
|
||||
type="number"
|
||||
label="Amount *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model="receive.data.memo"
|
||||
label="Memo"
|
||||
placeholder="LNbits invoice"></q-input>
|
||||
<div v-if="receive.status == 'pending'" class="row justify-between">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||
@click="createInvoice">Create invoice</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
<q-spinner v-if="receive.status == 'loading'" color="deep-purple" size="2.55em"></q-spinner>
|
||||
</q-form>
|
||||
<div v-else>
|
||||
<div class="text-center q-mb-md">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<qrcode :value="receive.paymentReq" :options="{width: 340}"></qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<!--<q-separator class="q-my-md"></q-separator>
|
||||
<p class="text-caption" style="word-break: break-all">
|
||||
{% raw %}{{ receive.paymentReq }}{% endraw %}
|
||||
</p>-->
|
||||
<div class="row justify-between">
|
||||
<q-btn flat color="grey" @click="copyText(receive.paymentReq)">Copy invoice</q-btn>
|
||||
<q-btn v-close-popup flat color="grey">Close</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="send.show" :position="($q.screen.gt.sm) ? 'standard' : 'top'">
|
||||
<q-card class="q-pa-md" style="width: 500px">
|
||||
<q-form v-if="!send.invoice" class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model="send.data.bolt11"
|
||||
type="textarea"
|
||||
label="Paste an invoice *"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn round dense flat icon="photo_camera">
|
||||
<q-tooltip>Use camera to scan an invoice</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
<div class="row justify-between">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="send.data.bolt11 == ''"
|
||||
@click="decodeInvoice">Read invoice</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
<div v-else>
|
||||
{% raw %}
|
||||
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
|
||||
<p style="word-break: break-all">
|
||||
<strong>Memo:</strong> {{ send.invoice.description }}<br>
|
||||
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br>
|
||||
<strong>Hash:</strong> {{ send.invoice.hash }}
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div v-if="canPay" class="row justify-between">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
@click="payInvoice">Send satoshis</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
<div v-else class="row justify-between">
|
||||
<q-btn unelevated disabled color="yellow" text-color="black">Not enough funds!</q-btn>
|
||||
<q-btn v-close-popup flat color="grey">Cancel</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endblock %}
|
@ -1,98 +0,0 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block menuitems %}
|
||||
|
||||
<li><a href="https://where39.com/"><p>Where39 anon locations</p><img src="static/where39.png" style="width:170px"></a></li>
|
||||
<li><a href="https://github.com/arcbtc/Quickening"><p>The Quickening <$8 PoS</p><img src="static/quick.gif" style="width:170px"></a></li>
|
||||
<li><a href="http://jigawatt.co/"><p>Buy BTC stamps + electronics</p><img src="static/stamps.jpg" style="width:170px"></a></li>
|
||||
<li><a href="mailto:ben@arc.wales"><h3>Advertise here!</h3></a></li>
|
||||
{% endblock %} {% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="{{ url_for('core.home') }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
</ol>
|
||||
<br /><br />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<h4>
|
||||
TESTING ONLY - wallet is still in BETA and very unstable
|
||||
</h4>
|
||||
</div></div></div>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<!-- Default box -->
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
{% block call_to_action %}
|
||||
<h1>
|
||||
<small>Make a wallet</small>
|
||||
</h1>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="walname"
|
||||
placeholder="Name your LNBits wallet"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="newwallet()">
|
||||
Submit
|
||||
</button>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Default box -->
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h1>
|
||||
<a href="index.html" class="logo"><b>LN</b>bits</a>
|
||||
<small>free and open-source lightning wallet</small>
|
||||
</h1>
|
||||
<p>
|
||||
LNbits is a simple, free and open-source lightning-network wallet
|
||||
for bits and bobs. You can run it on your own server, or use this
|
||||
one.
|
||||
<br /><br />
|
||||
The wallet can be used in a variety of ways, an instant wallet for
|
||||
LN demonstrations, a fallback wallet for the LNURL scheme, an
|
||||
accounts system to mitigate the risk of exposing applications to
|
||||
your full balance.
|
||||
<br /><br />
|
||||
The wallet can run on top of LND, lntxbot, paywall, opennode
|
||||
<br /><br />
|
||||
Please note that although one of the aims of this wallet is to
|
||||
mitigate exposure of all your funds, it’s still very BETA and may
|
||||
in fact do the opposite!
|
||||
<br />
|
||||
<a href="https://github.com/arcbtc/lnbits"
|
||||
>https://github.com/arcbtc/lnbits</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
{% endblock %}
|
@ -1,7 +1,17 @@
|
||||
from flask import render_template, send_from_directory
|
||||
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
|
||||
from os import path
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from .crud import (
|
||||
create_account,
|
||||
get_user,
|
||||
update_user_extension,
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
)
|
||||
|
||||
|
||||
@core_app.route("/favicon.ico")
|
||||
@ -11,4 +21,71 @@ def favicon():
|
||||
|
||||
@core_app.route("/")
|
||||
def home():
|
||||
return render_template("index.html")
|
||||
return render_template("core/index.html")
|
||||
|
||||
|
||||
@core_app.route("/extensions")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def extensions():
|
||||
extension_to_enable = request.args.get("enable", type=str)
|
||||
extension_to_disable = request.args.get("disable", type=str)
|
||||
|
||||
if extension_to_enable and extension_to_disable:
|
||||
abort(Status.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||
|
||||
if extension_to_enable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
|
||||
elif extension_to_disable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
|
||||
|
||||
return render_template("core/extensions.html", user=get_user(g.user.id))
|
||||
|
||||
|
||||
@core_app.route("/wallet")
|
||||
@validate_uuids(["usr", "wal"])
|
||||
def wallet():
|
||||
user_id = request.args.get("usr", type=str)
|
||||
wallet_id = request.args.get("wal", type=str)
|
||||
wallet_name = request.args.get("nme", type=str)
|
||||
|
||||
# just wallet_name: create a new user, then create a new wallet for user with wallet_name
|
||||
# just user_id: return the first user wallet or create one if none found (with default wallet_name)
|
||||
# user_id and wallet_name: create a new wallet for user with wallet_name
|
||||
# user_id and wallet_id: return that wallet if user is the owner
|
||||
# nothing: create everything
|
||||
|
||||
if not user_id:
|
||||
user = get_user(create_account().id)
|
||||
else:
|
||||
user = get_user(user_id) or abort(Status.NOT_FOUND, "User does not exist.")
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name:
|
||||
wallet = user.wallets[0]
|
||||
else:
|
||||
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
|
||||
if wallet_id not in user.wallet_ids:
|
||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||
|
||||
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id))
|
||||
|
||||
|
||||
@core_app.route("/deletewallet")
|
||||
@validate_uuids(["usr", "wal"], required=True)
|
||||
@check_user_exists()
|
||||
def deletewallet():
|
||||
wallet_id = request.args.get("wal", type=str)
|
||||
|
||||
if wallet_id not in g.user.wallet_ids:
|
||||
abort(Status.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
||||
|
||||
if g.user.wallets:
|
||||
return redirect(url_for("core.wallet", usr=g.user.id, wal=g.user.wallets[0].id))
|
||||
|
||||
return redirect(url_for("core.home"))
|
||||
|
@ -0,0 +1,62 @@
|
||||
from flask import g, jsonify
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from .crud import create_transaction
|
||||
|
||||
|
||||
@core_app.route("/api/v1/invoices", methods=["POST"])
|
||||
@api_validate_post_request(required_params=["amount", "memo"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_invoices():
|
||||
if not isinstance(g.data["amount"], int) or g.data["amount"] < 1:
|
||||
return jsonify({"message": "`amount` needs to be a positive integer."}), Status.BAD_REQUEST
|
||||
|
||||
if not isinstance(g.data["memo"], str) or not g.data["memo"].strip():
|
||||
return jsonify({"message": "`memo` needs to be a valid string."}), Status.BAD_REQUEST
|
||||
|
||||
try:
|
||||
r, payhash, payment_request = WALLET.create_invoice(g.data["amount"], g.data["memo"])
|
||||
server_error = not r.ok or "message" in r.json()
|
||||
except Exception:
|
||||
server_error = True
|
||||
|
||||
if server_error:
|
||||
return jsonify({"message": "Unexpected backend error. Try again later."}), 500
|
||||
|
||||
amount_msat = g.data["amount"] * 1000
|
||||
create_transaction(wallet_id=g.wallet.id, payhash=payhash, amount=amount_msat, memo=g.data["memo"])
|
||||
|
||||
return jsonify({"payment_request": payment_request, "payment_hash": payhash}), Status.CREATED
|
||||
|
||||
|
||||
@core_app.route("/api/v1/invoices/<payhash>", defaults={"incoming": True}, methods=["GET"])
|
||||
@core_app.route("/api/v1/payments/<payhash>", defaults={"incoming": False}, methods=["GET"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_transaction(payhash, incoming):
|
||||
tx = g.wallet.get_transaction(payhash)
|
||||
|
||||
if not tx:
|
||||
return jsonify({"message": "Transaction does not exist."}), Status.NOT_FOUND
|
||||
elif not tx.pending:
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
|
||||
try:
|
||||
is_settled = WALLET.get_invoice_status(payhash).settled
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
|
||||
if is_settled is True:
|
||||
tx.set_pending(False)
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
|
||||
|
||||
@core_app.route("/api/v1/transactions", methods=["GET"])
|
||||
@api_check_wallet_macaroon(key_type="invoice")
|
||||
def api_transactions():
|
||||
return jsonify(g.wallet.get_transactions()), Status.OK
|
2
lnbits/data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
@ -48,11 +48,12 @@ def init_databases() -> None:
|
||||
"""TODO: see how we can deal with migrations."""
|
||||
|
||||
schemas = [
|
||||
("database", os.path.join(LNBITS_PATH, "data", "schema.sql")),
|
||||
("database", os.path.join(LNBITS_PATH, "core", "schema.sql")),
|
||||
]
|
||||
|
||||
for extension in ExtensionManager().extensions:
|
||||
schemas.append((f"ext_{extension.code}", os.path.join(extension.path, "schema.sql")))
|
||||
extension_path = os.path.join(LNBITS_PATH, "extensions", extension.code)
|
||||
schemas.append((f"ext_{extension.code}", os.path.join(extension_path, "schema.sql")))
|
||||
|
||||
for schema in [s for s in schemas if os.path.exists(s[1])]:
|
||||
with open_db(schema[0]) as db:
|
||||
|
81
lnbits/decorators.py
Normal file
@ -0,0 +1,81 @@
|
||||
from flask import g, abort, jsonify, request
|
||||
from functools import wraps
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from .helpers import Status
|
||||
|
||||
|
||||
def api_check_wallet_macaroon(*, key_type: str = "invoice"):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
try:
|
||||
g.wallet = get_wallet_for_key(request.headers["Grpc-Metadata-macaroon"], key_type)
|
||||
except KeyError:
|
||||
return jsonify({"message": "`Grpc-Metadata-macaroon` header missing."}), Status.BAD_REQUEST
|
||||
|
||||
if not g.wallet:
|
||||
return jsonify({"message": "Wrong keys."}), Status.UNAUTHORIZED
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def api_validate_post_request(*, required_params: List[str] = []):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
if "application/json" not in request.headers["Content-Type"]:
|
||||
return jsonify({"message": "Content-Type must be `application/json`."}), Status.BAD_REQUEST
|
||||
|
||||
g.data = request.json
|
||||
|
||||
for param in required_params:
|
||||
if param not in g.data:
|
||||
return jsonify({"message": f"`{param}` is required."}), Status.BAD_REQUEST
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def check_user_exists(param: str = "usr"):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
g.user = get_user(request.args.get(param, type=str)) or abort(Status.NOT_FOUND, "User not found.")
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
query_params = {param: request.args.get(param, type=str) for param in params}
|
||||
|
||||
for param, value in query_params.items():
|
||||
if not value and (required is True or (required and param in required)):
|
||||
abort(Status.BAD_REQUEST, f"`{param}` is required.")
|
||||
|
||||
if value:
|
||||
try:
|
||||
UUID(value, version=version)
|
||||
except ValueError:
|
||||
abort(Status.BAD_REQUEST, f"`{param}` is not a valid UUID.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "LNEVENTS",
|
||||
"short_description": "LN tickets for events",
|
||||
"ion_icon": "calendar"
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
@ -42,7 +42,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
@ -63,7 +63,7 @@
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">Lightning tickets</i>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "SHORT-NAME-FOR-EXTENSIONS-PAGE",
|
||||
"short_description": "BLah blah blah.",
|
||||
"ion_icon": "calendar"
|
||||
"icon": "calendar",
|
||||
"contributors": ["github_username"]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
@ -43,7 +43,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
@ -63,7 +63,7 @@
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">example</i>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "TPOS",
|
||||
"short_description": "A shareable POS!",
|
||||
"ion_icon": "calculator"
|
||||
"icon": "dialpad",
|
||||
"contributors": ["talvasconcelos", "arcbtc"]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
@ -43,7 +43,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
@ -63,7 +63,7 @@
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">example</i>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "LNURLw",
|
||||
"short_description": "Make LNURL withdraw links",
|
||||
"ion_icon": "beer"
|
||||
"short_description": "Make LNURL withdraw links.",
|
||||
"icon": "crop_free",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
@ -41,7 +41,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
@ -61,7 +61,7 @@
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">Withdraw link maker</i>
|
||||
|
@ -2,18 +2,26 @@ import json
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import List
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
from .settings import LNBITS_PATH
|
||||
|
||||
|
||||
class Extension(NamedTuple):
|
||||
code: str
|
||||
is_valid: bool
|
||||
name: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
contributors: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ExtensionManager:
|
||||
def __init__(self):
|
||||
self._extension_folders: List[str] = [x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions"))][0]
|
||||
|
||||
@property
|
||||
def extensions(self) -> List[SimpleNamespace]:
|
||||
def extensions(self) -> List[Extension]:
|
||||
output = []
|
||||
|
||||
for extension in self._extension_folders:
|
||||
@ -25,18 +33,24 @@ class ExtensionManager:
|
||||
config = {}
|
||||
is_valid = False
|
||||
|
||||
output.append(SimpleNamespace(**{
|
||||
**{
|
||||
"code": extension,
|
||||
"is_valid": is_valid,
|
||||
"path": os.path.join(LNBITS_PATH, "extensions", extension),
|
||||
},
|
||||
**config
|
||||
}))
|
||||
output.append(Extension(**{**{"code": extension, "is_valid": is_valid}, **config}))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class Status:
|
||||
OK = 200
|
||||
CREATED = 201
|
||||
NO_CONTENT = 204
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
PAYMENT_REQUIRED = 402
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
TOO_MANY_REQUESTS = 429
|
||||
METHOD_NOT_ALLOWED = 405
|
||||
|
||||
|
||||
class MegaEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, sqlite3.Row):
|
||||
|
@ -11,5 +11,5 @@ WALLET = OpenNodeWallet(endpoint=os.getenv("OPENNODE_API_ENDPOINT"),admin_key=os
|
||||
LNBITS_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
LNBITS_DATA_FOLDER = os.getenv("LNBITS_DATA_FOLDER", os.path.join(LNBITS_PATH, "data"))
|
||||
|
||||
DEFAULT_USER_WALLET_NAME = os.getenv("DEFAULT_USER_WALLET_NAME", "Bitcoin LN Wallet")
|
||||
DEFAULT_USER_WALLET_NAME = os.getenv("DEFAULT_USER_WALLET_NAME", "LNbits wallet")
|
||||
FEE_RESERVE = float(os.getenv("FEE_RESERVE", 0))
|
||||
|
33
lnbits/static/css/base.css
Normal file
@ -0,0 +1,33 @@
|
||||
[v-cloak] {
|
||||
display: none; }
|
||||
|
||||
.bg-lnbits-dark {
|
||||
background-color: #1f2234; }
|
||||
|
||||
body.body--dark, body.body--dark .q-drawer--dark, body.body--dark .q-menu--dark {
|
||||
background: #1f2234; }
|
||||
|
||||
body.body--dark .q-card--dark {
|
||||
background: #333646; }
|
||||
|
||||
body.body--dark .q-table--dark {
|
||||
background: transparent; }
|
||||
|
||||
body.body--light, body.body--light .q-drawer {
|
||||
background: whitesmoke; }
|
||||
|
||||
body.body--dark .q-field--error .text-negative,
|
||||
body.body--dark .q-field--error .q-field__messages {
|
||||
color: yellow !important; }
|
||||
|
||||
.lnbits__q-table__icon-td {
|
||||
padding-left: 5px !important; }
|
||||
|
||||
.lnbits-drawer__q-list .q-item {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 5px !important;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px; }
|
||||
.lnbits-drawer__q-list .q-item.q-item--active {
|
||||
color: inherit;
|
||||
font-weight: bold; }
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
BIN
lnbits/static/images/quick.gif
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
lnbits/static/images/stamps.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
lnbits/static/images/where39.png
Normal file
After Width: | Height: | Size: 230 KiB |
146
lnbits/static/js/base.js
Normal file
@ -0,0 +1,146 @@
|
||||
var LOCALE = 'en'
|
||||
|
||||
var LNbits = {
|
||||
api: {
|
||||
request: function (method, url, macaroon, data) {
|
||||
return axios({
|
||||
method: method,
|
||||
url: url,
|
||||
headers: {
|
||||
'Grpc-Metadata-macaroon': macaroon
|
||||
},
|
||||
data: data
|
||||
});
|
||||
},
|
||||
createInvoice: function (wallet, amount, memo) {
|
||||
return this.request('post', '/api/v1/invoices', wallet.inkey, {
|
||||
amount: amount,
|
||||
memo: memo
|
||||
});
|
||||
},
|
||||
getInvoice: function (wallet, payhash) {
|
||||
return this.request('get', '/api/v1/invoices/' + payhash, wallet.inkey);
|
||||
},
|
||||
getTransactions: function (wallet) {
|
||||
return this.request('get', '/api/v1/transactions', wallet.inkey);
|
||||
}
|
||||
},
|
||||
href: {
|
||||
openWallet: function (wallet) {
|
||||
window.location.href = '/wallet?usr=' + wallet.user + '&wal=' + wallet.id;
|
||||
},
|
||||
createWallet: function (walletName, userId) {
|
||||
window.location.href = '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName;
|
||||
},
|
||||
deleteWallet: function (walletId, userId) {
|
||||
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId;
|
||||
}
|
||||
},
|
||||
map: {
|
||||
extension: function (data) {
|
||||
var obj = _.object(['code', 'isValid', 'name', 'shortDescription', 'icon'], data);
|
||||
obj.url = ['/', obj.code, '/'].join('');
|
||||
return obj;
|
||||
},
|
||||
transaction: function (data) {
|
||||
var obj = _.object(['payhash', 'pending', 'amount', 'fee', 'memo', 'time'], data);
|
||||
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm')
|
||||
obj.msat = obj.amount;
|
||||
obj.sat = obj.msat / 1000;
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat);
|
||||
return obj;
|
||||
},
|
||||
user: function (data) {
|
||||
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data);
|
||||
var mapWallet = this.wallet;
|
||||
obj.wallets = obj.wallets.map(function (obj) {
|
||||
return mapWallet(obj);
|
||||
}).sort(function (a, b) {
|
||||
return a.name > b.name;
|
||||
});
|
||||
return obj;
|
||||
},
|
||||
wallet: function (data) {
|
||||
var obj = _.object(['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], data);
|
||||
obj.sat = Math.round(obj.balance);
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat);
|
||||
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('');
|
||||
return obj;
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
formatSat: function (value) {
|
||||
return new Intl.NumberFormat(LOCALE).format(value);
|
||||
},
|
||||
notifyApiError: function (error) {
|
||||
var types = {
|
||||
400: 'warning',
|
||||
401: 'warning',
|
||||
500: 'negative'
|
||||
}
|
||||
Quasar.plugins.Notify.create({
|
||||
progress: true,
|
||||
timeout: 3000,
|
||||
type: types[error.response.status] || 'warning',
|
||||
message: error.response.data.message || null,
|
||||
caption: [error.response.status, ' ', error.response.statusText].join('') || null,
|
||||
icon: null
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var windowMixin = {
|
||||
data: function () {
|
||||
return {
|
||||
w: {
|
||||
visibleDrawer: false,
|
||||
extensions: [],
|
||||
user: null,
|
||||
wallet: null,
|
||||
transactions: [],
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleDarkMode: function () {
|
||||
this.$q.dark.toggle();
|
||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive);
|
||||
},
|
||||
copyText: function (text, message) {
|
||||
var notify = this.$q.notify;
|
||||
Quasar.utils.copyToClipboard(text).then(function () {
|
||||
notify({message: 'Copied to clipboard!'});
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'));
|
||||
if (window.user) {
|
||||
this.w.user = Object.freeze(LNbits.map.user(window.user));
|
||||
}
|
||||
if (window.wallet) {
|
||||
this.w.wallet = Object.freeze(LNbits.map.wallet(window.wallet));
|
||||
}
|
||||
if (window.transactions) {
|
||||
this.w.transactions = window.transactions.map(function (data) {
|
||||
return LNbits.map.transaction(data);
|
||||
});
|
||||
}
|
||||
if (window.extensions) {
|
||||
var user = this.w.user;
|
||||
this.w.extensions = Object.freeze(window.extensions.map(function (data) {
|
||||
return LNbits.map.extension(data);
|
||||
}).map(function (obj) {
|
||||
if (user) {
|
||||
obj.isEnabled = user.extensions.indexOf(obj.code) != -1;
|
||||
} else {
|
||||
obj.isEnabled = false;
|
||||
}
|
||||
return obj;
|
||||
}).sort(function (a, b) {
|
||||
return a.name > b.name;
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
122
lnbits/static/js/components.js
Normal file
@ -0,0 +1,122 @@
|
||||
Vue.component('lnbits-wallet-list', {
|
||||
data: function () {
|
||||
return {
|
||||
user: null,
|
||||
activeWallet: null,
|
||||
showForm: false,
|
||||
walletName: ''
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<q-list v-if="user && user.wallets.length" dense class="lnbits-drawer__q-list">
|
||||
<q-item-label header>Wallets</q-item-label>
|
||||
<q-item v-for="wallet in user.wallets" :key="wallet.id"
|
||||
clickable
|
||||
:active="activeWallet && activeWallet.id == wallet.id"
|
||||
tag="a" :href="wallet.url">
|
||||
<q-item-section side>
|
||||
<q-avatar size="md"
|
||||
:color="(activeWallet && activeWallet.id == wallet.id)
|
||||
? (($q.dark.isActive) ? 'deep-purple-5' : 'deep-purple')
|
||||
: 'grey-5'">
|
||||
<q-icon name="flash_on" :size="($q.dark.isActive) ? '21px' : '20px'"
|
||||
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1">{{ wallet.name }}</q-item-label>
|
||||
<q-item-label caption>{{ wallet.fsat }} sat</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side v-show="activeWallet && activeWallet.id == wallet.id">
|
||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="showForm = !showForm">
|
||||
<q-item-section side>
|
||||
<q-icon :name="(showForm) ? 'remove' : 'add'" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-caption">Add a wallet</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="showForm">
|
||||
<q-item-section>
|
||||
<q-form>
|
||||
<q-input filled dense v-model="walletName" label="Name wallet *">
|
||||
<template v-slot:append>
|
||||
<q-btn round dense flat icon="send" size="sm" @click="createWallet" :disable="walletName == ''"></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
`,
|
||||
methods: {
|
||||
createWallet: function () {
|
||||
LNbits.href.createWallet(this.walletName, this.user.id);
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user);
|
||||
}
|
||||
if (window.wallet) {
|
||||
this.activeWallet = LNbits.map.wallet(window.wallet);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('lnbits-extension-list', {
|
||||
data: function () {
|
||||
return {
|
||||
extensions: [],
|
||||
user: null
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<q-list v-if="user" dense class="lnbits-drawer__q-list">
|
||||
<q-item-label header>Extensions</q-item-label>
|
||||
<q-item v-for="extension in userExtensions" :key="extension.code"
|
||||
clickable
|
||||
tag="a" :href="[extension.url, '?usr=', user.id].join('')">
|
||||
<q-item-section side>
|
||||
<q-avatar size="md" color="grey-5">
|
||||
<q-icon :name="extension.icon" :size="($q.dark.isActive) ? '21px' : '20px'"
|
||||
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1">{{ extension.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable tag="a" :href="['/extensions?usr=', user.id].join('')">
|
||||
<q-item-section side>
|
||||
<q-icon name="clear_all" color="grey-5" size="md"></q-icon>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-caption">Manage extensions</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
`,
|
||||
computed: {
|
||||
userExtensions: function () {
|
||||
if (!this.user) return [];
|
||||
var userExtensions = this.user.extensions;
|
||||
return this.extensions.filter(function (obj) {
|
||||
return userExtensions.indexOf(obj.code) !== -1;
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.extensions = window.extensions.map(function (data) {
|
||||
return LNbits.map.extension(data);
|
||||
}).sort(function (a, b) {
|
||||
return a.name > b.name;
|
||||
});
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user);
|
||||
}
|
||||
}
|
||||
});
|
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 1.2 MiB |
53
lnbits/static/scss/base.scss
Normal file
@ -0,0 +1,53 @@
|
||||
$dark-background: #1f2234;
|
||||
$dark-card-background: #333646;
|
||||
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-lnbits-dark {
|
||||
background-color: $dark-background;
|
||||
}
|
||||
|
||||
body.body--dark,
|
||||
body.body--dark .q-drawer--dark,
|
||||
body.body--dark .q-menu--dark {
|
||||
background: $dark-background;
|
||||
}
|
||||
|
||||
body.body--dark .q-card--dark {
|
||||
background: $dark-card-background;
|
||||
}
|
||||
|
||||
body.body--dark .q-table--dark {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.body--light,
|
||||
body.body--light .q-drawer {
|
||||
background: whitesmoke;
|
||||
}
|
||||
|
||||
body.body--dark .q-field--error {
|
||||
.text-negative,
|
||||
.q-field__messages {
|
||||
color: yellow !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lnbits__q-table__icon-td {
|
||||
padding-left: 5px !important;
|
||||
}
|
||||
|
||||
.lnbits-drawer__q-list .q-item {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 5px !important;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
|
||||
&.q-item--active {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 14 KiB |
2
lnbits/static/vendor/axios@0.19.2/axios.min.js
vendored
Normal file
236
lnbits/static/vendor/bolt11/decoder.js
vendored
Normal file
@ -0,0 +1,236 @@
|
||||
//TODO - A reader MUST check that the signature is valid (see the n tagged field)
|
||||
//TODO - Tagged part of type f: the fallback on-chain address should be decoded into an address format
|
||||
//TODO - A reader MUST check that the SHA-2 256 in the h field exactly matches the hashed description.
|
||||
//TODO - A reader MUST use the n field to validate the signature instead of performing signature recovery if a valid n field is provided.
|
||||
|
||||
function decode(paymentRequest) {
|
||||
let input = paymentRequest.toLowerCase();
|
||||
let splitPosition = input.lastIndexOf('1');
|
||||
let humanReadablePart = input.substring(0, splitPosition);
|
||||
let data = input.substring(splitPosition + 1, input.length - 6);
|
||||
let checksum = input.substring(input.length - 6, input.length);
|
||||
if (!verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum))) {
|
||||
throw 'Malformed request: checksum is incorrect'; // A reader MUST fail if the checksum is incorrect.
|
||||
}
|
||||
return {
|
||||
'human_readable_part': decodeHumanReadablePart(humanReadablePart),
|
||||
'data': decodeData(data, humanReadablePart),
|
||||
'checksum': checksum
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHumanReadablePart(humanReadablePart) {
|
||||
let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb'];
|
||||
let prefix;
|
||||
prefixes.forEach(value => {
|
||||
if (humanReadablePart.substring(0, value.length) === value) {
|
||||
prefix = value;
|
||||
}
|
||||
});
|
||||
if (prefix == null) throw 'Malformed request: unknown prefix'; // A reader MUST fail if it does not understand the prefix.
|
||||
let amount = decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length));
|
||||
return {
|
||||
'prefix': prefix,
|
||||
'amount': amount
|
||||
}
|
||||
}
|
||||
|
||||
function decodeData(data, humanReadablePart) {
|
||||
let date32 = data.substring(0, 7);
|
||||
let dateEpoch = bech32ToInt(date32);
|
||||
let signature = data.substring(data.length - 104, data.length);
|
||||
let tagData = data.substring(7, data.length - 104);
|
||||
let decodedTags = decodeTags(tagData);
|
||||
let value = bech32ToFiveBitArray(date32 + tagData);
|
||||
value = fiveBitArrayTo8BitArray(value, true);
|
||||
value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value));
|
||||
return {
|
||||
'time_stamp': dateEpoch,
|
||||
'tags': decodedTags,
|
||||
'signature': decodeSignature(signature),
|
||||
'signing_data': value
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSignature(signature) {
|
||||
let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature));
|
||||
let recoveryFlag = data[data.length - 1];
|
||||
let r = byteArrayToHexString(data.slice(0, 32));
|
||||
let s = byteArrayToHexString(data.slice(32, data.length - 1));
|
||||
return {
|
||||
'r': r,
|
||||
's': s,
|
||||
'recovery_flag': recoveryFlag
|
||||
}
|
||||
}
|
||||
|
||||
function decodeAmount(str) {
|
||||
let multiplier = str.charAt(str.length - 1);
|
||||
let amount = str.substring(0, str.length - 1);
|
||||
if (amount.substring(0, 1) === '0') {
|
||||
throw 'Malformed request: amount cannot contain leading zeros';
|
||||
}
|
||||
amount = Number(amount);
|
||||
if (amount < 0 || !Number.isInteger(amount)) {
|
||||
throw 'Malformed request: amount must be a positive decimal integer'; // A reader SHOULD fail if amount contains a non-digit
|
||||
}
|
||||
|
||||
switch (multiplier) {
|
||||
case '':
|
||||
return 'Any amount'; // A reader SHOULD indicate if amount is unspecified
|
||||
case 'p':
|
||||
return amount / 10;
|
||||
case 'n':
|
||||
return amount * 100;
|
||||
case 'u':
|
||||
return amount * 100000;
|
||||
case 'm':
|
||||
return amount * 100000000;
|
||||
default:
|
||||
// A reader SHOULD fail if amount is followed by anything except a defined multiplier.
|
||||
throw 'Malformed request: undefined amount multiplier';
|
||||
}
|
||||
}
|
||||
|
||||
function decodeTags(tagData) {
|
||||
let tags = extractTags(tagData);
|
||||
let decodedTags = [];
|
||||
tags.forEach(value => decodedTags.push(decodeTag(value.type, value.length, value.data)));
|
||||
return decodedTags;
|
||||
}
|
||||
|
||||
function extractTags(str) {
|
||||
let tags = [];
|
||||
while (str.length > 0) {
|
||||
let type = str.charAt(0);
|
||||
let dataLength = bech32ToInt(str.substring(1, 3));
|
||||
let data = str.substring(3, dataLength + 3);
|
||||
tags.push({
|
||||
'type': type,
|
||||
'length': dataLength,
|
||||
'data': data
|
||||
});
|
||||
str = str.substring(3 + dataLength, str.length);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function decodeTag(type, length, data) {
|
||||
switch (type) {
|
||||
case 'p':
|
||||
if (length !== 52) break; // A reader MUST skip over a 'p' field that does not have data_length 52
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'payment_hash',
|
||||
'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)))
|
||||
};
|
||||
case 'd':
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'description',
|
||||
'value': bech32ToUTF8String(data)
|
||||
};
|
||||
case 'n':
|
||||
if (length !== 53) break; // A reader MUST skip over a 'n' field that does not have data_length 53
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'payee_public_key',
|
||||
'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)))
|
||||
};
|
||||
case 'h':
|
||||
if (length !== 52) break; // A reader MUST skip over a 'h' field that does not have data_length 52
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'description_hash',
|
||||
'value': data
|
||||
};
|
||||
case 'x':
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'expiry',
|
||||
'value': bech32ToInt(data)
|
||||
};
|
||||
case 'c':
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'min_final_cltv_expiry',
|
||||
'value': bech32ToInt(data)
|
||||
};
|
||||
case 'f':
|
||||
let version = bech32ToFiveBitArray(data.charAt(0))[0];
|
||||
if (version < 0 || version > 18) break; // a reader MUST skip over an f field with unknown version.
|
||||
data = data.substring(1, data.length);
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'fallback_address',
|
||||
'value': {
|
||||
'version': version,
|
||||
'fallback_address': data
|
||||
}
|
||||
};
|
||||
case 'r':
|
||||
data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data));
|
||||
let pubkey = data.slice(0, 33);
|
||||
let shortChannelId = data.slice(33, 41);
|
||||
let feeBaseMsat = data.slice(41, 45);
|
||||
let feeProportionalMillionths = data.slice(45, 49);
|
||||
let cltvExpiryDelta = data.slice(49, 51);
|
||||
return {
|
||||
'type': type,
|
||||
'length': length,
|
||||
'description': 'routing_information',
|
||||
'value': {
|
||||
'public_key': byteArrayToHexString(pubkey),
|
||||
'short_channel_id': byteArrayToHexString(shortChannelId),
|
||||
'fee_base_msat': byteArrayToInt(feeBaseMsat),
|
||||
'fee_proportional_millionths': byteArrayToInt(feeProportionalMillionths),
|
||||
'cltv_expiry_delta': byteArrayToInt(cltvExpiryDelta)
|
||||
}
|
||||
};
|
||||
default:
|
||||
// reader MUST skip over unknown fields
|
||||
}
|
||||
}
|
||||
|
||||
function polymod(values) {
|
||||
let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
||||
let chk = 1;
|
||||
values.forEach((value) => {
|
||||
let b = (chk >> 25);
|
||||
chk = (chk & 0x1ffffff) << 5 ^ value;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (((b >> i) & 1) === 1) {
|
||||
chk ^= GEN[i];
|
||||
} else {
|
||||
chk ^= 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
return chk;
|
||||
}
|
||||
|
||||
function expand(str) {
|
||||
let array = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array.push(str.charCodeAt(i) >> 5);
|
||||
}
|
||||
array.push(0);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array.push(str.charCodeAt(i) & 31);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function verify_checksum(hrp, data) {
|
||||
hrp = expand(hrp);
|
||||
let all = hrp.concat(data);
|
||||
let bool = polymod(all);
|
||||
return bool === 1;
|
||||
}
|
96
lnbits/static/vendor/bolt11/utils.js
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
||||
|
||||
function byteArrayToInt(byteArray) {
|
||||
let value = 0;
|
||||
for (let i = 0; i < byteArray.length; ++i) {
|
||||
value = (value << 8) + byteArray[i];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function bech32ToInt(str) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
sum = sum * 32;
|
||||
sum = sum + bech32CharValues.indexOf(str.charAt(i));
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
function bech32ToFiveBitArray(str) {
|
||||
let array = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array.push(bech32CharValues.indexOf(str.charAt(i)));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function fiveBitArrayTo8BitArray(int5Array, includeOverflow) {
|
||||
let count = 0;
|
||||
let buffer = 0;
|
||||
let byteArray = [];
|
||||
int5Array.forEach((value) => {
|
||||
buffer = (buffer << 5) + value;
|
||||
count += 5;
|
||||
if (count >= 8) {
|
||||
byteArray.push(buffer >> (count - 8) & 255);
|
||||
count -= 8;
|
||||
}
|
||||
});
|
||||
if (includeOverflow && count > 0) {
|
||||
byteArray.push(buffer << (8 - count) & 255);
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
function bech32ToUTF8String(str) {
|
||||
let int5Array = bech32ToFiveBitArray(str);
|
||||
let byteArray = fiveBitArrayTo8BitArray(int5Array);
|
||||
|
||||
let utf8String = '';
|
||||
for (let i = 0; i < byteArray.length; i++) {
|
||||
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2);
|
||||
}
|
||||
return decodeURIComponent(utf8String);
|
||||
}
|
||||
|
||||
function byteArrayToHexString(byteArray) {
|
||||
return Array.prototype.map.call(byteArray, function (byte) {
|
||||
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function textToHexString(text) {
|
||||
let hexString = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hexString += text.charCodeAt(i).toString(16);
|
||||
}
|
||||
return hexString;
|
||||
}
|
||||
|
||||
function epochToDate(int) {
|
||||
let date = new Date(int * 1000);
|
||||
return date.toUTCString();
|
||||
}
|
||||
|
||||
function isEmptyOrSpaces(str){
|
||||
return str === null || str.match(/^ *$/) !== null;
|
||||
}
|
||||
|
||||
function toFixed(x) {
|
||||
if (Math.abs(x) < 1.0) {
|
||||
var e = parseInt(x.toString().split('e-')[1]);
|
||||
if (e) {
|
||||
x *= Math.pow(10,e-1);
|
||||
x = '0.' + (new Array(e)).join('0') + x.toString().substring(2);
|
||||
}
|
||||
} else {
|
||||
var e = parseInt(x.toString().split('+')[1]);
|
||||
if (e > 20) {
|
||||
e -= 20;
|
||||
x /= Math.pow(10,e);
|
||||
x += (new Array(e+1)).join('0');
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
1
lnbits/static/vendor/chart.js@2.9.3/chart.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}
|
7
lnbits/static/vendor/chart.js@2.9.3/chart.min.js
vendored
Normal file
6
lnbits/static/vendor/quasar@1.9.7/quasar.ie.polyfills.umd.min.js
vendored
Normal file
1
lnbits/static/vendor/quasar@1.9.7/quasar.min.css
vendored
Normal file
34346
lnbits/static/vendor/quasar@1.9.7/quasar.umd.js
vendored
Normal file
6
lnbits/static/vendor/quasar@1.9.7/quasar.umd.min.js
vendored
Normal file
5
lnbits/static/vendor/underscore@1.9.2/underscore.min.js
vendored
Normal file
10
lnbits/static/vendor/vue-qrcode@1.0.2/vue-qrcode.min.js
vendored
Normal file
2926
lnbits/static/vendor/vue-router@3.1.6/vue-router.js
vendored
Normal file
6
lnbits/static/vendor/vue-router@3.1.6/vue-router.min.js
vendored
Normal file
11965
lnbits/static/vendor/vue@2.6.11/vue.js
vendored
Normal file
6
lnbits/static/vendor/vue@2.6.11/vue.min.js
vendored
Normal file
1055
lnbits/static/vendor/vuex@3.1.2/vuex.js
vendored
Normal file
6
lnbits/static/vendor/vuex@3.1.2/vuex.min.js
vendored
Normal file
Before Width: | Height: | Size: 59 KiB |
@ -1,477 +1,86 @@
|
||||
<!-- @format -->
|
||||
<!doctype html>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Date picker -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/datepicker.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Datepicker 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/datepicker.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<style>
|
||||
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background:#1f2234;
|
||||
border-left-color:#8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color:
|
||||
#2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper, .right-side {
|
||||
background-color:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color:
|
||||
#1f2234;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color:
|
||||
#4b646f;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid
|
||||
transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
color: #fff;
|
||||
background:#3e355a;
|
||||
border-left-color:#8964a9;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
|
||||
background-color:
|
||||
#1f2234 !important;
|
||||
}
|
||||
.alert-danger, .alert-error {
|
||||
|
||||
border-color: #fff;
|
||||
border: 1px solid
|
||||
|
||||
#fff;
|
||||
border-radius: 7px;
|
||||
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color:
|
||||
#f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
|
||||
background-color:
|
||||
#3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
|
||||
background-color:#333646;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.box-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
|
||||
background-color: #333646;
|
||||
|
||||
}
|
||||
|
||||
.h1 .small, .h1 small, .h2 .small, .h2 small, .h3 .small, .h3 small, .h4 .small, .h4 small, .h5 .small, .h5 small, .h6 .small, .h6 small, h1 .small, h1 small, h2 .small, h2 small, h3 .small, h3 small, h4 .small, h4 small, h5 .small, h5 small, h6 .small, h6 small {
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Material+Icons" type="text/css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/quasar@1.9.7/quasar.min.css') }}">
|
||||
{% assets 'base_css' %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
|
||||
{% endassets %}
|
||||
{% block styles %}{% endblock %}
|
||||
<title>{% block title %}LNbits{% endblock %}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
{% block head_scripts %}{% endblock %}
|
||||
</head>
|
||||
<body class="skin-blue">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b style="color: #8964a9;">LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Left side column. contains the logo and sidebar -->
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar">
|
||||
<!-- Sidebar user panel -->
|
||||
<body>
|
||||
<q-layout id="vue" view="hHh lpR lfr" v-cloak>
|
||||
|
||||
<!-- /.search form -->
|
||||
<!-- sidebar menu: : style can be found in sidebar.less -->
|
||||
<ul class="sidebar-menu">
|
||||
<li class="header">MENU</li>
|
||||
{% block menuitems %}{% endblock %}
|
||||
</ul>
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
<q-header bordered class="bg-lnbits-dark">
|
||||
<q-toolbar>
|
||||
<q-btn dense flat round icon="menu" @click="w.visibleDrawer = !w.visibleDrawer"></q-btn>
|
||||
<q-toolbar-title><strong>LN</strong>bits</q-toolbar-title>
|
||||
<q-badge color="yellow" text-color="black">
|
||||
<span><span v-show="$q.screen.gt.sm">USE WITH CAUTION - LNbits wallet is still in </span>BETA</span>
|
||||
</q-badge>
|
||||
<q-btn dense flat round @click="toggleDarkMode" :icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'" class="q-ml-lg" size="sm">
|
||||
<q-tooltip>Toggle Dark Mode</q-tooltip>
|
||||
</q-btn>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<q-drawer v-model="w.visibleDrawer" side="left" :width="($q.screen.lt.md) ? 260 : 230" show-if-above :elevated="$q.screen.lt.md">
|
||||
<lnbits-wallet-list></lnbits-wallet-list>
|
||||
<lnbits-extension-list class="q-pb-xl"></lnbits-extension-list>
|
||||
</q-drawer>
|
||||
|
||||
<footer class="main-footer">
|
||||
<div class="pull-right hidden-xs">
|
||||
<b>BETA</b>
|
||||
</div>
|
||||
<strong
|
||||
>Learn more about LNbits
|
||||
<a href="https://github.com/arcbtc/lnbits"
|
||||
>https://github.com/arcbtc/lnbits</a
|
||||
></strong
|
||||
>
|
||||
</footer>
|
||||
<q-page-container>
|
||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
||||
{% block page %}{% endblock %}
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
|
||||
<q-footer class="bg-transparent q-px-lg q-py-md" :class="{'text-dark': !$q.dark.isActive}">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title class="text-caption">
|
||||
<strong>LN</strong>bits, free and open-source lightning wallet
|
||||
</q-toolbar-title>
|
||||
<q-space></q-space>
|
||||
<q-btn flat dense :color="($q.dark.isActive) ? 'white' : 'deep-purple'" icon="code" type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener">
|
||||
<q-tooltip>View project in GitHub</q-tooltip>
|
||||
</q-btn>
|
||||
</q-toolbar>
|
||||
</q-footer>
|
||||
|
||||
</q-layout>
|
||||
|
||||
{% block vue_templates %}{% endblock %}
|
||||
|
||||
{% if DEBUG %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue@2.6.11/vue.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='vendor/vue-router@3.1.6/vue-router.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='vendor/vuex@3.1.2/vuex.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='vendor/quasar@1.9.7/quasar.umd.js') }}"></script>
|
||||
{% else %}
|
||||
{% assets output='__bundle__/vue.js',
|
||||
'vendor/quasar@1.9.7/quasar.ie.polyfills.umd.min.js',
|
||||
'vendor/vue@2.6.11/vue.min.js',
|
||||
'vendor/vue-router@3.1.6/vue-router.min.js',
|
||||
'vendor/vuex@3.1.2/vuex.min.js',
|
||||
'vendor/quasar@1.9.7/quasar.umd.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endif %}
|
||||
|
||||
{% assets filters='rjsmin', output='__bundle__/base.js',
|
||||
'vendor/axios@0.19.2/axios.min.js',
|
||||
'vendor/underscore@1.9.2/underscore.min.js',
|
||||
'js/base.js',
|
||||
'js/components.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='app.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
</html>
|
||||
|
@ -1,114 +0,0 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block messages %}
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header"><b>Instant wallet, bookmark to save</b></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block menuitems %}
|
||||
<li class="treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for w in user_wallets %}
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
|
||||
<div id="sidebarmake"></div>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="active treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-th"></i> <span>Extensions</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for extension in EXTENSIONS %}
|
||||
{% if extension.code in user_ext %}
|
||||
<li>
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<div id="wonga"></div>
|
||||
<h1>Wallet <small>Control panel</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li class="active">Extensions</li>
|
||||
</ol>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<h4>Bookmark to save your wallet. Wallet is in BETA, use with caution.</h4>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<!-- Small boxes (Stat box) -->
|
||||
<div class="row">
|
||||
|
||||
{% for extension in EXTENSIONS %}
|
||||
<div class="col-lg-3 col-xs-6">
|
||||
<!-- small box -->
|
||||
<div class="small-box bg-blue">
|
||||
<div class="inner">
|
||||
{% if extension.code in user_ext %}
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}" style="color: inherit">
|
||||
{% endif %}
|
||||
<h3>{{ extension.name }}</h3>
|
||||
<p>{{ extension.short_description }}</p>
|
||||
{% if extension.code in user_ext %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="ion ion-{{ extension.ion_icon }}"></i>
|
||||
</div>
|
||||
{% if extension.code in user_ext %}
|
||||
<a href="{{ url_for('extensions') }}?usr={{user}}&disable={{ extension.code }}" class="small-box-footer">Disable <i class="fa fa-arrow-circle-right"></i></a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('extensions') }}?usr={{user}}&enable={{ extension.code }}" class="small-box-footer">Enable <i class="fa fa-arrow-circle-right"></i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
<!-- /.content -->
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.user = {{ user | megajson | safe }}
|
||||
window.user_wallets = {{ user_wallets | megajson | safe }}
|
||||
window.user_ext = {{ user_ext | megajson | safe }}
|
||||
</script>
|
||||
{% endblock %}
|
477
lnbits/templates/legacy.html
Normal file
@ -0,0 +1,477 @@
|
||||
<!-- @format -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Date picker -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/datepicker.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Datepicker 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/datepicker.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<style>
|
||||
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background:#1f2234;
|
||||
border-left-color:#8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color:
|
||||
#2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper, .right-side {
|
||||
background-color:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color:
|
||||
#1f2234;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color:
|
||||
#4b646f;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background:
|
||||
#1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid
|
||||
transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
|
||||
|
||||
color: #fff;
|
||||
background:#3e355a;
|
||||
border-left-color:#8964a9;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background:
|
||||
#3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color:
|
||||
#3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
|
||||
background-color:
|
||||
#1f2234 !important;
|
||||
}
|
||||
.alert-danger, .alert-error {
|
||||
|
||||
border-color: #fff;
|
||||
border: 1px solid
|
||||
|
||||
#fff;
|
||||
border-radius: 7px;
|
||||
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color:
|
||||
#f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
|
||||
background-color:
|
||||
#3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n+1) {
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
|
||||
background-color:#333646;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
.box-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal-footer {
|
||||
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
background-color:
|
||||
#333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
|
||||
background-color: #333646;
|
||||
|
||||
}
|
||||
|
||||
.h1 .small, .h1 small, .h2 .small, .h2 small, .h3 .small, .h3 small, .h4 .small, .h4 small, .h5 .small, .h5 small, .h6 .small, .h6 small, h1 .small, h1 small, h2 .small, h2 small, h3 .small, h3 small, h4 .small, h4 small, h5 .small, h5 small, h6 .small, h6 small {
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color:
|
||||
#fff;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="skin-blue">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b style="color: #8964a9;">LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Left side column. contains the logo and sidebar -->
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar">
|
||||
<!-- Sidebar user panel -->
|
||||
|
||||
<!-- /.search form -->
|
||||
<!-- sidebar menu: : style can be found in sidebar.less -->
|
||||
<ul class="sidebar-menu">
|
||||
<li class="header">MENU</li>
|
||||
{% block menuitems %}{% endblock %}
|
||||
</ul>
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<div class="pull-right hidden-xs">
|
||||
<b>BETA</b>
|
||||
</div>
|
||||
<strong
|
||||
>Learn more about LNbits
|
||||
<a href="https://github.com/arcbtc/lnbits"
|
||||
>https://github.com/arcbtc/lnbits</a
|
||||
></strong
|
||||
>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='app.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
</html>
|
12
lnbits/templates/macros.jinja
Normal file
@ -0,0 +1,12 @@
|
||||
{% macro window_vars(user, wallet) -%}
|
||||
<script>
|
||||
window.extensions = {{ EXTENSIONS | tojson | safe }};
|
||||
{% if user %}
|
||||
window.user = {{ user | tojson | safe }};
|
||||
{% endif %}
|
||||
{% if wallet %}
|
||||
window.wallet = {{ wallet | tojson | safe }};
|
||||
window.transactions = {{ wallet.get_transactions() | tojson | safe }};
|
||||
{% endif %}
|
||||
</script>
|
||||
{%- endmacro %}
|
Before Width: | Height: | Size: 79 KiB |
@ -1,6 +1,6 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
@ -9,7 +9,7 @@
|
||||
<li class="header"><b>Instant wallet, bookmark to save</b></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block menuitems %}
|
||||
@ -42,7 +42,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
@ -15,7 +15,8 @@ class LndWallet(Wallet):
|
||||
payment_hash, payment_request = None, None
|
||||
r = post(
|
||||
url=f"{self.endpoint}/v1/invoices",
|
||||
headers=self.auth_admin, verify=False,
|
||||
headers=self.auth_admin,
|
||||
verify=False,
|
||||
json={"value": amount, "memo": memo, "private": True},
|
||||
)
|
||||
|
||||
@ -33,7 +34,10 @@ class LndWallet(Wallet):
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = post(
|
||||
url=f"{self.endpoint}/v1/channels/transactions", headers=self.auth_admin, verify=False, json={"payment_request": bolt11}
|
||||
url=f"{self.endpoint}/v1/channels/transactions",
|
||||
headers=self.auth_admin,
|
||||
verify=False,
|
||||
json={"payment_request": bolt11},
|
||||
)
|
||||
return PaymentResponse(r, not r.ok)
|
||||
|
||||
@ -46,7 +50,12 @@ class LndWallet(Wallet):
|
||||
return TxStatus(r, r.json()["settled"])
|
||||
|
||||
def get_payment_status(self, payment_hash: str) -> TxStatus:
|
||||
r = get(url=f"{self.endpoint}/v1/payments", headers=self.auth_admin, verify=False, params={"include_incomplete": True})
|
||||
r = get(
|
||||
url=f"{self.endpoint}/v1/payments",
|
||||
headers=self.auth_admin,
|
||||
verify=False,
|
||||
params={"include_incomplete": True},
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
return TxStatus(r, None)
|
||||
|
@ -3,18 +3,23 @@ bitstring==3.1.6
|
||||
certifi==2019.11.28
|
||||
chardet==3.0.4
|
||||
click==7.0
|
||||
flask-assets==2.0
|
||||
flask-compress==1.4.0
|
||||
flask-talisman==0.7.0
|
||||
flask==1.1.1
|
||||
idna==2.8
|
||||
gevent==1.4.0
|
||||
greenlet==0.4.15
|
||||
gunicorn==20.0.4
|
||||
idna==2.9
|
||||
itsdangerous==1.1.0
|
||||
jinja2==2.11.1
|
||||
lnurl==0.3.3
|
||||
markupsafe==1.1.1
|
||||
pydantic==1.4
|
||||
requests==2.22.0
|
||||
pyscss==1.3.5
|
||||
requests==2.23.0
|
||||
six==1.14.0
|
||||
typing-extensions==3.7.4.1 ; python_version < '3.8'
|
||||
urllib3==1.25.8
|
||||
webassets==2.0
|
||||
werkzeug==1.0.0
|
||||
gevent==1.4.0
|
||||
greenlet==0.4.15
|
||||
|