refactor: unify responses in backend wallets

This commit is contained in:
Eneko Illarramendi 2020-01-10 21:26:42 +01:00
parent 676fa29852
commit 47b93a97d6
8 changed files with 200 additions and 139 deletions

View file

@ -1,6 +1,9 @@
FLASK_APP=lnbits FLASK_APP=lnbits
FLASK_ENV=development FLASK_ENV=development
LND_API_ENDPOINT=https://mylnd.io/rest/
LND_ADMIN_MACAROON=LND_ADMIN_MACAROON
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/ LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
LNTXBOT_ADMIN_KEY=LNTXBOT_ADMIN_KEY LNTXBOT_ADMIN_KEY=LNTXBOT_ADMIN_KEY
LNTXBOT_INVOICE_KEY=LNTXBOT_INVOICE_KEY LNTXBOT_INVOICE_KEY=LNTXBOT_INVOICE_KEY

View file

@ -37,7 +37,8 @@ def deletewallet():
with Database() as db: with Database() as db:
db.execute( db.execute(
""" """
UPDATE wallets AS w SET UPDATE wallets AS w
SET
user = 'del:' || w.user, user = 'del:' || w.user,
adminkey = 'del:' || w.adminkey, adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey inkey = 'del:' || w.inkey
@ -73,19 +74,19 @@ def lnurlwallet():
withdraw_res = LnurlWithdrawResponse(**data) withdraw_res = LnurlWithdrawResponse(**data)
invoice = WALLET.create_invoice(withdraw_res.max_sats, "lnbits lnurl funding").json() _, pay_hash, pay_req = WALLET.create_invoice(withdraw_res.max_sats, "LNbits lnurl funding")
payment_hash = invoice["payment_hash"]
r = requests.get( r = requests.get(
withdraw_res.callback.base, withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": invoice["pay_req"]}}, params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": pay_req}},
) )
if not r.ok: if not r.ok:
return redirect(url_for("home")) return redirect(url_for("home"))
data = json.loads(r.text) data = json.loads(r.text)
for i in range(10): for i in range(10):
r = WALLET.get_invoice_status(payment_hash) r = WALLET.get_invoice_status(pay_hash).raw_response
if not r.ok: if not r.ok:
continue continue
@ -106,7 +107,7 @@ def lnurlwallet():
) )
db.execute( db.execute(
"INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 0, ?)", "INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 0, ?)",
(payment_hash, withdraw_res.max_sats * 1000, wallet_id, "lnbits lnurl funding",), (pay_hash, withdraw_res.max_sats * 1000, wallet_id, "LNbits lnurl funding",),
) )
return redirect(url_for("wallet", usr=user_id, wal=wallet_id)) return redirect(url_for("wallet", usr=user_id, wal=wallet_id))
@ -192,10 +193,7 @@ def wallet():
wallet = db.fetchone( wallet = db.fetchone(
""" """
SELECT SELECT
coalesce( coalesce((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance,
(SELECT balance/1000 FROM balances WHERE wallet = wallets.id),
0
) * ? AS balance,
* *
FROM wallets FROM wallets
WHERE user = ? AND id = ? WHERE user = ? AND id = ?
@ -205,7 +203,8 @@ def wallet():
transactions = db.fetchall( transactions = db.fetchall(
""" """
SELECT * FROM apipayments SELECT *
FROM apipayments
WHERE wallet = ? AND pending = 0 WHERE wallet = ? AND pending = 0
ORDER BY time ORDER BY time
""", """,
@ -245,39 +244,36 @@ def api_invoices():
if not wallet: if not wallet:
return jsonify({"ERROR": "NO KEY"}), 200 return jsonify({"ERROR": "NO KEY"}), 200
r = WALLET.create_invoice(postedjson["value"], postedjson["memo"]) r, pay_hash, pay_req = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
if not r.ok or r.json().get("error"):
if not r.ok or "error" in r.json():
return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500 return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500
data = r.json()
pay_req = data["pay_req"]
payment_hash = data["payment_hash"]
amount_msat = int(postedjson["value"]) * 1000 amount_msat = int(postedjson["value"]) * 1000
db.execute( db.execute(
"INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 1, ?)", "INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 1, ?)",
(payment_hash, amount_msat, wallet["id"], postedjson["memo"],), (pay_hash, amount_msat, wallet["id"], postedjson["memo"],),
) )
return jsonify({"pay_req": pay_req, "payment_hash": payment_hash}), 200 return jsonify({"pay_req": pay_req, "payment_hash": pay_hash}), 200
@app.route("/v1/channels/transactions", methods=["GET", "POST"]) @app.route("/v1/channels/transactions", methods=["GET", "POST"])
def api_transactions(): def api_transactions():
if request.headers["Content-Type"] != "application/json": if request.headers["Content-Type"] != "application/json":
return jsonify({"ERROR": "MUST BE JSON"}), 200 return jsonify({"ERROR": "MUST BE JSON"}), 400
data = request.json data = request.json
if "payment_request" not in data: if "payment_request" not in data:
return jsonify({"ERROR": "NO PAY REQ"}), 200 return jsonify({"ERROR": "NO PAY REQ"}), 400
with Database() as db: with Database() as db:
wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],)) wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
if not wallet: if not wallet:
return jsonify({"ERROR": "BAD AUTH"}), 200 return jsonify({"ERROR": "BAD AUTH"}), 401
# decode the invoice # decode the invoice
invoice = bolt11.decode(data["payment_request"]) invoice = bolt11.decode(data["payment_request"])
@ -331,12 +327,13 @@ def api_transactions():
@app.route("/v1/invoice/<payhash>", methods=["GET"]) @app.route("/v1/invoice/<payhash>", methods=["GET"])
def api_checkinvoice(payhash): def api_checkinvoice(payhash):
if request.headers["Content-Type"] != "application/json": if request.headers["Content-Type"] != "application/json":
return jsonify({"ERROR": "MUST BE JSON"}), 200 return jsonify({"ERROR": "MUST BE JSON"}), 400
with Database() as db: with Database() as db:
payment = db.fetchone( payment = db.fetchone(
""" """
SELECT pending FROM apipayments SELECT pending
FROM apipayments
INNER JOIN wallets AS w ON apipayments.wallet = w.id INNER JOIN wallets AS w ON apipayments.wallet = w.id
WHERE payhash = ? WHERE payhash = ?
AND (w.adminkey = ? OR w.inkey = ?) AND (w.adminkey = ? OR w.inkey = ?)
@ -350,14 +347,9 @@ def api_checkinvoice(payhash):
if not payment["pending"]: # pending if not payment["pending"]: # pending
return jsonify({"PAID": "TRUE"}), 200 return jsonify({"PAID": "TRUE"}), 200
r = WALLET.get_invoice_status(payhash) if not WALLET.get_invoice_status(payhash).settled:
if not r.ok or r.json().get("error"):
return jsonify({"PAID": "FALSE"}), 200 return jsonify({"PAID": "FALSE"}), 200
data = r.json()
if "preimage" not in data:
return jsonify({"PAID": "FALSE"}), 400
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,)) db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
return jsonify({"PAID": "TRUE"}), 200 return jsonify({"PAID": "TRUE"}), 200
@ -385,13 +377,13 @@ def api_checkpending():
kind = pendingtx["kind"] kind = pendingtx["kind"]
if kind == "send": if kind == "send":
status = WALLET.get_final_payment_status(payhash) payment_complete = WALLET.get_payment_status(payhash).settled
if status == "complete": if payment_complete:
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,)) db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
elif status == "failed": elif payment_complete is False:
db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,)) db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,))
elif kind == "recv": elif kind == "recv" and WALLET.get_invoice_status(payhash).settled:
if WALLET.is_invoice_paid(payhash):
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,)) db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
return "" return ""

