diff --git a/lnbits/__init__.py b/lnbits/__init__.py index f1f53b83f..d2cc75321 100644 --- a/lnbits/__init__.py +++ b/lnbits/__init__.py @@ -1,41 +1,57 @@ -import uuid -import os import json +import os import requests -import re +import uuid -from flask import Flask, jsonify, render_template, request, redirect, url_for -from lnurl import Lnurl, LnurlWithdrawResponse, encode -from datetime import datetime +from flask import Flask, jsonify, redirect, render_template, request, url_for +from flask_talisman import Talisman +from lnurl import Lnurl, LnurlWithdrawResponse from . import bolt11 -from .db import Database, ExtDatabase, FauDatabase +from .core import core_app +from .db import open_db, open_ext_db +from .extensions.withdraw import withdraw_ext from .helpers import megajson from .settings import LNBITS_PATH, WALLET, DEFAULT_USER_WALLET_NAME, FEE_RESERVE app = Flask(__name__) +Talisman(app, content_security_policy={ + "default-src": [ + "'self'", + "'unsafe-eval'", + "'unsafe-inline'", + "cdnjs.cloudflare.com", + "code.ionicframework.com", + "code.jquery.com", + "fonts.googleapis.com", + "fonts.gstatic.com", + "maxcdn.bootstrapcdn.com", + ] +}) + +# filters app.jinja_env.filters["megajson"] = megajson +# blueprints +app.register_blueprint(core_app) +app.register_blueprint(withdraw_ext, url_prefix="/withdraw") + @app.before_first_request def init(): - with Database() as db: + with open_db() as db: with open(os.path.join(LNBITS_PATH, "data", "schema.sql")) as schemafile: for stmt in schemafile.read().split(";\n\n"): db.execute(stmt, []) -@app.route("/") -def home(): - return render_template("index.html") - @app.route("/deletewallet") def deletewallet(): user_id = request.args.get("usr") wallet_id = request.args.get("wal") - with Database() as db: + with open_db() as db: db.execute( """ UPDATE wallets AS w @@ -94,7 +110,7 @@ def lnurlwallet(): data = r.json() break - with Database() as db: + with open_db() as db: wallet_id = uuid.uuid4().hex user_id = uuid.uuid4().hex wallet_name = DEFAULT_USER_WALLET_NAME @@ -135,7 +151,7 @@ def wallet(): # just wallet_name: create a user, then generate a wallet_id and create # nothing: create everything - with Database() as db: + with open_db() as db: # ensure this user exists # ------------------------------- @@ -224,15 +240,15 @@ def wallet(): ) -@app.route("/v1/invoices", methods=["GET", "POST"]) +@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(): + # Form validation + if int(postedjson["value"]) < 0 or not postedjson["memo"].replace(" ", "").isalnum(): return jsonify({"ERROR": "FORM ERROR"}), 401 if "value" not in postedjson: @@ -247,7 +263,7 @@ def api_invoices(): if "memo" not in postedjson: return jsonify({"ERROR": "NO MEMO"}), 400 - with Database() as db: + 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"],), @@ -271,7 +287,7 @@ def api_invoices(): return jsonify({"pay_req": pay_req, "payment_hash": pay_hash}), 200 -@app.route("/v1/channels/transactions", methods=["GET", "POST"]) +@app.route("/api/v1/channels/transactions", methods=["GET", "POST"]) def api_transactions(): if request.headers["Content-Type"] != "application/json": @@ -283,7 +299,7 @@ def api_transactions(): if "payment_request" not in data: return jsonify({"ERROR": "NO PAY REQ"}), 400 - with Database() as db: + with open_db() as db: wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],)) if not wallet: @@ -332,12 +348,12 @@ def api_transactions(): return jsonify({"PAID": "TRUE", "payment_hash": invoice.payment_hash}), 200 -@app.route("/v1/invoice/", methods=["GET"]) +@app.route("/api/v1/invoice/", methods=["GET"]) def api_checkinvoice(payhash): if request.headers["Content-Type"] != "application/json": return jsonify({"ERROR": "MUST BE JSON"}), 400 - with Database() as db: + with open_db() as db: payment = db.fetchone( """ SELECT pending @@ -362,12 +378,12 @@ def api_checkinvoice(payhash): return jsonify({"PAID": "TRUE"}), 200 -@app.route("/v1/payment/", methods=["GET"]) +@app.route("/api/v1/payment/", methods=["GET"]) def api_checkpayment(payhash): if request.headers["Content-Type"] != "application/json": return jsonify({"ERROR": "MUST BE JSON"}), 400 - with Database() as db: + with open_db() as db: payment = db.fetchone( """ SELECT pending @@ -392,9 +408,9 @@ def api_checkpayment(payhash): return jsonify({"PAID": "TRUE"}), 200 -@app.route("/v1/checkpending", methods=["POST"]) +@app.route("/api/v1/checkpending", methods=["POST"]) def api_checkpending(): - with Database() as db: + with open_db() as db: for pendingtx in db.fetchall( """ SELECT @@ -427,10 +443,6 @@ def api_checkpending(): return "" - - -###########EXTENSIONS STUFF - ADD TO LNFAUCET FOLDER IF POSS - # Checks DB to see if the extensions are activated or not activated for the user @app.route("/extensions") def extensions(): @@ -441,15 +453,14 @@ def extensions(): if usr: if not len(usr) > 20: return redirect(url_for("home")) - - with Database() as db: + with open_db() as db: user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,)) - with ExtDatabase() as Extdd: - user_ext = Extdd.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) + with open_ext_db() as ext_db: + user_ext = ext_db.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) if not user_ext: - Extdd.execute( + ext_db.execute( """ INSERT OR IGNORE INTO overview (user) VALUES (?) """, @@ -459,283 +470,15 @@ def extensions(): if lnevents: if int(lnevents) != user_ext[0][1] and int(lnevents) < 2: - Extdd.execute("UPDATE overview SET lnevents = ? WHERE user = ?", (int(lnevents),usr,)) - user_ext = Extdd.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) + ext_db.execute("UPDATE overview SET lnevents = ? WHERE user = ?", (int(lnevents), usr,)) + user_ext = ext_db.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) if lnjoust: if int(lnjoust) != user_ext[0][2] and int(lnjoust) < 2: - Extdd.execute("UPDATE overview SET lnjoust = ? WHERE user = ?", (int(lnjoust),usr,)) - user_ext = Extdd.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) + ext_db.execute("UPDATE overview SET lnjoust = ? WHERE user = ?", (int(lnjoust), usr,)) + user_ext = ext_db.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) if withdraw: if int(withdraw) != user_ext[0][3] and int(withdraw) < 2: - Extdd.execute("UPDATE overview SET withdraw = ? WHERE user = ?", (int(withdraw),usr,)) - user_ext = Extdd.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) + ext_db.execute("UPDATE overview SET withdraw = ? WHERE user = ?", (int(withdraw), usr,)) + user_ext = ext_db.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) - return render_template( - "extensions.html", user_wallets=user_wallets, user=usr, user_ext=user_ext - ) - - -# Main withdraw link page -@app.route("/withdraw") -def withdraw(): - usr = request.args.get("usr") - fauc = request.args.get("fauc") - - if usr: - if not len(usr) > 20: - return redirect(url_for("home")) - - #Get all the data - with Database() as db: - user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,)) - - with ExtDatabase() as Extdd: - user_ext = Extdd.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,)) - - #If del is selected by user from withdraw page, the withdraw link is to be deleted - faudel = request.args.get("del") - if faudel: - Faudb.execute("DELETE FROM withdraws WHERE uni = ?", (faudel,)) - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,)) - - return render_template( - "withdraw.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau - ) - - -#Returns encoded LNURL if web url and parameter gieven -@app.route("/v1/lnurlencode//", methods=["GET"]) -def api_lnurlencode(urlstr, parstr): - - if not urlstr: - return jsonify({"STATUS": "FALSE"}), 200 - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,)) - randar = user_fau[0][15].split(",") - # randar = randar[:-1] - print(int(user_fau[0][10])-1) - #If "Unique links" selected get correct rand, if not there is only one rand - if user_fau[0][12] > 0: - rand = randar[user_fau[0][10]-2] - print(rand) - else: - rand = randar[0] - - lnurlstr = encode( "https://" + urlstr + "/v1/lnurlfetch/" + urlstr + "/" + parstr + "/" + rand) - - return jsonify({"STATUS": "TRUE", "LNURL": lnurlstr}), 200 - - -#Returns LNURL json -@app.route("/v1/lnurlfetch///", methods=["GET"]) -def api_lnurlfetch(parstr, urlstr, rand): - - if not parstr: - return jsonify({"STATUS": "FALSE", "ERROR": "NO WALL ID"}), 200 - - if not urlstr: - - return jsonify({"STATUS": "FALSE", "ERROR": "NO URL"}), 200 - - with FauDatabase() as Faudb: - - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,)) - print(user_fau[0][0]) - - k1str = uuid.uuid4().hex - - Faudb.execute("UPDATE withdraws SET withdrawals = ? WHERE uni = ?", (k1str ,parstr,)) - - res = LnurlWithdrawResponse( - callback='https://' + urlstr + '/v1/lnurlwithdraw/' + rand + '/', - k1=k1str, - min_withdrawable=user_fau[0][8]*1000, - max_withdrawable=user_fau[0][7]*1000, - default_description="LNURL withdraw", - ) - print("res") - return res.json(), 200 - - - -#Pays invoice if passed k1 invoice and rand -@app.route("/v1/lnurlwithdraw//", methods=["GET"]) -def api_lnurlwithdraw(rand): - k1 = request.args.get("k1") - pr = request.args.get("pr") - print(rand) - - - if not k1: - return jsonify({"STATUS": "FALSE", "ERROR": "NO k1"}), 200 - - if not pr: - return jsonify({"STATUS": "FALSE", "ERROR": "NO PR"}), 200 - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,)) - - if not user_fau: - return jsonify({"status":"ERROR", "reason":"NO AUTH"}), 400 - - if user_fau[0][10] <1: - return jsonify({"status":"ERROR", "reason":"withdraw SPENT"}), 400 - - # Check withdraw time - dt = datetime.now() - seconds = dt.timestamp() - secspast = seconds - user_fau[0][14] - print(secspast) - - if secspast < user_fau[0][11]: - return jsonify({"status":"ERROR", "reason":"WAIT " + str(int(user_fau[0][11] - secspast)) + "s"}), 400 - - randar = user_fau[0][15].split(",") - if rand not in randar: - return jsonify({"status":"ERROR", "reason":"BAD AUTH"}), 400 - if len(randar) > 2: - randar.remove(rand) - randstr = ','.join(randar) - - print(randstr) - - # Update time and increments - upinc = (int(user_fau[0][10]) - 1) - Faudb.execute("UPDATE withdraws SET inc = ?, rand = ?, tmestmp = ? WHERE withdrawals = ?", (upinc, randstr, seconds, k1,)) - - header = {'Content-Type': 'application/json','Grpc-Metadata-macaroon':str(user_fau[0][4])} - - data = {'payment_request': pr} - - r = requests.post(url = "https://lnbits.com/v1/channels/transactions", headers=header, data=json.dumps(data)) - - r_json=r.json() - if "ERROR" in r_json: - return jsonify({"status":"ERROR", "reason":r_json["ERROR"]}), 400 - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,)) - - return jsonify({"status":"OK"}), 200 - - - -@app.route("/withdrawmaker", methods=["GET", "POST"]) -def withdrawmaker(): - data = request.json - amt = data["amt"] - tit = data["tit"] - wal = data["wal"] - minamt = data["minamt"] - maxamt = data["maxamt"] - tme = data["tme"] - uniq = data["uniq"] - usr = data["usr"] - wall = wal.split("-") - - #Form validation - if int(amt) < 0 or not tit.replace(' ','').isalnum() or wal == "" or int(minamt) < 0 or int(maxamt) < 0 or int(minamt) > int(maxamt) or int(tme) < 0: - return jsonify({"ERROR": "FORM ERROR"}), 401 - - #If id that means its a link being edited, delet the record first - if "id" in data: - unid = data["id"].split("-") - uni = unid[1] - print(data["id"]) - print(uni) - with FauDatabase() as Faudb: - Faudb.execute("DELETE FROM withdraws WHERE uni = ?", (unid[1],)) - else: - uni = uuid.uuid4().hex - - # Randomiser for random QR option - rand = "" - if uniq > 0: - for x in range(0,int(amt)): - rand += uuid.uuid4().hex[0:5] + "," - else: - rand = uuid.uuid4().hex[0:5] + "," - - with Database() as dbb: - user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr,wall[1],)) - if not user_wallets: - return jsonify({"ERROR": "NO WALLET USER"}), 401 - - # Get time - dt = datetime.now() - seconds = dt.timestamp() - print(seconds) - - #Add to DB - with FauDatabase() as db: - db.execute( - "INSERT OR IGNORE INTO withdraws (usr, wal, walnme, adm, uni, tit, maxamt, minamt, spent, inc, tme, uniq, withdrawals, tmestmp, rand) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - ( - usr, - wall[1], - user_wallets[0][1], - user_wallets[0][3], - uni, - tit, - maxamt, - minamt, - 0, - amt, - tme, - uniq, - 0, - seconds, - rand, - ), - ) - - #Get updated records - with ExtDatabase() as Extdd: - user_ext = Extdd.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) - if not user_ext: - return jsonify({"ERROR": "NO WALLET USER"}), 401 - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,)) - if not user_fau: - return jsonify({"ERROR": "NO WALLET USER"}), 401 - - - return render_template( - "withdraw.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau - ) - - -#Simple shareable link -@app.route("/displaywithdraw", methods=["GET", "POST"]) -def displaywithdraw(): - fauid = request.args.get("id") - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE uni = ?", (fauid,)) - - return render_template( - "displaywithdraw.html", user_fau=user_fau, - ) - -#Simple printable page of links -@app.route("/printwithdraw//", methods=["GET", "POST"]) -def printwithdraw(urlstr): - fauid = request.args.get("id") - - with FauDatabase() as Faudb: - user_fau = Faudb.fetchall("SELECT * FROM withdraws WHERE uni = ?", (fauid,)) - randar = user_fau[0][15].split(",") - randar = randar[:-1] - lnurlar = [] - print(len(randar)) - for d in range(len(randar)): - lnurlar.append( encode("https://"+ urlstr +"/v1/lnurlfetch/" + urlstr + "/" + fauid + "/" + randar[d])) - - return render_template( - "printwithdraws.html", lnurlar=lnurlar, user_fau=user_fau[0], - ) + return render_template("extensions.html", user_wallets=user_wallets, user=usr, user_ext=user_ext) diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 27c5307ef..32ab5ee90 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -49,9 +49,9 @@ def decode(pr: str) -> Invoice: if tag == "d": invoice.description = trim_to_bytes(tagdata).decode("utf-8") elif tag == "h" and data_length == 52: - invoice.description = hexlify(trim_to_bytes(tagdata)).decode('ascii') + invoice.description = hexlify(trim_to_bytes(tagdata)).decode("ascii") elif tag == "p" and data_length == 52: - invoice.payment_hash = hexlify(trim_to_bytes(tagdata)).decode('ascii') + invoice.payment_hash = hexlify(trim_to_bytes(tagdata)).decode("ascii") return invoice diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py new file mode 100644 index 000000000..84f71954b --- /dev/null +++ b/lnbits/core/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +core_app = Blueprint("core", __name__, template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/core/static/favicon.ico b/lnbits/core/static/favicon.ico new file mode 100644 index 000000000..00af0ce26 Binary files /dev/null and b/lnbits/core/static/favicon.ico differ diff --git a/lnbits/templates/index.html b/lnbits/core/templates/index.html similarity index 97% rename from lnbits/templates/index.html rename to lnbits/core/templates/index.html index 05f9640a1..490f7b8b1 100644 --- a/lnbits/templates/index.html +++ b/lnbits/core/templates/index.html @@ -13,7 +13,7 @@


