refactor: decorators, models and more broken bits

This commit is contained in:
Eneko Illarramendi 2020-03-04 23:11:15 +01:00
parent ee70161854
commit f98a5040ac
69 changed files with 52715 additions and 1018 deletions

View File

@ -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
View File

@ -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__

View File

@ -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
View File

View 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()

View File

@ -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
View 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
View 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,4 @@
new Vue({
el: '#vue',
mixins: [windowMixin]
});

View File

@ -0,0 +1,14 @@
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
walletName: ''
};
},
methods: {
createWallet: function () {
LNbits.href.createWallet(this.walletName);
}
}
});

View 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);
});
});
}
}
});

View 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 %}

View 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, its 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 %}

View 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 %}

View File

@ -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, its 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 %}

View File

@ -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"))

View File

@ -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
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -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
View 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

View File

@ -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"]
}

View File

@ -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>

View File

@ -1,5 +1,6 @@
{
"name": "SHORT-NAME-FOR-EXTENSIONS-PAGE",
"short_description": "BLah blah blah.",
"ion_icon": "calendar"
"icon": "calendar",
"contributors": ["github_username"]
}

View File

@ -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>

View File

@ -1,5 +1,6 @@
{
"name": "TPOS",
"short_description": "A shareable POS!",
"ion_icon": "calculator"
"icon": "dialpad",
"contributors": ["talvasconcelos", "arcbtc"]
}

View File

@ -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>

View File

@ -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"]
}

View File

@ -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>

View File

@ -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):

View File

@ -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))

View 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; }

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

146
lnbits/static/js/base.js Normal file
View 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;
}));
}
}
};

View 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);
}
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View 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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

236
lnbits/static/vendor/bolt11/decoder.js vendored Normal file
View 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
View 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;
}

View 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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

11965
lnbits/static/vendor/vue@2.6.11/vue.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1055
lnbits/static/vendor/vuex@3.1.2/vuex.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

@ -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>

View File

@ -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 %}

View 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>

View 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 %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@ -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 %}

View File

@ -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)

View File

@ -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