View file

@ -1,6 +1,6 @@
import os import os
from .wallets import LntxbotWallet # OR LndHubWallet from .wallets import LntxbotWallet # OR LndWallet
WALLET = LntxbotWallet( WALLET = LntxbotWallet(
@ -10,7 +10,7 @@ WALLET = LntxbotWallet(
) )
# OR # OR
# WALLET = LndHubWallet(uri=os.getenv("LNDHUB_URI")) # WALLET = LndWallet(endpoint=os.getenv("LND_API_ENDPOINT"), admin_macaroon=os.getenv("LND_ADMIN_MACAROON"))
LNBITS_PATH = os.path.dirname(os.path.realpath(__file__)) LNBITS_PATH = os.path.dirname(os.path.realpath(__file__))
DATABASE_PATH = os.getenv("DATABASE_PATH") or os.path.join(LNBITS_PATH, "data", "database.sqlite3") DATABASE_PATH = os.getenv("DATABASE_PATH") or os.path.join(LNBITS_PATH, "data", "database.sqlite3")

View file

@ -1,65 +0,0 @@
import requests
from abc import ABC, abstractmethod
from requests import Response
class WalletResponse(Response):
"""TODO: normalize different wallet responses
"""
class Wallet(ABC):
@abstractmethod
def create_invoice(self, amount: int, memo: str = "") -> WalletResponse:
pass
@abstractmethod
def pay_invoice(self, bolt11: str) -> WalletResponse:
pass
@abstractmethod
def get_invoice_status(self, payment_hash: str, wait: bool = True) -> WalletResponse:
pass
class LndHubWallet(Wallet):
def __init__(self, *, uri: str):
raise NotImplementedError
class LntxbotWallet(Wallet):
def __init__(self, *, endpoint: str, admin_key: str, invoice_key: str) -> WalletResponse:
self.endpoint = endpoint
self.auth_admin = {"Authorization": f"Basic {admin_key}"}
self.auth_invoice = {"Authorization": f"Basic {invoice_key}"}
def create_invoice(self, amount: int, memo: str = "") -> WalletResponse:
return requests.post(
url=f"{self.endpoint}/addinvoice", headers=self.auth_invoice, json={"amt": str(amount), "memo": memo}
)
def pay_invoice(self, bolt11: str) -> WalletResponse:
return requests.post(url=f"{self.endpoint}/payinvoice", headers=self.auth_admin, json={"invoice": bolt11})
def get_invoice_status(self, payment_hash: str, wait: bool = True) -> Response:
wait = 'true' if wait else 'false'
return requests.post(url=f"{self.endpoint}/invoicestatus/{payment_hash}?wait={wait}", headers=self.auth_invoice)
def is_invoice_paid(self, payment_hash: str) -> False:
r = self.get_invoice_status(payment_hash)
if not r.ok or r.json().get('error'):
return False
data = r.json()
if "preimage" not in data or not data["preimage"]:
return False
return True
def get_final_payment_status(self, payment_hash: str) -> str:
r = requests.post(url=f"{self.endpoint}/paymentstatus/{payment_hash}", headers=self.auth_invoice)
if not r.ok:
return "unknown"
return r.json().get('status', 'unknown')

View file

@ -0,0 +1,4 @@
# flake8: noqa
from .lnd import LndWallet
from .lntxbot import LntxbotWallet

32
lnbits/wallets/base.py Normal file
View file

@ -0,0 +1,32 @@
from abc import ABC, abstractmethod
from requests import Response
from typing import NamedTuple, Optional
class InvoiceResponse(NamedTuple):
raw_response: Response
payment_hash: Optional[str] = None
payment_request: Optional[str] = None
class TxStatus(NamedTuple):
raw_response: Response
settled: Optional[bool] = None
class Wallet(ABC):
@abstractmethod
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
pass
@abstractmethod
def pay_invoice(self, bolt11: str) -> Response:
pass
@abstractmethod
def get_invoice_status(self, payment_hash: str, wait: bool = True) -> TxStatus:
pass
@abstractmethod
def get_payment_status(self, payment_hash: str) -> TxStatus:
pass

48
lnbits/wallets/lnd.py Normal file
View file

@ -0,0 +1,48 @@
from requests import Response, get, post
from .base import InvoiceResponse, TxStatus, Wallet
class LndWallet(Wallet):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
def __init__(self, *, endpoint: str, admin_macaroon: str):
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = {"Grpc-Metadata-macaroon": admin_macaroon}
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
payment_hash, payment_request = None, None
r = post(
url=f"{self.endpoint}/v1/invoices",
headers=self.auth_admin,
json={"value": f"{amount}", "description_hash": memo}, # , "private": True},
)
if r.ok:
data = r.json()
payment_hash, payment_request = data["r_hash"], data["payment_request"]
return InvoiceResponse(r, payment_hash, payment_request)
def pay_invoice(self, bolt11: str) -> Response:
raise NotImplementedError
def get_invoice_status(self, payment_hash: str, wait: bool = True) -> TxStatus:
r = get(url=f"{self.endpoint}/v1/invoice", headers=self.auth_admin, params={"r_hash": payment_hash})
if not r.ok:
return TxStatus(r, None)
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, params={"include_incomplete": True})
if not r.ok:
return TxStatus(r, None)
payments = [p for p in r.json()["payments"] if p["payment_hash"] == payment_hash]
payment = payments[0] if payments else None
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
return TxStatus(r, {0: None, 1: None, 2: True, 3: False}[payment["status"]] if payment else None)

