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_ENV=development
LND_API_ENDPOINT=https://mylnd.io/rest/
LND_ADMIN_MACAROON=LND_ADMIN_MACAROON
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
LNTXBOT_ADMIN_KEY=LNTXBOT_ADMIN_KEY
LNTXBOT_INVOICE_KEY=LNTXBOT_INVOICE_KEY

136
lnbits/__init__.py

@ -37,12 +37,13 @@ def deletewallet():
with Database() as db:
db.execute(
"""
UPDATE wallets AS w SET
user = 'del:' || w.user,
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
UPDATE wallets AS w
SET
user = 'del:' || w.user,
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
WHERE id = ? AND user = ?
""",
""",
(wallet_id, user_id),
)
@ -73,19 +74,19 @@ def lnurlwallet():
withdraw_res = LnurlWithdrawResponse(**data)
invoice = WALLET.create_invoice(withdraw_res.max_sats, "lnbits lnurl funding").json()
payment_hash = invoice["payment_hash"]
_, pay_hash, pay_req = WALLET.create_invoice(withdraw_res.max_sats, "LNbits lnurl funding")
r = requests.get(
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:
return redirect(url_for("home"))
data = json.loads(r.text)
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:
continue
@ -106,7 +107,7 @@ def lnurlwallet():
)
db.execute(
"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))
@ -136,8 +137,8 @@ def wallet():
db.execute(
"""
INSERT OR IGNORE INTO accounts (id) VALUES (?)
""",
INSERT OR IGNORE INTO accounts (id) VALUES (?)
""",
(usr,),
)
@ -155,9 +156,9 @@ def wallet():
wallet_id = uuid.uuid4().hex
db.execute(
"""
INSERT INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
""",
INSERT INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
""",
(wallet_id, wallet_name, usr, uuid.uuid4().hex, uuid.uuid4().hex),
)
@ -167,13 +168,13 @@ def wallet():
# ------------------------------------------------------------
db.execute(
"""
INSERT OR REPLACE INTO wallets (id, user, name, adminkey, inkey)
VALUES (?, ?,
coalesce((SELECT name FROM wallets WHERE id = ?), ?),
coalesce((SELECT adminkey FROM wallets WHERE id = ?), ?),
coalesce((SELECT inkey FROM wallets WHERE id = ?), ?)
)
""",
INSERT OR REPLACE INTO wallets (id, user, name, adminkey, inkey)
VALUES (?, ?,
coalesce((SELECT name FROM wallets WHERE id = ?), ?),
coalesce((SELECT adminkey FROM wallets WHERE id = ?), ?),
coalesce((SELECT inkey FROM wallets WHERE id = ?), ?)
)
""",
(
wallet_id,
usr,
@ -191,24 +192,22 @@ def wallet():
wallet = db.fetchone(
"""
SELECT
coalesce(
(SELECT balance/1000 FROM balances WHERE wallet = wallets.id),
0
) * ? AS balance,
*
FROM wallets
WHERE user = ? AND id = ?
""",
SELECT
coalesce((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance,
*
FROM wallets
WHERE user = ? AND id = ?
""",
(1 - FEE_RESERVE, usr, wallet_id),
)
transactions = db.fetchall(
"""
SELECT * FROM apipayments
WHERE wallet = ? AND pending = 0
ORDER BY time
""",
SELECT *
FROM apipayments
WHERE wallet = ? AND pending = 0
ORDER BY time
""",
(wallet_id,),
)
@ -245,39 +244,36 @@ def api_invoices():
if not wallet:
return jsonify({"ERROR": "NO KEY"}), 200
r = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
if not r.ok or r.json().get("error"):
return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500
r, pay_hash, pay_req = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
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
db.execute(
"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"])
def api_transactions():
if request.headers["Content-Type"] != "application/json":
return jsonify({"ERROR": "MUST BE JSON"}), 200
return jsonify({"ERROR": "MUST BE JSON"}), 400
data = request.json
if "payment_request" not in data:
return jsonify({"ERROR": "NO PAY REQ"}), 200
return jsonify({"ERROR": "NO PAY REQ"}), 400
with Database() as db:
wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
if not wallet:
return jsonify({"ERROR": "BAD AUTH"}), 200
return jsonify({"ERROR": "BAD AUTH"}), 401
# decode the invoice
invoice = bolt11.decode(data["payment_request"])
@ -331,16 +327,17 @@ def api_transactions():
@app.route("/v1/invoice/<payhash>", methods=["GET"])
def api_checkinvoice(payhash):
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:
payment = db.fetchone(
"""
SELECT pending FROM apipayments
INNER JOIN wallets AS w ON apipayments.wallet = w.id
WHERE payhash = ?
AND (w.adminkey = ? OR w.inkey = ?)
""",
SELECT pending
FROM apipayments
INNER JOIN wallets AS w ON apipayments.wallet = w.id
WHERE payhash = ?
AND (w.adminkey = ? OR w.inkey = ?)
""",
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
)
@ -350,14 +347,9 @@ def api_checkinvoice(payhash):
if not payment["pending"]: # pending
return jsonify({"PAID": "TRUE"}), 200
r = WALLET.get_invoice_status(payhash)
if not r.ok or r.json().get("error"):
if not WALLET.get_invoice_status(payhash).settled:
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,))
return jsonify({"PAID": "TRUE"}), 200
@ -368,30 +360,30 @@ def api_checkpending():
for pendingtx in db.fetchall(
"""
SELECT
payhash,
CASE
WHEN amount < 0 THEN 'send'
ELSE 'recv'
END AS kind
payhash,
CASE
WHEN amount < 0 THEN 'send'
ELSE 'recv'
END AS kind
FROM apipayments
INNER JOIN wallets ON apipayments.wallet = wallets.id
WHERE time > strftime('%s', 'now') - 86400
AND pending = 1
AND (adminkey = ? OR inkey = ?)
""",
AND pending = 1
AND (adminkey = ? OR inkey = ?)
""",
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
):
payhash = pendingtx["payhash"]
kind = pendingtx["kind"]
if kind == "send":
status = WALLET.get_final_payment_status(payhash)
if status == "complete":
payment_complete = WALLET.get_payment_status(payhash).settled
if payment_complete:
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,))
elif kind == "recv":
if WALLET.is_invoice_paid(payhash):
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
elif kind == "recv" and WALLET.get_invoice_status(payhash).settled:
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
return ""

4
lnbits/settings.py

@ -1,6 +1,6 @@
import os
from .wallets import LntxbotWallet # OR LndHubWallet
from .wallets import LntxbotWallet # OR LndWallet
WALLET = LntxbotWallet(
@ -10,7 +10,7 @@ WALLET = LntxbotWallet(
)
# 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__))
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