Browse Source

big bundle of changes.

* reorganize templates even more (and a small layout break)
* rename wallets.hash and accounts.userhash to id
* refactor /wallets so it's idempotent for each param combination
* many small changes
fee_issues
fiatjaf 5 years ago
parent
commit
14dfa9ecc6
  1. 263
      LNbits/__init__.py
  2. 8
      LNbits/data/schema.sql
  3. 1
      LNbits/db.py
  4. 5
      LNbits/helpers.py
  5. 1
      LNbits/settings.py
  6. 57
      LNbits/templates/base.html
  7. 210
      LNbits/templates/index.html
  8. 1034
      LNbits/templates/wallet.html

263
LNbits/__init__.py

@ -1,13 +1,13 @@
import lnurl import lnurl
import uuid
import os import os
import requests import requests
from flask import Flask, jsonify, render_template, request from flask import Flask, jsonify, render_template, request, redirect, url_for
from . import bolt11 from . import bolt11
from .db import Database from .db import Database
from .helpers import encrypt from .settings import DATABASE_PATH, LNBITS_PATH, WALLET, DEFAULT_USER_WALLET_NAME
from .settings import DATABASE_PATH, LNBITS_PATH, WALLET
app = Flask(__name__) app = Flask(__name__)
@ -35,21 +35,24 @@ def home():
@app.route("/deletewallet") @app.route("/deletewallet")
def deletewallet(): def deletewallet():
theid = request.args.get("usr")
thewal = request.args.get("wal") thewal = request.args.get("wal")
with Database() as db: with Database() as db:
wallet_row = db.fetchone("SELECT * FROM wallets WHERE hash = ?", (thewal,)) db.execute(
"""
if not wallet_row: UPDATE wallets AS w SET
return render_template("index.html") user = 'del:' || w.user,
adminkey = 'del:' || w.adminkey,
db.execute("UPDATE wallets SET user = ? WHERE hash = ?", (f"del{wallet_row[4]}", wallet_row[0])) inkey = 'del:' || w.inkey
db.execute("UPDATE wallets SET adminkey = ? WHERE hash = ?", (f"del{wallet_row[5]}", wallet_row[0])) WHERE id = ? AND user = ?
db.execute("UPDATE wallets SET inkey = ? WHERE hash = ?", (f"del{wallet_row[6]}", wallet_row[0])) """,
(thewal, theid),
)
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (wallet_row[4],)) next_wallet = db.fetchone("SELECT hash FROM wallets WHERE user = ?", (theid,))
if user_wallets: if next_wallet:
return render_template("deletewallet.html", theid=user_wallets[0][4], thewal=user_wallets[0][0]) return redirect(url_for("wallet", usr=theid, wal=next_wallet[0]))
return render_template("index.html") return render_template("index.html")
@ -60,13 +63,13 @@ def lnurlwallet():
invoice = WALLET.create_invoice(withdraw_res.max_sats).json() invoice = WALLET.create_invoice(withdraw_res.max_sats).json()
payment_hash = invoice["payment_hash"] payment_hash = invoice["payment_hash"]
rrr = requests.get( r = requests.get(
withdraw_res.callback.base, withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": invoice["pay_req"]}}, params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": invoice["pay_req"]}},
) )
dataaa = rrr.json() data = r.json()
if dataaa["status"] != "OK": if data["status"] != "OK":
"""TODO: show some kind of error?""" """TODO: show some kind of error?"""
return render_template("index.html") return render_template("index.html")
@ -76,112 +79,104 @@ def lnurlwallet():
data = r.json() data = r.json()
with Database() as db: with Database() as db:
adminkey = encrypt(payment_hash)[0:20] adminkey = uuid.uuid4().hex
inkey = encrypt(adminkey)[0:20] inkey = uuid.uuid4().hex
thewal = encrypt(inkey)[0:20] thewal = uuid.uuid4().hex
theid = encrypt(thewal)[0:20] theid = uuid.uuid4().hex
thenme = "Bitcoin LN Wallet" thenme = DEFAULT_USER_WALLET_NAME
db.execute("INSERT INTO accounts (userhash) VALUES (?)", (theid,))
adminkey = encrypt(theid)
inkey = encrypt(adminkey)
db.execute("INSERT INTO accounts (id) VALUES (?)", (theid,))
db.execute( db.execute(
"INSERT INTO wallets (hash, name, user, adminkey, inkey) VALUES (?, ?, ?, ?, ?)", "INSERT INTO wallets (id, name, user, adminkey, inkey) VALUES (?, ?, ?, ?, ?)",
(thewal, thenme, theid, adminkey, inkey), (thewal, thenme, theid, adminkey, inkey),
) )
return render_template( return redirect(url_for("wallet", usr=theid, wal=thewal))
"lnurlwallet.html",
len=len("1"),
walnme=thenme,
walbal=withdraw_res.max_sats,
theid=theid,
thewal=thewal,
adminkey=adminkey,
inkey=inkey,
)
@app.route("/wallet") @app.route("/wallet")
def wallet(): def wallet():
theid = request.args.get("usr") usr = request.args.get("usr")
thewal = request.args.get("wal") wallet_id = request.args.get("wal")
thenme = request.args.get("nme") wallet_name = request.args.get("nme") or DEFAULT_USER_WALLET_NAME
if not thewal: # just usr: return a the first user wallet or create one if none found
return render_template("index.html") # 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 Database() as db: with Database() as db:
user_exists = db.fetchone("SELECT * FROM accounts WHERE userhash = ?", (theid,)) # ensure this user exists
# -------------------------------
if not user_exists:
# user does not exist: create an account if not usr:
# -------------------------------------- usr = uuid.uuid4().hex
return redirect(url_for("wallet", usr=usr, wal=wallet_id, nme=wallet_name))
db.execute("INSERT INTO accounts (userhash) VALUES (?)", (theid,))
# user exists
# -----------
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (theid,))
if user_wallets:
# user has wallets
# ----------------
wallet_row = db.fetchone(
"""
SELECT
(SELECT balance/1000 FROM balances WHERE wallet = wallets.hash),
name,
adminkey,
inkey
FROM wallets
WHERE user = ? AND hash = ?
""",
(theid, thewal,),
)
transactions = []
return render_template(
"wallet.html",
thearr=user_wallets,
len=len(user_wallets),
walnme=wallet_row[1],
user=theid,
walbal=wallet_row[0],
theid=theid,
thewal=thewal,
transactions=transactions,
adminkey=wallet_row[2],
inkey=wallet_row[3],
)
# user has no wallets
# -------------------
adminkey = encrypt(theid)
inkey = encrypt(adminkey)
db.execute( db.execute(
"INSERT INTO wallets (hash, name, user, adminkey, inkey) VALUES (?, ?, ?, ?, ?)", """
(thewal, thenme, theid, adminkey, inkey), INSERT INTO accounts (id) VALUES (?)
ON CONFLICT (id) DO NOTHING
""",
(usr,),
) )
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
if not wallet_id:
# if not given, fetch the first wallet from this user or create
# -------------------------------------------------------------
if user_wallets:
wallet_id = user_wallets[0]["id"]
else:
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 INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET user = ?
""",
(wallet_id, wallet_name, usr, uuid.uuid4().hex, uuid.uuid4().hex, usr),
)
# 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,
name,
adminkey,
inkey
FROM wallets
WHERE user = ? AND id = ?
""",
(usr, wallet_id),
)
transactions = []
return render_template( return render_template(
"wallet.html", "wallet.html", user_wallets=user_wallets, wallet=wallet, user=usr, transactions=transactions,
len=1,
walnme=thenme,
walbal=0,
theid=theid,
thewal=thewal,
adminkey=adminkey,
inkey=inkey,
transactions=[],
) )
@ -205,12 +200,12 @@ def api_invoices():
return jsonify({"ERROR": "NO MEMO"}), 400 return jsonify({"ERROR": "NO MEMO"}), 400
with Database() as db: with Database() as db:
wallet_row = db.fetchone( wallet = db.fetchone(
"SELECT hash FROM wallets WHERE inkey = ? OR adminkey = ?", "SELECT id FROM wallets WHERE inkey = ? OR adminkey = ?",
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"],), (request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"],),
) )
if not wallet_row: if not wallet:
return jsonify({"ERROR": "NO KEY"}), 200 return jsonify({"ERROR": "NO KEY"}), 200
r = WALLET.create_invoice(postedjson["value"], postedjson["memo"]) r = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
@ -218,10 +213,11 @@ def api_invoices():
pay_req = data["pay_req"] pay_req = data["pay_req"]
payment_hash = data["payment_hash"] payment_hash = data["payment_hash"]
amount_msat = int(postedjson["value"]) * 1000
db.execute( db.execute(
"INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, true, ?)", "INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, true, ?)",
(payment_hash, int(postedjson["value"]) * 1000, wallet_row[0], postedjson["memo"],), (payment_hash, amount_msat, wallet["id"], postedjson["memo"],),
) )
return jsonify({"pay_req": pay_req, "payment_hash": payment_hash}), 200 return jsonify({"pay_req": pay_req, "payment_hash": payment_hash}), 200
@ -238,39 +234,11 @@ def api_transactions():
return jsonify({"ERROR": "NO PAY REQ"}), 200 return jsonify({"ERROR": "NO PAY REQ"}), 200
with Database() as db: with Database() as db:
wallet_row = db.fetchone( wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
"SELECT hash FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],)
)
if not wallet_row: if not wallet:
return jsonify({"ERROR": "BAD AUTH"}), 200 return jsonify({"ERROR": "BAD AUTH"}), 200
# TODO: check this unused code
# move sats calculation to a helper
# ---------------------------------
"""
s = postedjson["payment_request"]
result = re.search("lnbc(.*)1p", s)
tempp = result.group(1)
alpha = ""
num = ""
for i in range(len(tempp)):
if tempp[i].isdigit():
num = num + tempp[i]
else:
alpha += tempp[i]
sats = ""
if alpha == "n":
sats = int(num) / 10
elif alpha == "u":
sats = int(num) * 100
elif alpha == "m":
sats = int(num) * 100000
"""
# ---------------------------------
# decode the invoice # decode the invoice
invoice = bolt11.decode(data["payment_request"]) invoice = bolt11.decode(data["payment_request"])
if invoice.amount_msat == 0: if invoice.amount_msat == 0:
@ -283,13 +251,13 @@ def api_transactions():
invoice.payment_hash, invoice.payment_hash,
-int(invoice.amount_msat), -int(invoice.amount_msat),
-int(invoice.amount_msat * 0.01), -int(invoice.amount_msat * 0.01),
wallet_row[0], wallet["id"],
invoice.description, invoice.description,
), ),
) )
# check balance # check balance
balance = db.fetchone("SELECT balance/1000 FROM balances WHERE wallet = ?", (wallet_row[0],))[0] balance = db.fetchone("SELECT balance/1000 FROM balances WHERE wallet = ?", (wallet["id"],))[0]
if balance < 0: if balance < 0:
return jsonify({"ERROR": "INSUFFICIENT BALANCE"}), 403 return jsonify({"ERROR": "INSUFFICIENT BALANCE"}), 403
@ -321,24 +289,23 @@ def api_checkinvoice(payhash):
return jsonify({"ERROR": "MUST BE JSON"}), 200 return jsonify({"ERROR": "MUST BE JSON"}), 200
with Database() as db: with Database() as db:
payment_row = db.fetchone( payment = db.fetchone(
""" """
SELECT pending FROM apipayments SELECT pending FROM apipayments
INNER JOIN wallets AS w ON apipayments.wallet = w.hash INNER JOIN wallets AS w ON apipayments.wallet = w.id
WHERE payhash = ? WHERE payhash = ?
AND (w.adminkey = ? OR w.inkey = ?) AND (w.adminkey = ? OR w.inkey = ?)
""", """,
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]), (payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
) )
if not payment_row: if not payment:
return jsonify({"ERROR": "NO INVOICE"}), 404 return jsonify({"ERROR": "NO INVOICE"}), 404
if not payment_row[0]: # pending if not payment["pending"]: # pending
return jsonify({"PAID": "TRUE"}), 200 return jsonify({"PAID": "TRUE"}), 200
r = WALLET.get_invoice_status(payhash) r = WALLET.get_invoice_status(payhash)
if not r.ok: if not r.ok:
return jsonify({"PAID": "FALSE"}), 400 return jsonify({"PAID": "FALSE"}), 400