47
lnbits/wallets/lntxbot.py Normal file
View file

@ -0,0 +1,47 @@
from requests import Response, post
from .base import InvoiceResponse, TxStatus, Wallet
class LntxbotWallet(Wallet):
"""https://github.com/fiatjaf/lntxbot/blob/master/api.go"""
def __init__(self, *, endpoint: str, admin_key: str, invoice_key: str):
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = {"Authorization": f"Basic {admin_key}"}
self.auth_invoice = {"Authorization": f"Basic {invoice_key}"}
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
payment_hash, payment_request = None, None
r = post(url=f"{self.endpoint}/addinvoice", headers=self.auth_invoice, json={"amt": str(amount), "memo": memo})
if r.ok:
data = r.json()
payment_hash, payment_request = data["payment_hash"], data["pay_req"]
return InvoiceResponse(r, payment_hash, payment_request)
def pay_invoice(self, bolt11: str) -> Response:
return post(url=f"{self.endpoint}/payinvoice", headers=self.auth_admin, json={"invoice": bolt11})
def get_invoice_status(self, payment_hash: str, wait: bool = True) -> TxStatus:
wait = "true" if wait else "false"
r = post(url=f"{self.endpoint}/invoicestatus/{payment_hash}?wait={wait}", headers=self.auth_invoice)
data = r.json()
if not r.ok or "error" in data:
return TxStatus(r, None)
if "preimage" not in data or not data["preimage"]:
return TxStatus(r, False)
return TxStatus(r, True)
def get_payment_status(self, payment_hash: str) -> TxStatus:
r = post(url=f"{self.endpoint}/paymentstatus/{payment_hash}", headers=self.auth_invoice)
data = r.json()
if not r.ok or "error" in data:
return TxStatus(r, None)
return TxStatus(r, {"complete": True, "failed": False, "unknown": None}[data.get("status", "unknown")])