Browse Source

refactor: unify responses in backend wallets

fee_issues
Eneko Illarramendi 5 years ago
parent
commit
47b93a97d6
  1. 3
      .env.example
  2. 136
      lnbits/__init__.py
  3. 4
      lnbits/settings.py
  4. 65
      lnbits/wallets.py
  5. 4
      lnbits/wallets/__init__.py
  6. 32
      lnbits/wallets/base.py
  7. 48
      lnbits/wallets/lnd.py
  8. 47
      lnbits/wallets/lntxbot.py

3
.env.example

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

136
lnbits/__init__.py

@ -37,12 +37,13 @@ def deletewallet():
with Database() as db: with Database() as db:
db.execute( db.execute(
""" """
UPDATE wallets AS w SET UPDATE wallets AS w
user = 'del:' || w.user, SET
adminkey = 'del:' || w.adminkey, user = 'del:' || w.user,
inkey = 'del:' || w.inkey adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
WHERE id = ? AND user = ? WHERE id = ? AND user = ?
""", """,
(wallet_id, user_id), (wallet_id, user_id),
) )
@ -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))
@ -136,8 +137,8 @@ def wallet():
db.execute( db.execute(
""" """
INSERT OR IGNORE INTO accounts (id) VALUES (?) INSERT OR IGNORE INTO accounts (id) VALUES (?)
""", """,
(usr,), (usr,),
) )
@ -155,9 +156,9 @@ def wallet():
wallet_id = uuid.uuid4().hex wallet_id = uuid.uuid4().hex
db.execute( db.execute(
""" """
INSERT INTO wallets (id, name, user, adminkey, inkey) INSERT INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
(wallet_id, wallet_name, usr, uuid.uuid4().hex, uuid.uuid4().hex), (wallet_id, wallet_name, usr, uuid.uuid4().hex, uuid.uuid4().hex),
) )
@ -167,13 +168,13 @@ def wallet():
# ------------------------------------------------------------ # ------------------------------------------------------------
db.execute( db.execute(
""" """
INSERT OR REPLACE INTO wallets (id, user, name, adminkey, inkey) INSERT OR REPLACE INTO wallets (id, user, name, adminkey, inkey)
VALUES (?, ?, VALUES (?, ?,
coalesce((SELECT name FROM wallets WHERE id = ?), ?), coalesce((SELECT name FROM wallets WHERE id = ?), ?),
coalesce((SELECT adminkey FROM wallets WHERE id = ?), ?), coalesce((SELECT adminkey FROM wallets WHERE id = ?), ?),
coalesce((SELECT inkey FROM wallets WHERE id = ?), ?) coalesce((SELECT inkey FROM wallets WHERE id = ?), ?)
) )
""", """,
( (
wallet_id, wallet_id,
usr, usr,
@ -191,24 +192,22 @@ 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 FROM wallets
) * ? AS balance, WHERE user = ? AND id = ?
* """,
FROM wallets
WHERE user = ? AND id = ?
""",
(1 - FEE_RESERVE, usr, wallet_id), (1 - FEE_RESERVE, usr, wallet_id),
) )
transactions = db.fetchall( transactions = db.fetchall(
""" """
SELECT * FROM apipayments SELECT *
WHERE wallet = ? AND pending = 0 FROM apipayments
ORDER BY time WHERE wallet = ? AND pending = 0
""", ORDER BY time
""",
(wallet_id,), (wallet_id,),
) )
@ -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"):
return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500
data = r.json() if not r.ok or "error" in r.json():
return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500
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,16 +327,17 @@ 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
INNER JOIN wallets AS w ON apipayments.wallet = w.id FROM apipayments
WHERE payhash = ? INNER JOIN wallets AS w ON apipayments.wallet = w.id
AND (w.adminkey = ? OR w.inkey = ?) WHERE payhash = ?
""", AND (w.adminkey = ? OR w.inkey = ?)
""",
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]), (payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
) )
@ -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
@ -368,30 +360,30 @@ def api_checkpending():
for pendingtx in db.fetchall( for pendingtx in db.fetchall(
""" """
SELECT SELECT
payhash, payhash,
CASE CASE
WHEN amount < 0 THEN 'send' WHEN amount < 0 THEN 'send'
ELSE 'recv' ELSE 'recv'
END AS kind END AS kind
FROM apipayments FROM apipayments
INNER JOIN wallets ON apipayments.wallet = wallets.id INNER JOIN wallets ON apipayments.wallet = wallets.id
WHERE time > strftime('%s', 'now') - 86400 WHERE time > strftime('%s', 'now') - 86400
AND pending = 1 AND pending = 1
AND (adminkey = ? OR inkey = ?) AND (adminkey = ? OR inkey = ?)
""", """,
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]), (request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
): ):
payhash = pendingtx["payhash"] payhash = pendingtx["payhash"]
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 ""

4
lnbits/settings.py

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

65
lnbits/wallets.py

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

4
lnbits/wallets/__init__.py

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

32
lnbits/wallets/base.py

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

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

@ -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")])
Loading…
Cancel
Save