mirror of https://github.com/lukechilds/lnbits.git
fiatjaf
4 years ago
committed by
fiatjaf
9 changed files with 293 additions and 0 deletions
@ -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. |
@ -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 |
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"name": "lndhub", |
||||
|
"short_description": "Access lnbits from BlueWallet or Zeus.", |
||||
|
"icon": "navigation", |
||||
|
"contributors": ["fiatjaf"] |
||||
|
} |
@ -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 |
@ -0,0 +1,2 @@ |
|||||
|
def migrate(): |
||||
|
pass |
@ -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 %} |
@ -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": "", |
||||
|
} |
@ -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") |
@ -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…
Reference in new issue