diff --git a/lnbits/core/views.py b/lnbits/core/views.py new file mode 100644 index 000000000..4626412ad --- /dev/null +++ b/lnbits/core/views.py @@ -0,0 +1,14 @@ +from flask import render_template, send_from_directory +from os import path + +from lnbits.core import core_app + + +@core_app.route("/favicon.ico") +def favicon(): + return send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico") + + +@core_app.route("/") +def home(): + return render_template("index.html") diff --git a/lnbits/core/views_api.py b/lnbits/core/views_api.py new file mode 100644 index 000000000..e69de29bb diff --git a/lnbits/db.py b/lnbits/db.py index fb7abfc9f..fd7b21cad 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -1,40 +1,13 @@ -import sqlite3 import os -from .settings import DATABASE_PATH -from .settings import LNBITS_PATH +import sqlite3 + +from typing import Optional + +from .settings import DATABASE_PATH, LNBITS_PATH class Database: - def __init__(self, db_path: str = DATABASE_PATH): - self.path = db_path - self.connection = sqlite3.connect(db_path) - self.connection.row_factory = sqlite3.Row - self.cursor = self.connection.cursor() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.cursor.close() - self.connection.close() - - def fetchall(self, query: str, values: tuple) -> list: - """Given a query, return cursor.fetchall() rows.""" - self.cursor.execute(query, values) - return self.cursor.fetchall() - - def fetchone(self, query: str, values: tuple): - self.cursor.execute(query, values) - return self.cursor.fetchone() - - def execute(self, query: str, values: tuple) -> None: - """Given a query, cursor.execute() it.""" - self.cursor.execute(query, values) - self.connection.commit() - - -class ExtDatabase: - def __init__(self, db_path: str = os.path.join(LNBITS_PATH, "extensions", "overview.sqlite3")): + def __init__(self, db_path: str): self.path = db_path self.connection = sqlite3.connect(db_path) self.connection.row_factory = sqlite3.Row @@ -62,31 +35,11 @@ class ExtDatabase: self.connection.commit() -class FauDatabase: - def __init__(self, db_path: str = os.path.join(LNBITS_PATH, "extensions", "faucet", "database.sqlite3")): - self.path = db_path - self.connection = sqlite3.connect(db_path) - self.connection.row_factory = sqlite3.Row - self.cursor = self.connection.cursor() +def open_db(db_path: str = DATABASE_PATH) -> Database: + return Database(db_path=db_path) - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.cursor.close() - self.connection.close() - - def fetchall(self, query: str, values: tuple) -> list: - """Given a query, return cursor.fetchall() rows.""" - self.cursor.execute(query, values) - return self.cursor.fetchall() - - def fetchone(self, query: str, values: tuple): - self.cursor.execute(query, values) - return self.cursor.fetchone() - - def execute(self, query: str, values: tuple) -> None: - """Given a query, cursor.execute() it.""" - self.cursor.execute(query, values) - self.connection.commit() +def open_ext_db(extension: Optional[str] = None) -> Database: + if extension: + return open_db(os.path.join(LNBITS_PATH, "extensions", extension, "database.sqlite3")) + return open_db(os.path.join(LNBITS_PATH, "extensions", "overview.sqlite3")) diff --git a/lnbits/extensions/events/README.md b/lnbits/extensions/events/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lnbits/extensions/joust/README.md b/lnbits/extensions/joust/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lnbits/extensions/lnevents/README.md b/lnbits/extensions/lnevents/README.md deleted file mode 100644 index 1d2f1341f..000000000 --- a/lnbits/extensions/lnevents/README.md +++ /dev/null @@ -1,73 +0,0 @@ - -![Lightning network wallet](https://i.imgur.com/arUWZbH.png) -# LNbits -Simple free and open-source Python lightning-network wallet/accounts system. Use https://lnbits.com, or run your own LNbits server! - -LNbits is a very simple server that sits on top of a funding source, and can be used as: -* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet! -* Fallback wallet for the LNURL scheme -* Instant wallet for LN demonstrations - -The wallet can run on top of any lightning-network funding source such as LND, lntxbot, paywall, opennode, etc. This first BETA release runs on top of lntxbot, other releases coming soon, although if you are impatient, it could be ported to other funding sources relatively easily. Contributors very welcome :) - -LNbits is still in BETA. Please report any vulnerabilities responsibly -## LNbits as an account system -LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come.. - - -![Lightning network wallet](https://i.imgur.com/Sd4ri3T.png) - -Each wallet also comes with its own API keys, to help partition the exposure of your funding source. - -(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats) - -![lnurl ATM](https://i.imgur.com/ABruzAn.png) - -## LNbits as an LNURL-withdraw fallback -LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the LNURL-withdraw. - -![lnurl fallback](https://i.imgur.com/CPBKHIv.png) -https://github.com/btcontract/lnurl-rfc/blob/master/spec.md - -Adding **/lnurl?lightning="LNURL-WITHDRAW"** will trigger a withdraw that builds an LNbits wallet. -Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds. - -![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg) - -## LNbits as an insta-wallet -Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon). -"Go to this website", has a lot less friction than "Download this app". - -![lnurl ATM](https://i.imgur.com/SF5KoIe.png) - -# Running LNbits locally -Download this repo - -LNbits uses [Flask](http://flask.pocoo.org/). -Feel free to contribute to the project. - -Application dependencies ------------------------- -The application uses [Pipenv][pipenv] to manage Python packages. -While in development, you will need to install all dependencies: - - $ pipenv shell - $ pipenv install --dev - -You will need to set the variables in .env.example, and rename the file to .env - -![lnurl ATM](https://i.imgur.com/ri2zOe8.png) - -Running the server ------------------- - - $ flask run - -There is an environment variable called `FLASK_ENV` that has to be set to `development` -if you want to run Flask in debug mode with autoreload - -[pipenv]: https://docs.pipenv.org/#install-pipenv-today - -# Tip me -If you like this project and might even use or extend it, why not send some tip love! -https://paywall.link/to/f4e4e diff --git a/lnbits/extensions/lnjoust/README.md b/lnbits/extensions/lnjoust/README.md deleted file mode 100644 index 1d2f1341f..000000000 --- a/lnbits/extensions/lnjoust/README.md +++ /dev/null @@ -1,73 +0,0 @@ - -![Lightning network wallet](https://i.imgur.com/arUWZbH.png) -# LNbits -Simple free and open-source Python lightning-network wallet/accounts system. Use https://lnbits.com, or run your own LNbits server! - -LNbits is a very simple server that sits on top of a funding source, and can be used as: -* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet! -* Fallback wallet for the LNURL scheme -* Instant wallet for LN demonstrations - -The wallet can run on top of any lightning-network funding source such as LND, lntxbot, paywall, opennode, etc. This first BETA release runs on top of lntxbot, other releases coming soon, although if you are impatient, it could be ported to other funding sources relatively easily. Contributors very welcome :) - -LNbits is still in BETA. Please report any vulnerabilities responsibly -## LNbits as an account system -LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come.. - - -![Lightning network wallet](https://i.imgur.com/Sd4ri3T.png) - -Each wallet also comes with its own API keys, to help partition the exposure of your funding source. - -(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats) - -![lnurl ATM](https://i.imgur.com/ABruzAn.png) - -## LNbits as an LNURL-withdraw fallback -LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the LNURL-withdraw. - -![lnurl fallback](https://i.imgur.com/CPBKHIv.png) -https://github.com/btcontract/lnurl-rfc/blob/master/spec.md - -Adding **/lnurl?lightning="LNURL-WITHDRAW"** will trigger a withdraw that builds an LNbits wallet. -Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds. - -![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg) - -## LNbits as an insta-wallet -Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon). -"Go to this website", has a lot less friction than "Download this app". - -![lnurl ATM](https://i.imgur.com/SF5KoIe.png) - -# Running LNbits locally -Download this repo - -LNbits uses [Flask](http://flask.pocoo.org/). -Feel free to contribute to the project. - -Application dependencies ------------------------- -The application uses [Pipenv][pipenv] to manage Python packages. -While in development, you will need to install all dependencies: - - $ pipenv shell - $ pipenv install --dev - -You will need to set the variables in .env.example, and rename the file to .env - -![lnurl ATM](https://i.imgur.com/ri2zOe8.png) - -Running the server ------------------- - - $ flask run - -There is an environment variable called `FLASK_ENV` that has to be set to `development` -if you want to run Flask in debug mode with autoreload - -[pipenv]: https://docs.pipenv.org/#install-pipenv-today - -# Tip me -If you like this project and might even use or extend it, why not send some tip love! -https://paywall.link/to/f4e4e diff --git a/lnbits/extensions/withdraw/README.md b/lnbits/extensions/withdraw/README.md index 1d2f1341f..e69de29bb 100644 --- a/lnbits/extensions/withdraw/README.md +++ b/lnbits/extensions/withdraw/README.md @@ -1,73 +0,0 @@ - -![Lightning network wallet](https://i.imgur.com/arUWZbH.png) -# LNbits -Simple free and open-source Python lightning-network wallet/accounts system. Use https://lnbits.com, or run your own LNbits server! - -LNbits is a very simple server that sits on top of a funding source, and can be used as: -* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet! -* Fallback wallet for the LNURL scheme -* Instant wallet for LN demonstrations - -The wallet can run on top of any lightning-network funding source such as LND, lntxbot, paywall, opennode, etc. This first BETA release runs on top of lntxbot, other releases coming soon, although if you are impatient, it could be ported to other funding sources relatively easily. Contributors very welcome :) - -LNbits is still in BETA. Please report any vulnerabilities responsibly -## LNbits as an account system -LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come.. - - -![Lightning network wallet](https://i.imgur.com/Sd4ri3T.png) - -Each wallet also comes with its own API keys, to help partition the exposure of your funding source. - -(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats) - -![lnurl ATM](https://i.imgur.com/ABruzAn.png) - -## LNbits as an LNURL-withdraw fallback -LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the LNURL-withdraw. - -![lnurl fallback](https://i.imgur.com/CPBKHIv.png) -https://github.com/btcontract/lnurl-rfc/blob/master/spec.md - -Adding **/lnurl?lightning="LNURL-WITHDRAW"** will trigger a withdraw that builds an LNbits wallet. -Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds. - -![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg) - -## LNbits as an insta-wallet -Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon). -"Go to this website", has a lot less friction than "Download this app". - -![lnurl ATM](https://i.imgur.com/SF5KoIe.png) - -# Running LNbits locally -Download this repo - -LNbits uses [Flask](http://flask.pocoo.org/). -Feel free to contribute to the project. - -Application dependencies ------------------------- -The application uses [Pipenv][pipenv] to manage Python packages. -While in development, you will need to install all dependencies: - - $ pipenv shell - $ pipenv install --dev - -You will need to set the variables in .env.example, and rename the file to .env - -![lnurl ATM](https://i.imgur.com/ri2zOe8.png) - -Running the server ------------------- - - $ flask run - -There is an environment variable called `FLASK_ENV` that has to be set to `development` -if you want to run Flask in debug mode with autoreload - -[pipenv]: https://docs.pipenv.org/#install-pipenv-today - -# Tip me -If you like this project and might even use or extend it, why not send some tip love! -https://paywall.link/to/f4e4e diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py new file mode 100644 index 000000000..f1e1c2697 --- /dev/null +++ b/lnbits/extensions/withdraw/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +withdraw_ext = Blueprint("withdraw", __name__, static_folder="static", template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py new file mode 100644 index 000000000..e69de29bb diff --git a/lnbits/extensions/withdraw/index.html b/lnbits/extensions/withdraw/index.html deleted file mode 100644 index 155038ead..000000000 --- a/lnbits/extensions/withdraw/index.html +++ /dev/null @@ -1,233 +0,0 @@ - - -{% extends "base.html" %} {% block messages %} - - - ! - - -{% endblock %} {% block menuitems %} -
  • - - Wallets - - - -
  • - -
  • - - Extensions - -
      - - - {% if user_ext[0][1] %} -
    • - LNEvents -
    • - {% endif %} - - {% if user_ext[0][2] %} -
    • - LNJoust -
    • - {% endif %} - - {% if user_ext[0][3] %} -
    • - Faucet -
    • - {% endif %} - - -
    -
  • - -{% endblock %} {% block body %} - -
    - -
    -

    - Wallet - Control panel -
    -

    - -

    -
    -

    - Bookmark to save your wallet. Wallet is in BETA, use with caution. -

    -
    -
    - - -
    - -
    - - {% if not user_ext[0][2] %} -
    - -
    -
    -

    - LNJoust -

    -

    - LN powered Joust gamesmaster -

    -
    -
    - -
    - - - Activate - -
    -
    - - {% else %} -
    - -
    -
    -

    - LNJoust -

    -

    - LN powered Joust gamesmaster -

    -
    -
    - -
    - - Deactivate - -
    -
    - {% endif %} - {% if not user_ext[0][1] %} -
    - -
    -
    -

    - LNEvents -

    -

    - Lightning powered tickets -

    -
    -
    - -
    - - Activate - -
    -
    - - {% else %} -
    - -
    -
    -

    - LNEvents -

    -

    - Lightning powered tickets -

    -
    -
    - -
    - - Deactivate - -
    -
    - {% endif %} - {% if not user_ext[0][3] %} -
    - -
    -
    -

    - Faucet -

    -

    - Make LNURL faucets -

    -
    -
    - -
    - - Activate - -
    -
    - {% else %} -
    - -
    -
    -

    - Faucet -

    -

    - Make LNURL faucets -

    -
    -
    - -
    - - Deactivate - -
    -
    - {% endif %} -
    - - -
    - - -
    -{% endblock %} diff --git a/lnbits/templates/displaywithdraw.html b/lnbits/extensions/withdraw/templates/withdraw/display.html similarity index 85% rename from lnbits/templates/displaywithdraw.html rename to lnbits/extensions/withdraw/templates/withdraw/display.html index cc90344aa..cb6846d12 100644 --- a/lnbits/templates/displaywithdraw.html +++ b/lnbits/extensions/withdraw/templates/withdraw/display.html @@ -23,7 +23,7 @@ /> @@ -34,7 +34,7 @@ media="screen" href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}" /> - - - - - - - - - - @@ -142,7 +121,7 @@ type="text/javascript" > - + - - - - - - {% endblock %} diff --git a/lnbits/templates/printwithdraws.html b/lnbits/extensions/withdraw/templates/withdraw/print.html similarity index 85% rename from lnbits/templates/printwithdraws.html rename to lnbits/extensions/withdraw/templates/withdraw/print.html index 16ae475b3..3a4827839 100644 --- a/lnbits/templates/printwithdraws.html +++ b/lnbits/extensions/withdraw/templates/withdraw/print.html @@ -21,7 +21,7 @@ /> @@ -32,7 +32,7 @@ media="screen" href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}" /> - - - - - - - - - - @@ -143,7 +122,7 @@ type="text/javascript" > - + - - - - - - - \ No newline at end of file + diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py new file mode 100644 index 000000000..ed30fa924 --- /dev/null +++ b/lnbits/extensions/withdraw/views.py @@ -0,0 +1,160 @@ +import uuid + +from flask import jsonify, render_template, request, redirect, url_for +from lnurl import encode as lnurl_encode +from datetime import datetime + +from lnbits.db import open_db, open_ext_db +from lnbits.extensions.withdraw import withdraw_ext + + +@withdraw_ext.route("/") +def index(): + """Main withdraw link page.""" + + usr = request.args.get("usr") + + if usr: + if not len(usr) > 20: + return redirect(url_for("home")) + + # Get all the data + with open_db() as db: + user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,)) + + with open_ext_db() as ext_db: + user_ext = ext_db.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,)) + + # If del is selected by user from withdraw page, the withdraw link is to be deleted + faudel = request.args.get("del") + if faudel: + withdraw_ext_db.execute("DELETE FROM withdraws WHERE uni = ?", (faudel,)) + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,)) + + return render_template( + "withdraw/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau + ) + + +@withdraw_ext.route("/create", methods=["GET", "POST"]) +def create(): + """.""" + + data = request.json + amt = data["amt"] + tit = data["tit"] + wal = data["wal"] + minamt = data["minamt"] + maxamt = data["maxamt"] + tme = data["tme"] + uniq = data["uniq"] + usr = data["usr"] + wall = wal.split("-") + + # Form validation + if ( + int(amt) < 0 + or not tit.replace(" ", "").isalnum() + or wal == "" + or int(minamt) < 0 + or int(maxamt) < 0 + or int(minamt) > int(maxamt) + or int(tme) < 0 + ): + return jsonify({"ERROR": "FORM ERROR"}), 401 + + # If id that means its a link being edited, delet the record first + if "id" in data: + unid = data["id"].split("-") + uni = unid[1] + with open_ext_db("withdraw") as withdraw_ext_db: + withdraw_ext_db.execute("DELETE FROM withdraws WHERE uni = ?", (unid[1],)) + else: + uni = uuid.uuid4().hex + + # Randomiser for random QR option + rand = "" + if uniq > 0: + for x in range(0, int(amt)): + rand += uuid.uuid4().hex[0:5] + "," + else: + rand = uuid.uuid4().hex[0:5] + "," + + with open_db() as dbb: + user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr, wall[1],)) + if not user_wallets: + return jsonify({"ERROR": "NO WALLET USER"}), 401 + + # Get time + dt = datetime.now() + seconds = dt.timestamp() + + # Add to DB + with open_ext_db("withdraw") as db: + db.execute( + "INSERT OR IGNORE INTO withdraws (usr, wal, walnme, adm, uni, tit, maxamt, minamt, spent, inc, tme, uniq, withdrawals, tmestmp, rand) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + usr, + wall[1], + user_wallets[0][1], + user_wallets[0][3], + uni, + tit, + maxamt, + minamt, + 0, + amt, + tme, + uniq, + 0, + seconds, + rand, + ), + ) + + # Get updated records + with open_ext_db() as ext_db: + user_ext = ext_db.fetchall("SELECT * FROM overview WHERE user = ?", (usr,)) + if not user_ext: + return jsonify({"ERROR": "NO WALLET USER"}), 401 + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE usr = ?", (usr,)) + if not user_fau: + return jsonify({"ERROR": "NO WALLET USER"}), 401 + + return render_template( + "withdraw/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_fau=user_fau + ) + + +@withdraw_ext.route("/display", methods=["GET", "POST"]) +def display(): + """Simple shareable link.""" + fauid = request.args.get("id") + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (fauid,)) + + return render_template("withdraw/display.html", user_fau=user_fau,) + + +@withdraw_ext.route("/print//", methods=["GET", "POST"]) +def print_qr(urlstr): + """Simple printable page of links.""" + fauid = request.args.get("id") + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (fauid,)) + randar = user_fau[0][15].split(",") + randar = randar[:-1] + lnurlar = [] + + for d in range(len(randar)): + url = url_for("withdraw.api_lnurlfetch", _external=True, urlstr=urlstr, parstr=fauid, rand=randar[d]) + lnurlar.append(lnurl_encode(url.replace("http", "https"))) + + return render_template("withdraw/print.html", lnurlar=lnurlar, user_fau=user_fau[0],) diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py new file mode 100644 index 000000000..3cc8073fe --- /dev/null +++ b/lnbits/extensions/withdraw/views_api.py @@ -0,0 +1,116 @@ +import uuid +import json +import requests + +from flask import jsonify, request, url_for +from lnurl import LnurlWithdrawResponse, encode as lnurl_encode +from datetime import datetime + +from lnbits.db import open_ext_db +from lnbits.extensions.withdraw import withdraw_ext + + +@withdraw_ext.route("/api/v1/lnurlencode//", methods=["GET"]) +def api_lnurlencode(urlstr, parstr): + """Returns encoded LNURL if web url and parameter gieven.""" + + if not urlstr: + return jsonify({"status": "FALSE"}), 200 + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,)) + randar = user_fau[0][15].split(",") + # randar = randar[:-1] + # If "Unique links" selected get correct rand, if not there is only one rand + if user_fau[0][12] > 0: + rand = randar[user_fau[0][10] - 2] + else: + rand = randar[0] + + url = url_for("withdraw.api_lnurlfetch", _external=True, urlstr=urlstr, parstr=parstr, rand=rand) + + return jsonify({"status": "TRUE", "lnurl": lnurl_encode(url.replace("http", "https"))}), 200 + + +@withdraw_ext.route("/api/v1/lnurlfetch///", methods=["GET"]) +def api_lnurlfetch(parstr, urlstr, rand): + """Returns LNURL json.""" + + if not parstr: + return jsonify({"status": "FALSE", "ERROR": "NO WALL ID"}), 200 + + if not urlstr: + + return jsonify({"status": "FALSE", "ERROR": "NO URL"}), 200 + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE uni = ?", (parstr,)) + k1str = uuid.uuid4().hex + withdraw_ext_db.execute("UPDATE withdraws SET withdrawals = ? WHERE uni = ?", (k1str, parstr,)) + + res = LnurlWithdrawResponse( + callback=url_for("withdraw.api_lnurlwithdraw", _external=True, rand=rand).replace("http", "https"), + k1=k1str, + min_withdrawable=user_fau[0][8] * 1000, + max_withdrawable=user_fau[0][7] * 1000, + default_description="LNbits LNURL withdraw", + ) + + return res.json(), 200 + + +@withdraw_ext.route("/api/v1/lnurlwithdraw//", methods=["GET"]) +def api_lnurlwithdraw(rand): + """Pays invoice if passed k1 invoice and rand.""" + + k1 = request.args.get("k1") + pr = request.args.get("pr") + + if not k1: + return jsonify({"status": "FALSE", "ERROR": "NO k1"}), 200 + + if not pr: + return jsonify({"status": "FALSE", "ERROR": "NO PR"}), 200 + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,)) + + if not user_fau: + return jsonify({"status": "ERROR", "reason": "NO AUTH"}), 400 + + if user_fau[0][10] < 1: + return jsonify({"status": "ERROR", "reason": "withdraw SPENT"}), 400 + + # Check withdraw time + dt = datetime.now() + seconds = dt.timestamp() + secspast = seconds - user_fau[0][14] + + if secspast < user_fau[0][11]: + return jsonify({"status": "ERROR", "reason": "WAIT " + str(int(user_fau[0][11] - secspast)) + "s"}), 400 + + randar = user_fau[0][15].split(",") + if rand not in randar: + return jsonify({"status": "ERROR", "reason": "BAD AUTH"}), 400 + if len(randar) > 2: + randar.remove(rand) + randstr = ",".join(randar) + + # Update time and increments + upinc = int(user_fau[0][10]) - 1 + withdraw_ext_db.execute( + "UPDATE withdraws SET inc = ?, rand = ?, tmestmp = ? WHERE withdrawals = ?", (upinc, randstr, seconds, k1,) + ) + + header = {"Content-Type": "application/json", "Grpc-Metadata-macaroon": str(user_fau[0][4])} + data = {"payment_request": pr} + r = requests.post(url=url_for("api_transactions", _external=True), headers=header, data=json.dumps(data)) + r_json = r.json() + + if "ERROR" in r_json: + return jsonify({"status": "ERROR", "reason": r_json["ERROR"]}), 400 + + with open_ext_db("withdraw") as withdraw_ext_db: + user_fau = withdraw_ext_db.fetchall("SELECT * FROM withdraws WHERE withdrawals = ?", (k1,)) + + return jsonify({"status": "OK"}), 200 diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 30e19258a..eca91a643 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -3,14 +3,11 @@ import sqlite3 class MegaEncoder(json.JSONEncoder): - def default(self, o): - if type(o) == sqlite3.Row: - val = {} - for k in o.keys(): - val[k] = o[k] - return val - return o + def default(self, obj): + if isinstance(obj, sqlite3.Row): + return {k: obj[k] for k in obj.keys()} + return obj -def megajson(o): - return json.dumps(o, cls=MegaEncoder) +def megajson(obj): + return json.dumps(obj, cls=MegaEncoder) diff --git a/lnbits/settings.py b/lnbits/settings.py index 6fc2e7c25..88c17457f 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -9,7 +9,7 @@ WALLET = OpenNodeWallet(endpoint=os.getenv("OPENNODE_API_ENDPOINT"),admin_key=os LNBITS_PATH = os.path.dirname(os.path.realpath(__file__)) -DATABASE_PATH = os.getenv("DATABASE_PATH") or os.path.join(LNBITS_PATH, "data", "database.sqlite3") -DEFAULT_USER_WALLET_NAME = os.getenv("DEFAULT_USER_WALLET_NAME") or "Bitcoin LN Wallet" +DATABASE_PATH = os.getenv("DATABASE_PATH", os.path.join(LNBITS_PATH, "data", "database.sqlite3")) -FEE_RESERVE = float(os.getenv("FEE_RESERVE") or 0) +DEFAULT_USER_WALLET_NAME = os.getenv("DEFAULT_USER_WALLET_NAME", "Bitcoin LN Wallet") +FEE_RESERVE = float(os.getenv("FEE_RESERVE", 0)) diff --git a/lnbits/static/app.js b/lnbits/static/app.js index dd88f14ae..bc9cd92c7 100644 --- a/lnbits/static/app.js +++ b/lnbits/static/app.js @@ -48,7 +48,7 @@ function getAjax(url, thekey, success) { } xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey) xhr.setRequestHeader('Content-Type', 'application/json') - + xhr.send() return xhr } @@ -90,10 +90,10 @@ function sendfundspaste() { '
    Memo: ' + outmemo + '' + - "

    " + - ""+ "" - + window.top.location.href = "lnurlwallet?lightning=" + getQueryVariable("lightning"); } @@ -195,15 +195,15 @@ function sendfunds(invoice) { '

    Processing...

    <


    '; postAjax( - '/v1/channels/transactions', + '/api/v1/channels/transactions', JSON.stringify({payment_request: invoice}), wallet.adminkey, function(data) { thehash = JSON.parse(data).payment_hash - setInterval(function(){ - getAjax('/v1/payment/' + thehash, wallet.adminkey, function(datab) { + setInterval(function(){ + getAjax('/api/v1/payment/' + thehash, wallet.adminkey, function(datab) { console.log(JSON.parse(datab).PAID) if (JSON.parse(datab).PAID == 'TRUE') { window.location.href = 'wallet?wal=' + wallet.id + '&usr=' + user @@ -220,7 +220,7 @@ function scanQRsend() { "