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
This commit is contained in:
fiatjaf 2019-12-14 15:49:32 -03:00
parent 1cee6dad42
commit 14dfa9ecc6
8 changed files with 796 additions and 795 deletions

View File

@ -1,13 +1,13 @@
import lnurl
import uuid
import os
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 .db import Database
from .helpers import encrypt
from .settings import DATABASE_PATH, LNBITS_PATH, WALLET
from .settings import DATABASE_PATH, LNBITS_PATH, WALLET, DEFAULT_USER_WALLET_NAME
app = Flask(__name__)
@ -35,21 +35,24 @@ def home():
@app.route("/deletewallet")
def deletewallet():
theid = request.args.get("usr")
thewal = request.args.get("wal")
with Database() as db:
wallet_row = db.fetchone("SELECT * FROM wallets WHERE hash = ?", (thewal,))
db.execute(
"""
UPDATE wallets AS w SET
user = 'del:' || w.user,
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
WHERE id = ? AND user = ?
""",
(thewal, theid),
)
if not wallet_row:
return render_template("index.html")
db.execute("UPDATE wallets SET user = ? WHERE hash = ?", (f"del{wallet_row[4]}", wallet_row[0]))
db.execute("UPDATE wallets SET adminkey = ? WHERE hash = ?", (f"del{wallet_row[5]}", wallet_row[0]))
db.execute("UPDATE wallets SET inkey = ? WHERE hash = ?", (f"del{wallet_row[6]}", wallet_row[0]))
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (wallet_row[4],))
if user_wallets:
return render_template("deletewallet.html", theid=user_wallets[0][4], thewal=user_wallets[0][0])
next_wallet = db.fetchone("SELECT hash FROM wallets WHERE user = ?", (theid,))
if next_wallet:
return redirect(url_for("wallet", usr=theid, wal=next_wallet[0]))
return render_template("index.html")
@ -60,13 +63,13 @@ def lnurlwallet():
invoice = WALLET.create_invoice(withdraw_res.max_sats).json()
payment_hash = invoice["payment_hash"]
rrr = requests.get(
r = requests.get(
withdraw_res.callback.base,
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?"""
return render_template("index.html")
@ -76,112 +79,104 @@ def lnurlwallet():
data = r.json()
with Database() as db:
adminkey = encrypt(payment_hash)[0:20]
inkey = encrypt(adminkey)[0:20]
thewal = encrypt(inkey)[0:20]
theid = encrypt(thewal)[0:20]
thenme = "Bitcoin LN Wallet"
db.execute("INSERT INTO accounts (userhash) VALUES (?)", (theid,))
adminkey = encrypt(theid)
inkey = encrypt(adminkey)
adminkey = uuid.uuid4().hex
inkey = uuid.uuid4().hex
thewal = uuid.uuid4().hex
theid = uuid.uuid4().hex
thenme = DEFAULT_USER_WALLET_NAME
db.execute("INSERT INTO accounts (id) VALUES (?)", (theid,))
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),
)
return render_template(
"lnurlwallet.html",
len=len("1"),
walnme=thenme,
walbal=withdraw_res.max_sats,
theid=theid,
thewal=thewal,
adminkey=adminkey,
inkey=inkey,
)
return redirect(url_for("wallet", usr=theid, wal=thewal))
@app.route("/wallet")
def wallet():
theid = request.args.get("usr")
thewal = request.args.get("wal")
thenme = request.args.get("nme")
usr = request.args.get("usr")
wallet_id = request.args.get("wal")
wallet_name = request.args.get("nme") or DEFAULT_USER_WALLET_NAME
if not thewal:
return render_template("index.html")
# 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 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
# --------------------------------------
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)
if not usr:
usr = uuid.uuid4().hex
return redirect(url_for("wallet", usr=usr, wal=wallet_id, nme=wallet_name))
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(
"wallet.html",
len=1,
walnme=thenme,
walbal=0,
theid=theid,
thewal=thewal,
adminkey=adminkey,
inkey=inkey,
transactions=[],
"wallet.html", user_wallets=user_wallets, wallet=wallet, user=usr, transactions=transactions,
)
@ -205,12 +200,12 @@ def api_invoices():
return jsonify({"ERROR": "NO MEMO"}), 400
with Database() as db:
wallet_row = db.fetchone(
"SELECT hash FROM wallets WHERE inkey = ? OR adminkey = ?",
wallet = db.fetchone(
"SELECT id FROM wallets WHERE inkey = ? OR adminkey = ?",
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"],),
)
if not wallet_row:
if not wallet:
return jsonify({"ERROR": "NO KEY"}), 200
r = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
@ -218,10 +213,11 @@ def api_invoices():
pay_req = data["pay_req"]
payment_hash = data["payment_hash"]
amount_msat = int(postedjson["value"]) * 1000
db.execute(
"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
@ -238,39 +234,11 @@ def api_transactions():
return jsonify({"ERROR": "NO PAY REQ"}), 200
with Database() as db:
wallet_row = db.fetchone(
"SELECT hash FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],)
)
wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
if not wallet_row:
if not wallet:
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
invoice = bolt11.decode(data["payment_request"])
if invoice.amount_msat == 0:
@ -283,13 +251,13 @@ def api_transactions():
invoice.payment_hash,
-int(invoice.amount_msat),
-int(invoice.amount_msat * 0.01),
wallet_row[0],
wallet["id"],
invoice.description,
),
)
# 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:
return jsonify({"ERROR": "INSUFFICIENT BALANCE"}), 403
@ -321,24 +289,23 @@ def api_checkinvoice(payhash):
return jsonify({"ERROR": "MUST BE JSON"}), 200
with Database() as db:
payment_row = db.fetchone(
payment = db.fetchone(
"""
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 = ?
AND (w.adminkey = ? OR w.inkey = ?)
""",
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
)
if not payment_row:
if not payment:
return jsonify({"ERROR": "NO INVOICE"}), 404
if not payment_row[0]: # pending
if not payment["pending"]: # pending
return jsonify({"PAID": "TRUE"}), 200
r = WALLET.get_invoice_status(payhash)
if not r.ok:
return jsonify({"PAID": "FALSE"}), 400

View File

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

View File

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

View File

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

View File

@ -14,3 +14,4 @@ WALLET = LntxbotWallet(
LNBITS_PATH = os.path.dirname(os.path.realpath(__file__))
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"

View File

@ -239,6 +239,61 @@
></script>
</head>
<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>
</html>

View File

@ -1,146 +1,94 @@
<!-- @format -->
{% extends "base.html" %} {% block body %}
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="index.html" 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"></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">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 -->
<section class="content">
<div class="row">
<div class="col-md-4">
<!-- 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, its still very BETA and
may in fact do the opposite!
<br />
<a href="https://github.com/arcbtc/FOSSAW"
>https://github.com/arcbtc/lnbits</a
>
</p>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<div class="col-md-4">
<!-- Default box -->
<div class="box">
<div class="box-header">
<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>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</div>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<footer class="main-footer">
<div class="pull-right hidden-xs">
<b>BETA</b>
{% extends "base.html" %} {% block menuitems %}
<li>
<a href="/"><i class="fa fa-book"></i> Home</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="/"><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>
<strong
>Learn more about LNbits
<a href="https://github.com/arcbtc/FOSSAW"
>https://github.com/arcbtc/lnbits</a
></strong
>
</footer>
</section>
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-md-4">
<!-- 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, its still very BETA and may
in fact do the opposite!
<br />
<a href="https://github.com/arcbtc/FOSSAW"
>https://github.com/arcbtc/lnbits</a
>
</p>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<div class="col-md-4">
<!-- Default box -->
<div class="box">
<div class="box-header">
<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>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</div>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<script>
function makeid(length) {

File diff suppressed because it is too large Load Diff