8
LNbits/data/schema.sql

@ -1,11 +1,11 @@
CREATE TABLE IF NOT EXISTS accounts ( CREATE TABLE IF NOT EXISTS accounts (
userhash text PRIMARY KEY, id text PRIMARY KEY,
email text, email text,
pass text pass text
); );
CREATE TABLE IF NOT EXISTS wallets ( CREATE TABLE IF NOT EXISTS wallets (
hash text PRIMARY KEY, id text PRIMARY KEY,
name text NOT NULL, name text NOT NULL,
user text NOT NULL, user text NOT NULL,
adminkey text NOT NULL, adminkey text NOT NULL,
@ -21,9 +21,7 @@ CREATE TABLE IF NOT EXISTS apipayments (
memo text memo text
); );
DROP VIEW IF EXISTS balances; CREATE VIEW IF NOT EXISTS balances AS
CREATE VIEW balances AS
SELECT wallet, coalesce(sum(s), 0) AS balance FROM ( SELECT wallet, coalesce(sum(s), 0) AS balance FROM (
SELECT wallet, sum(amount) AS s -- incoming SELECT wallet, sum(amount) AS s -- incoming
FROM apipayments FROM apipayments

1
LNbits/db.py

@ -7,6 +7,7 @@ class Database:
def __init__(self, db_path: str = DATABASE_PATH): def __init__(self, db_path: str = DATABASE_PATH):
self.path = db_path self.path = db_path
self.connection = sqlite3.connect(db_path) self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
def __enter__(self): def __enter__(self):

5
LNbits/helpers.py

@ -1,5 +0,0 @@
import hashlib
def encrypt(string: str):
return hashlib.sha256(string.encode()).hexdigest()

1
LNbits/settings.py

@ -14,3 +14,4 @@ WALLET = LntxbotWallet(
LNBITS_PATH = os.path.dirname(os.path.realpath(__file__)) LNBITS_PATH = os.path.dirname(os.path.realpath(__file__))
DATABASE_PATH = os.getenv("DATABASE_PATH") or os.path.join(LNBITS_PATH, "data", "database.sqlite3") DATABASE_PATH = os.getenv("DATABASE_PATH") or os.path.join(LNBITS_PATH, "data", "database.sqlite3")
DEFAULT_USER_WALLET_NAME = os.getenv("DEFAULT_USER_WALLET_NAME") or "Bitcoin LN Wallet"

57
LNbits/templates/base.html

@ -239,6 +239,61 @@
></script> ></script>
</head> </head>
<body class="skin-blue"> <body class="skin-blue">
{% block body %}{% endblock %} <div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="/" class="logo"><b>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/FOSSAW"
>https://github.com/arcbtc/lnbits</a
></strong
>
</footer>
</body> </body>
</html> </html>

210
LNbits/templates/index.html

@ -1,146 +1,94 @@
<!-- @format --> <!-- @format -->
{% extends "base.html" %} {% block body %} {% extends "base.html" %} {% block menuitems %}
<div class="wrapper"> <li>
<header class="main-header"> <a href="/"><i class="fa fa-book"></i> Home</a>
<!-- Logo --> </li>
<a href="index.html" class="logo"><b>LN</b>bits</a> {% endblock %} {% block body %}
<!-- Header Navbar: style can be found in header.less --> <!-- Right side column. Contains the navbar and content of the page -->
<nav class="navbar navbar-static-top" role="navigation"> <div class="content-wrapper">
<!-- Sidebar toggle button--> <!-- Content Header (Page header) -->
<a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button"> <section class="content-header">
<span class="sr-only">Toggle navigation</span> <ol class="breadcrumb">
</a> <li>
<div class="navbar-custom-menu"> <a href="/"><i class="fa fa-dashboard"></i> Home</a>
<ul class="nav navbar-nav"> </li>
<!-- Messages: style can be found in dropdown.less--> </ol>
<li class="dropdown messages-menu"></li> <br /><br />
</ul> <div class="alert alert-danger alert-dismissable">
</div> <h4>
</nav> Warning - Wallet is still in BETA and very, very #reckless, please be
</header> careful with your funds!
<!-- Left side column. contains the logo and sidebar --> </h4>
<aside class="main-sidebar"> </div>
<!-- sidebar: style can be found in sidebar.less --> </section>
<section class="sidebar">
<!-- Sidebar user panel -->
<!-- /.search form -->
<!-- sidebar menu: : style can be found in sidebar.less -->
<ul class="sidebar-menu">
<li class="header">MAIN NAVIGATION</li>
<li>
<a href="../documentation/index.html"
><i class="fa fa-book"></i> Home</a
>
</li>
</ul>
</section>
<!-- /.sidebar -->
</aside>
<!-- 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="/index.html"><i class="fa fa-dashboard"></i> Home</a>
</li>
</ol>
<br /><br />
<div class="alert alert-danger alert-dismissable">
<h4>
Warning - Wallet is still in BETA and very, very #reckless, please be
careful with your funds!
</h4>
</div>
</section>
<!-- Main content --> <!-- Main content -->
<section class="content"> <section class="content">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<!-- Default box --> <!-- Default box -->
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
<h1> <h1>
<a href="index.html" class="logo"><b>LN</b>bits</a> <a href="index.html" class="logo"><b>LN</b>bits</a>
<small>free and open-source lightning wallet</small> <small>free and open-source lightning wallet</small>
</h1> </h1>
<p> <p>
LNbits is a simple, free and open-source lightning-network LNbits is a simple, free and open-source lightning-network wallet
wallet for bits and bobs. You can run it on your own server, or for bits and bobs. You can run it on your own server, or use this
use this one. one.
<br /><br /> <br /><br />
The wallet can be used in a variety of ways, an instant wallet The wallet can be used in a variety of ways, an instant wallet for
for LN demonstrations, a fallback wallet for the LNURL scheme, LN demonstrations, a fallback wallet for the LNURL scheme, an
an accounts system to mitigate the risk of exposing applications accounts system to mitigate the risk of exposing applications to
to your full balance. your full balance.
<br /><br /> <br /><br />
The wallet can run on top of LND, lntxbot, paywall, opennode The wallet can run on top of LND, lntxbot, paywall, opennode
<br /><br /> <br /><br />
Please note that although one of the aims of this wallet is to 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 mitigate exposure of all your funds, it’s still very BETA and may
may in fact do the opposite! in fact do the opposite!
<br /> <br />
<a href="https://github.com/arcbtc/FOSSAW" <a href="https://github.com/arcbtc/FOSSAW"
>https://github.com/arcbtc/lnbits</a >https://github.com/arcbtc/lnbits</a
> >
</p> </p>
</div>
<!-- /.box-body -->
</div> </div>
<!-- /.box --> <!-- /.box-body -->
</div> </div>
<!-- /.box -->
</div>
<div class="col-md-4"> <div class="col-md-4">
<!-- Default box --> <!-- Default box -->
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
<h1> <h1>
<small>Make a wallet</small> <small>Make a wallet</small>
</h1> </h1>
<div class="form-group"> <div class="form-group">
<input <input
type="text" type="text"
class="form-control" class="form-control"
id="walname" id="walname"
placeholder="Name your LNBits wallet" placeholder="Name your LNBits wallet"
required required
/> />
</div>
<button
type="button"
class="btn btn-primary"
onclick="newwallet()"
>
Submit
</button>
</div> </div>
<!-- /.box-body --> <button type="button" class="btn btn-primary" onclick="newwallet()">
Submit
</button>
</div> </div>
<!-- /.box --> <!-- /.box-body -->
</div> </div>
<!-- /.box -->
</div> </div>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<footer class="main-footer">
<div class="pull-right hidden-xs">
<b>BETA</b>
</div> </div>
<strong </section>
>Learn more about LNbits <!-- /.content -->
<a href="https://github.com/arcbtc/FOSSAW"
>https://github.com/arcbtc/lnbits</a
></strong
>
</footer>
</div> </div>
<!-- /.content-wrapper -->
<script> <script>
function makeid(length) { function makeid(length) {

1034
LNbits/templates/wallet.html

File diff suppressed because it is too large
Loading…
Cancel
Save