lndhub extension backend.
This commit is contained in:
parent
5ac91ee2cf
commit
ce038193cf
9 changed files with 293 additions and 0 deletions
6
lnbits/extensions/lndhub/README.md
Normal file
6
lnbits/extensions/lndhub/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<h1>lndhub Extension</h1>
|
||||
<h2>*connect to your lnbits wallet from BlueWallet or Zeus*</h2>
|
||||
|
||||
Lndhub has nothing to do with lnd, it is just the name of the HTTP/JSON protocol https://bluewallet.io/ uses to talk to their Lightning custodian server at https://lndhub.io/.
|
||||
|
||||
Despite not having been planned to this, Lndhub because somewhat a standard for custodian wallet communication when https://t.me/lntxbot and https://zeusln.app/ implemented the same interface. And with this extension LNBits joins the same club.
|
||||
8
lnbits/extensions/lndhub/__init__.py
Normal file
8
lnbits/extensions/lndhub/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from flask import Blueprint
|
||||
|
||||
|
||||
lndhub_ext: Blueprint = Blueprint("lndhub", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
6
lnbits/extensions/lndhub/config.json
Normal file
6
lnbits/extensions/lndhub/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "lndhub",
|
||||
"short_description": "Access lnbits from BlueWallet or Zeus.",
|
||||
"icon": "navigation",
|
||||
"contributors": ["fiatjaf"]
|
||||
}
|
||||
25
lnbits/extensions/lndhub/decorators.py
Normal file
25
lnbits/extensions/lndhub/decorators.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from base64 import b64decode
|
||||
from flask import jsonify, g, request
|
||||
from functools import wraps
|
||||
|
||||
from lnbits.core.crud import get_wallet_for_key
|
||||
|
||||
|
||||
def check_wallet(requires_admin=False):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
token = request.headers["Authorization"].split("Bearer ")[1]
|
||||
key_type, key = b64decode(token).decode("utf-8").split(":")
|
||||
|
||||
if requires_admin and key_type != "admin":
|
||||
return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
|
||||
|
||||
g.wallet = get_wallet_for_key(key, key_type)
|
||||
if not g.wallet:
|
||||
return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
2
lnbits/extensions/lndhub/migrations.py
Normal file
2
lnbits/extensions/lndhub/migrations.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
def migrate():
|
||||
pass
|
||||
28
lnbits/extensions/lndhub/templates/lndhub/index.html
Normal file
28
lnbits/extensions/lndhub/templates/lndhub/index.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-md"></h5>
|
||||
{% for wallet in g.user.wallets %}
|
||||
<q-card>
|
||||
<code class="text-caption">
|
||||
lndhub://admin:{{wallet.adminkey}}@{% raw %}{{baseURL}}{% endraw %}/lndhub/ext/
|
||||
</code>
|
||||
</q-card>
|
||||
{% endfor %}
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
baseURL: location.protocol + '//' + location.hostname
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
21
lnbits/extensions/lndhub/utils.py
Normal file
21
lnbits/extensions/lndhub/utils.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from binascii import unhexlify
|
||||
|
||||
from lnbits.bolt11 import Invoice
|
||||
|
||||
|
||||
def to_buffer(payment_hash: str):
|
||||
return {"type": "Buffer", "data": [b for b in unhexlify(payment_hash)]}
|
||||
|
||||
|
||||
def decoded_as_lndhub(invoice: Invoice):
|
||||
return {
|
||||
"destination": invoice.payee,
|
||||
"payment_hash": invoice.payment_hash,
|
||||
"num_satoshis": invoice.amount_msat / 1000,
|
||||
"timestamp": str(invoice.date),
|
||||
"expiry": str(invoice.expiry),
|
||||
"description": invoice.description,
|
||||
"fallback_addr": "",
|
||||
"cltv_expiry": invoice.min_final_cltv_expiry,
|
||||
"route_hints": "",
|
||||
}
|
||||
11
lnbits/extensions/lndhub/views.py
Normal file
11
lnbits/extensions/lndhub/views.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from flask import render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.lndhub import lndhub_ext
|
||||
|
||||
|
||||
@lndhub_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def lndhub_index():
|
||||
return render_template("lndhub/index.html")
|
||||
186
lnbits/extensions/lndhub/views_api.py
Normal file
186
lnbits/extensions/lndhub/views_api.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import time
|
||||
from base64 import urlsafe_b64encode
|
||||
from flask import jsonify, g, request
|
||||
|
||||
from lnbits.core.services import pay_invoice, create_invoice
|
||||
from lnbits.decorators import api_validate_post_request
|
||||
from lnbits import bolt11
|
||||
|
||||
from lnbits.extensions.lndhub import lndhub_ext
|
||||
from .decorators import check_wallet
|
||||
from .utils import to_buffer, decoded_as_lndhub
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/getinfo", methods=["GET"])
|
||||
def lndhub_getinfo():
|
||||
return jsonify({"error": True, "code": 1, "message": "bad auth"})
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/auth", methods=["POST"])
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"login": {"type": "string", "required": True, "excludes": "refresh_token"},
|
||||
"password": {"type": "string", "required": True, "excludes": "refresh_token"},
|
||||
"refresh_token": {"type": "string", "required": True, "excludes": ["login", "password"]},
|
||||
}
|
||||
)
|
||||
def lndhub_auth():
|
||||
token = (
|
||||
g.data["token"]
|
||||
if "token" in g.data and g.data["token"]
|
||||
else urlsafe_b64encode((g.data["login"] + ":" + g.data["password"]).encode("utf-8")).decode("ascii")
|
||||
)
|
||||
return jsonify({"refresh_token": token, "access_token": token})
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/addinvoice", methods=["POST"])
|
||||
@check_wallet()
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"amt": {"type": "string", "required": True},
|
||||
"memo": {"type": "string", "required": True},
|
||||
"preimage": {"type": "string", "required": False},
|
||||
}
|
||||
)
|
||||
def lndhub_addinvoice():
|
||||
try:
|
||||
_, pr = create_invoice(
|
||||
wallet_id=g.wallet.id,
|
||||
amount=int(g.data["amt"]),
|
||||
memo=g.data["memo"],
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify(
|
||||
{
|
||||
"error": True,
|
||||
"code": 7,
|
||||
"message": "Failed to create invoice: " + str(e),
|
||||
}
|
||||
)
|
||||
|
||||
invoice = bolt11.decode(pr)
|
||||
return jsonify(
|
||||
{
|
||||
"pay_req": pr,
|
||||
"payment_request": pr,
|
||||
"add_index": "500",
|
||||
"r_hash": to_buffer(invoice.payment_hash),
|
||||
"hash": invoice.payment_hash,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/payinvoice", methods=["POST"])
|
||||
@check_wallet(requires_admin=True)
|
||||
@api_validate_post_request(schema={"invoice": {"type": "string", "required": True}})
|
||||
def lndhub_payinvoice():
|
||||
try:
|
||||
pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["invoice"])
|
||||
except Exception as e:
|
||||
return jsonify(
|
||||
{
|
||||
"error": True,
|
||||
"code": 10,
|
||||
"message": "Payment failed: " + str(e),
|
||||
}
|
||||
)
|
||||
|
||||
invoice: bolt11.Invoice = bolt11.decode(g.data["invoice"])
|
||||
return jsonify(
|
||||
{
|
||||
"payment_error": "",
|
||||
"payment_preimage": "0" * 64,
|
||||
"route": {},
|
||||
"payment_hash": invoice.payment_hash,
|
||||
"decoded": decoded_as_lndhub(invoice),
|
||||
"fee_msat": 0,
|
||||
"type": "paid_invoice",
|
||||
"fee": 0,
|
||||
"value": invoice.amount_msat / 1000,
|
||||
"timestamp": int(time.time()),
|
||||
"memo": invoice.description,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/balance", methods=["GET"])
|
||||
@check_wallet()
|
||||
def lndhub_balance():
|
||||
return jsonify({"BTC": {"AvailableBalance": g.wallet.balance}})
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/gettxs", methods=["GET"])
|
||||
@check_wallet()
|
||||
def lndhub_gettxs():
|
||||
limit = int(request.args.get("limit", 200))
|
||||
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
"payment_preimage": payment.preimage,
|
||||
"payment_hash": payment.payment_hash,
|
||||
"fee_msat": payment.fee * 1000,
|
||||
"type": "paid_invoice",
|
||||
"fee": payment.fee,
|
||||
"value": int(payment.amount / 1000),
|
||||
"timestamp": payment.time,
|
||||
"memo": payment.memo if not payment.pending else "Payment in transition",
|
||||
}
|
||||
for payment in g.wallet.get_payments(
|
||||
pending=True, complete=True, outgoing=True, incoming=False, order="ASC"
|
||||
)[0:limit]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/getuserinvoices", methods=["GET"])
|
||||
@check_wallet()
|
||||
def lndhub_getuserinvoices():
|
||||
limit = int(request.args.get("limit", 200))
|
||||
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
"r_hash": to_buffer(invoice.payment_hash),
|
||||
"payment_request": invoice.bolt11,
|
||||
"add_index": "500",
|
||||
"description": invoice.memo,
|
||||
"payment_hash": invoice.payment_hash,
|
||||
"ispaid": not invoice.pending,
|
||||
"amt": int(invoice.amount / 1000),
|
||||
"expire_time": int(time.time() + 1800),
|
||||
"timestamp": invoice.time,
|
||||
"type": "user_invoice",
|
||||
}
|
||||
for invoice in g.wallet.get_payments(
|
||||
pending=True, complete=True, incoming=True, outgoing=False, order="ASC"
|
||||
)[:limit]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/getbtc", methods=["GET"])
|
||||
@check_wallet()
|
||||
def lndhub_getbtc():
|
||||
"load an address for incoming onchain btc"
|
||||
return jsonify([])
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/getpending", methods=["GET"])
|
||||
@check_wallet()
|
||||
def lndhub_getpending():
|
||||
"pending onchain transactions"
|
||||
return jsonify([])
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/decodeinvoice", methods=["GET"])
|
||||
def lndhub_decodeinvoice():
|
||||
invoice = request.args.get("invoice")
|
||||
inv = bolt11.decode(invoice)
|
||||
return jsonify(decoded_as_lndhub(inv))
|
||||
|
||||
|
||||
@lndhub_ext.route("/ext/checkrouteinvoice", methods=["GET"])
|
||||
def lndhub_checkrouteinvoice():
|
||||
"not implemented on canonical lndhub"
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue