Browse Source

refactor: a wallet is a wallet is a wallet

fee_issues
Eneko Illarramendi 5 years ago
parent
commit
d03785558b
  1. 2
      .env.example
  2. 3
      lnbits/__init__.py
  3. 24
      lnbits/core/crud.py
  4. 10
      lnbits/core/models.py
  5. 20
      lnbits/core/static/js/wallet.js
  6. 45
      lnbits/core/views/api.py
  7. 22
      lnbits/core/views/lnurl.py
  8. 10
      lnbits/settings.py
  9. 6
      lnbits/static/js/base.js
  10. 15
      lnbits/wallets/base.py
  11. 46
      lnbits/wallets/lnd.py
  12. 39
      lnbits/wallets/lnpay.py
  13. 55
      lnbits/wallets/lntxbot.py
  14. 47
      lnbits/wallets/opennode.py
  15. 4
      requirements.txt

2
.env.example

@ -5,6 +5,8 @@ LNBITS_WITH_ONION=0
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_FEE_RESERVE=0 LNBITS_FEE_RESERVE=0
LNBITS_BACKEND_WALLET_CLASS="LntxbotWallet"
LND_API_ENDPOINT=https://mylnd.io/rest/ LND_API_ENDPOINT=https://mylnd.io/rest/
LND_ADMIN_MACAROON=LND_ADMIN_MACAROON LND_ADMIN_MACAROON=LND_ADMIN_MACAROON
LND_INVOICE_MACAROON=LND_INVOICE_MACAROON LND_INVOICE_MACAROON=LND_INVOICE_MACAROON

3
lnbits/__init__.py

@ -3,8 +3,6 @@ import importlib
from flask import Flask from flask import Flask
from flask_assets import Environment, Bundle from flask_assets import Environment, Bundle
from flask_compress import Compress from flask_compress import Compress
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_talisman import Talisman from flask_talisman import Talisman
from os import getenv from os import getenv
@ -21,7 +19,6 @@ valid_extensions = [ext for ext in ExtensionManager().extensions if ext.is_valid
# ----------------------- # -----------------------
Compress(app) Compress(app)
Limiter(app, key_func=get_remote_address, default_limits=["1 per second"])
Talisman( Talisman(
app, app,
force_https=getenv("LNBITS_WITH_ONION", 0) == 0, force_https=getenv("LNBITS_WITH_ONION", 0) == 0,

24
lnbits/core/crud.py

@ -125,15 +125,15 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
# --------------- # ---------------
def get_wallet_payment(wallet_id: str, payhash: str) -> Optional[Payment]: def get_wallet_payment(wallet_id: str, checking_id: str) -> Optional[Payment]:
with open_db() as db: with open_db() as db:
row = db.fetchone( row = db.fetchone(
""" """
SELECT payhash, amount, fee, pending, memo, time SELECT payhash as checking_id, amount, fee, pending, memo, time
FROM apipayments FROM apipayments
WHERE wallet = ? AND payhash = ? WHERE wallet = ? AND payhash = ?
""", """,
(wallet_id, payhash), (wallet_id, checking_id),
) )
return Payment(**row) if row else None return Payment(**row) if row else None
@ -148,7 +148,7 @@ def get_wallet_payments(wallet_id: str, *, include_all_pending: bool = False) ->
rows = db.fetchall( rows = db.fetchall(
f""" f"""
SELECT payhash, amount, fee, pending, memo, time SELECT payhash as checking_id, amount, fee, pending, memo, time
FROM apipayments FROM apipayments
WHERE wallet = ? AND {clause} WHERE wallet = ? AND {clause}
ORDER BY time DESC ORDER BY time DESC
@ -163,24 +163,26 @@ def get_wallet_payments(wallet_id: str, *, include_all_pending: bool = False) ->
# -------- # --------
def create_payment(*, wallet_id: str, payhash: str, amount: str, memo: str, fee: int = 0) -> Payment: def create_payment(
*, wallet_id: str, checking_id: str, amount: str, memo: str, fee: int = 0, pending: bool = True
) -> Payment:
with open_db() as db: with open_db() as db:
db.execute( db.execute(
""" """
INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee) INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
(wallet_id, payhash, amount, 1, memo, fee), (wallet_id, checking_id, amount, int(pending), memo, fee),
) )
return get_wallet_payment(wallet_id, payhash) return get_wallet_payment(wallet_id, checking_id)
def update_payment_status(payhash: str, pending: bool) -> None: def update_payment_status(checking_id: str, pending: bool) -> None:
with open_db() as db: with open_db() as db:
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), payhash,)) db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), checking_id,))
def delete_payment(payhash: str) -> None: def delete_payment(checking_id: str) -> None:
with open_db() as db: with open_db() as db:
db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,)) db.execute("DELETE FROM apipayments WHERE payhash = ?", (checking_id,))

10
lnbits/core/models.py

@ -29,10 +29,10 @@ class Wallet(NamedTuple):
def balance(self) -> int: def balance(self) -> int:
return int(self.balance / 1000) return int(self.balance / 1000)
def get_payment(self, payhash: str) -> "Payment": def get_payment(self, checking_id: str) -> "Payment":
from .crud import get_wallet_payment from .crud import get_wallet_payment
return get_wallet_payment(self.id, payhash) return get_wallet_payment(self.id, checking_id)
def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]: def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]:
from .crud import get_wallet_payments from .crud import get_wallet_payments
@ -41,7 +41,7 @@ class Wallet(NamedTuple):
class Payment(NamedTuple): class Payment(NamedTuple):
payhash: str checking_id: str
pending: bool pending: bool
amount: int amount: int
fee: int fee: int
@ -67,9 +67,9 @@ class Payment(NamedTuple):
def set_pending(self, pending: bool) -> None: def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status from .crud import update_payment_status
update_payment_status(self.payhash, pending) update_payment_status(self.checking_id, pending)
def delete(self) -> None: def delete(self) -> None:
from .crud import delete_payment from .crud import delete_payment
delete_payment(self.payhash) delete_payment(self.checking_id)

20
lnbits/core/static/js/wallet.js

@ -160,6 +160,7 @@ new Vue({
: false; : false;
}, },
paymentsFiltered: function () { paymentsFiltered: function () {
return this.payments;
return this.payments.filter(function (obj) { return this.payments.filter(function (obj) {
return obj.isPaid; return obj.isPaid;
}); });
@ -222,7 +223,7 @@ new Vue({
self.receive.paymentReq = response.data.payment_request; self.receive.paymentReq = response.data.payment_request;
self.receive.paymentChecker = setInterval(function () { self.receive.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.w.wallet, response.data.payment_hash).then(function (response) { LNbits.api.getPayment(self.w.wallet, response.data.checking_id).then(function (response) {
if (response.data.paid) { if (response.data.paid) {
self.fetchPayments(); self.fetchPayments();
self.receive.show = false; self.receive.show = false;
@ -284,13 +285,10 @@ new Vue({
icon: null icon: null
}); });
LNbits.api.payInvoice(this.w.wallet, this.send.data.bolt11).catch(function (error) { LNbits.api.payInvoice(this.w.wallet, this.send.data.bolt11).then(function (response) {
LNbits.utils.notifyApiError(error);
});
self.send.paymentChecker = setInterval(function () { self.send.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.w.wallet, self.send.invoice.hash).then(function (response) { LNbits.api.getPayment(self.w.wallet, response.data.checking_id).then(function (res) {
if (response.data.paid) { if (res.data.paid) {
self.send.show = false; self.send.show = false;
clearInterval(self.send.paymentChecker); clearInterval(self.send.paymentChecker);
dismissPaymentMsg(); dismissPaymentMsg();
@ -298,6 +296,10 @@ new Vue({
} }
}); });
}, 2000); }, 2000);
}).catch(function (error) {
dismissPaymentMsg();
LNbits.utils.notifyApiError(error);
});
}, },
deleteWallet: function (walletId, user) { deleteWallet: function (walletId, user) {
LNbits.href.deleteWallet(walletId, user); LNbits.href.deleteWallet(walletId, user);
@ -327,8 +329,6 @@ new Vue({
}, },
created: function () { created: function () {
this.fetchPayments(); this.fetchPayments();
setTimeout(function () { setTimeout(this.checkPendingPayments(), 1200);
this.checkPendingPayments();
}, 1100);
} }
}); });

45
lnbits/core/views/api.py

@ -15,9 +15,9 @@ def api_payments():
if "check_pending" in request.args: if "check_pending" in request.args:
for payment in g.wallet.get_payments(include_all_pending=True): for payment in g.wallet.get_payments(include_all_pending=True):
if payment.is_out: if payment.is_out:
payment.set_pending(WALLET.get_payment_status(payment.payhash).pending) payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending)
elif payment.is_in: elif payment.is_in:
payment.set_pending(WALLET.get_invoice_status(payment.payhash).pending) payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
return jsonify(g.wallet.get_payments()), Status.OK return jsonify(g.wallet.get_payments()), Status.OK
@ -32,18 +32,17 @@ def api_payments_create_invoice():
return jsonify({"message": "`memo` needs to be a valid string."}), Status.BAD_REQUEST return jsonify({"message": "`memo` needs to be a valid string."}), Status.BAD_REQUEST
try: try:
r, payhash, payment_request = WALLET.create_invoice(g.data["amount"], g.data["memo"]) ok, checking_id, payment_request, error_message = WALLET.create_invoice(g.data["amount"], g.data["memo"])
server_error = not r.ok or "message" in r.json() except Exception as e:
except Exception: ok, error_message = False, str(e)
server_error = True
if server_error: if not ok:
return jsonify({"message": "Unexpected backend error. Try again later."}), Status.INTERNAL_SERVER_ERROR return jsonify({"message": error_message or "Unexpected backend error."}), Status.INTERNAL_SERVER_ERROR
amount_msat = g.data["amount"] * 1000 amount_msat = g.data["amount"] * 1000
create_payment(wallet_id=g.wallet.id, payhash=payhash, amount=amount_msat, memo=g.data["memo"]) create_payment(wallet_id=g.wallet.id, checking_id=checking_id, amount=amount_msat, memo=g.data["memo"])
return jsonify({"payment_request": payment_request, "payment_hash": payhash}), Status.CREATED return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
@api_check_wallet_macaroon(key_type="invoice") @api_check_wallet_macaroon(key_type="invoice")
@ -61,24 +60,24 @@ def api_payments_pay_invoice():
if invoice.amount_msat > g.wallet.balance_msat: if invoice.amount_msat > g.wallet.balance_msat:
return jsonify({"message": "Insufficient balance."}), Status.FORBIDDEN return jsonify({"message": "Insufficient balance."}), Status.FORBIDDEN
ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(g.data["bolt11"])
if ok:
create_payment( create_payment(
wallet_id=g.wallet.id, wallet_id=g.wallet.id,
payhash=invoice.payment_hash, checking_id=checking_id,
amount=-invoice.amount_msat, amount=-invoice.amount_msat,
memo=invoice.description, memo=invoice.description,
fee=-invoice.amount_msat * FEE_RESERVE, fee=-invoice.amount_msat * FEE_RESERVE,
) )
r, server_error, fee_msat, error_message = WALLET.pay_invoice(g.data["bolt11"])
except Exception as e: except Exception as e:
server_error = True ok, error_message = False, str(e)
error_message = str(e)
if server_error: if not ok:
return jsonify({"message": error_message}), Status.INTERNAL_SERVER_ERROR return jsonify({"message": error_message or "Unexpected backend error."}), Status.INTERNAL_SERVER_ERROR
return jsonify({"payment_hash": invoice.payment_hash}), Status.CREATED return jsonify({"checking_id": checking_id}), Status.CREATED
@core_app.route("/api/v1/payments", methods=["POST"]) @core_app.route("/api/v1/payments", methods=["POST"])
@ -89,10 +88,10 @@ def api_payments_create():
return api_payments_create_invoice() return api_payments_create_invoice()
@core_app.route("/api/v1/payments/<payhash>", methods=["GET"]) @core_app.route("/api/v1/payments/<checking_id>", methods=["GET"])
@api_check_wallet_macaroon(key_type="invoice") @api_check_wallet_macaroon(key_type="invoice")
def api_payment(payhash): def api_payment(checking_id):
payment = g.wallet.get_payment(payhash) payment = g.wallet.get_payment(checking_id)
if not payment: if not payment:
return jsonify({"message": "Payment does not exist."}), Status.NOT_FOUND return jsonify({"message": "Payment does not exist."}), Status.NOT_FOUND
@ -101,9 +100,9 @@ def api_payment(payhash):
try: try:
if payment.is_out: if payment.is_out:
is_paid = WALLET.get_payment_status(payhash).paid is_paid = WALLET.get_payment_status(checking_id).paid
elif payment.is_in: elif payment.is_in:
is_paid = WALLET.get_invoice_status(payhash).paid is_paid = WALLET.get_invoice_status(checking_id).paid
except Exception: except Exception:
return jsonify({"paid": False}), Status.OK return jsonify({"paid": False}), Status.OK

22
lnbits/core/views/lnurl.py

@ -14,12 +14,20 @@ from ..crud import create_account, get_user, create_wallet, create_payment
@core_app.route("/lnurlwallet") @core_app.route("/lnurlwallet")
def lnurlwallet(): def lnurlwallet():
memo = "LNbits LNURL funding"
try: try:
withdraw_res = handle_lnurl(request.args.get("lightning"), response_class=LnurlWithdrawResponse) withdraw_res = handle_lnurl(request.args.get("lightning"), response_class=LnurlWithdrawResponse)
except LnurlException: except LnurlException:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
_, payhash, payment_request = WALLET.create_invoice(withdraw_res.max_sats, "LNbits LNURL funding") try:
ok, checking_id, payment_request, error_message = WALLET.create_invoice(withdraw_res.max_sats, memo)
except Exception as e:
ok, error_message = False, str(e)
if not ok:
abort(Status.INTERNAL_SERVER_ERROR, error_message)
r = requests.get( r = requests.get(
withdraw_res.callback.base, withdraw_res.callback.base,
@ -30,16 +38,20 @@ def lnurlwallet():
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10): for i in range(10):
r = WALLET.get_invoice_status(payhash).raw_response invoice_status = WALLET.get_invoice_status(checking_id)
sleep(i) sleep(i)
if not r.ok: if not invoice_status.paid:
continue continue
break break
user = get_user(create_account().id) user = get_user(create_account().id)
wallet = create_wallet(user_id=user.id) wallet = create_wallet(user_id=user.id)
create_payment( # TODO: not pending? create_payment(
wallet_id=wallet.id, payhash=payhash, amount=withdraw_res.max_sats * 1000, memo="LNbits lnurl funding" wallet_id=wallet.id,
checking_id=checking_id,
amount=withdraw_res.max_sats * 1000,
memo=memo,
pending=invoice_status.pending,
) )
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))

10
lnbits/settings.py

@ -1,15 +1,13 @@
import importlib
import os import os
from .wallets import OpenNodeWallet # OR LndWallet OR OpennodeWallet
WALLET = OpenNodeWallet(endpoint=os.getenv("OPENNODE_API_ENDPOINT"),admin_key=os.getenv("OPENNODE_ADMIN_KEY"),invoice_key=os.getenv("OPENNODE_INVOICE_KEY"))
#WALLET = LntxbotWallet(endpoint=os.getenv("LNTXBOT_API_ENDPOINT"),admin_key=os.getenv("LNTXBOT_ADMIN_KEY"),invoice_key=os.getenv("LNTXBOT_INVOICE_KEY"))
#WALLET = LndWallet(endpoint=os.getenv("LND_API_ENDPOINT"),admin_macaroon=os.getenv("LND_ADMIN_MACAROON"),invoice_macaroon=os.getenv("LND_INVOICE_MACAROON"),read_macaroon=os.getenv("LND_READ_MACAROON"))
#WALLET = LNPayWallet(endpoint=os.getenv("LNPAY_API_ENDPOINT"),admin_key=os.getenv("LNPAY_ADMIN_KEY"),invoice_key=os.getenv("LNPAY_INVOICE_KEY"),api_key=os.getenv("LNPAY_API_KEY"),read_key=os.getenv("LNPAY_READ_KEY"))
wallets_module = importlib.import_module(f"lnbits.wallets")
wallet_class = getattr(wallets_module, os.getenv("LNBITS_BACKEND_WALLET_CLASS", "LntxbotWallet"))
LNBITS_PATH = os.path.dirname(os.path.realpath(__file__)) LNBITS_PATH = os.path.dirname(os.path.realpath(__file__))
LNBITS_DATA_FOLDER = os.getenv("LNBITS_DATA_FOLDER", os.path.join(LNBITS_PATH, "data")) LNBITS_DATA_FOLDER = os.getenv("LNBITS_DATA_FOLDER", os.path.join(LNBITS_PATH, "data"))
WALLET = wallet_class()
DEFAULT_WALLET_NAME = os.getenv("LNBITS_DEFAULT_WALLET_NAME", "LNbits wallet") DEFAULT_WALLET_NAME = os.getenv("LNBITS_DEFAULT_WALLET_NAME", "LNbits wallet")
FEE_RESERVE = float(os.getenv("LNBITS_FEE_RESERVE", 0)) FEE_RESERVE = float(os.getenv("LNBITS_FEE_RESERVE", 0))

6
lnbits/static/js/base.js

@ -67,7 +67,7 @@ var LNbits = {
}, },
payment: function (data) { payment: function (data) {
var obj = _.object(['payhash', 'pending', 'amount', 'fee', 'memo', 'time'], data); var obj = _.object(['payhash', 'pending', 'amount', 'fee', 'memo', 'time'], data);
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm') obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.msat = obj.amount; obj.msat = obj.amount;
obj.sat = obj.msat / 1000; obj.sat = obj.msat / 1000;
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat); obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat);
@ -86,9 +86,9 @@ var LNbits = {
400: 'warning', 400: 'warning',
401: 'warning', 401: 'warning',
500: 'negative' 500: 'negative'
} };
Quasar.plugins.Notify.create({ Quasar.plugins.Notify.create({
timeout: 3000, timeout: 5000,
type: types[error.response.status] || 'warning', type: types[error.response.status] || 'warning',
message: error.response.data.message || null, message: error.response.data.message || null,
caption: [error.response.status, ' ', error.response.statusText].join('').toUpperCase() || null, caption: [error.response.status, ' ', error.response.statusText].join('').toUpperCase() || null,

15
lnbits/wallets/base.py

@ -1,23 +1,22 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from requests import Response
from typing import NamedTuple, Optional from typing import NamedTuple, Optional
class InvoiceResponse(NamedTuple): class InvoiceResponse(NamedTuple):
raw_response: Response ok: bool
payment_hash: Optional[str] = None checking_id: Optional[str] = None # payment_hash, rpc_id
payment_request: Optional[str] = None payment_request: Optional[str] = None
error_message: Optional[str] = None
class PaymentResponse(NamedTuple): class PaymentResponse(NamedTuple):
raw_response: Response ok: bool
failed: bool = False checking_id: Optional[str] = None # payment_hash, rcp_id
fee_msat: int = 0 fee_msat: int = 0
error_message: Optional[str] = None error_message: Optional[str] = None
class PaymentStatus(NamedTuple): class PaymentStatus(NamedTuple):
raw_response: Response
paid: Optional[bool] = None paid: Optional[bool] = None
@property @property
@ -35,9 +34,9 @@ class Wallet(ABC):
pass pass
@abstractmethod @abstractmethod
def get_invoice_status(self, payment_hash: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
pass pass
@abstractmethod @abstractmethod
def get_payment_status(self, payment_hash: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
pass pass

46
lnbits/wallets/lnd.py

@ -1,36 +1,38 @@
from os import getenv
from requests import get, post from requests import get, post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LndWallet(Wallet): class LndWallet(Wallet):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" """https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
def __init__(self, *, endpoint: str, admin_macaroon: str, invoice_macaroon: str, read_macaroon: str): def __init__(self):
endpoint = getenv("LND_API_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = {"Grpc-Metadata-macaroon": admin_macaroon} self.auth_admin = {"Grpc-Metadata-macaroon": getenv("LND_ADMIN_MACAROON")}
self.auth_invoice = {"Grpc-Metadata-macaroon": invoice_macaroon} self.auth_invoice = {"Grpc-Metadata-macaroon": getenv("LND_INVOICE_MACAROON")}
self.auth_read = {"Grpc-Metadata-macaroon": read_macaroon} self.auth_read = {"Grpc-Metadata-macaroon": getenv("LND_READ_MACAROON")}
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
payment_hash, payment_request = None, None
r = post( r = post(
url=f"{self.endpoint}/v1/invoices", url=f"{self.endpoint}/v1/invoices",
headers=self.auth_admin, headers=self.auth_admin,
verify=False, verify=False,
json={"value": amount, "memo": memo, "private": True}, json={"value": amount, "memo": memo, "private": True},
) )
ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.ok: if r.ok:
data = r.json() data = r.json()
payment_request = data["payment_request"] payment_request = data["payment_request"]
rr = get(url=f"{self.endpoint}/v1/payreq/{payment_request}", headers=self.auth_read, verify=False,) rr = get(url=f"{self.endpoint}/v1/payreq/{payment_request}", headers=self.auth_read, verify=False)
if rr.ok: if rr.ok:
dataa = rr.json() checking_id = rr.json()["payment_hash"]
payment_hash = dataa["payment_hash"]
return InvoiceResponse(r, payment_hash, payment_request) return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post( r = post(
@ -39,17 +41,25 @@ class LndWallet(Wallet):
verify=False, verify=False,
json={"payment_request": bolt11}, json={"payment_request": bolt11},
) )
return PaymentResponse(r, not r.ok) ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
data = r.json()["data"]
if "payment_error" in data and data["payment_error"]:
ok, error_message = False, data["payment_error"]
else:
checking_id = data["payment_hash"]
return PaymentResponse(ok, checking_id, fee_msat, error_message)
def get_invoice_status(self, payment_hash: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/v1/invoice/{payment_hash}", headers=self.auth_read, verify=False) r = get(url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth_read, verify=False)
if not r.ok or "settled" not in r.json(): if not r.ok or "settled" not in r.json():
return PaymentStatus(r, None) return PaymentStatus(None)
return PaymentStatus(r, r.json()["settled"]) return PaymentStatus(r.json()["settled"])
def get_payment_status(self, payment_hash: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get( r = get(
url=f"{self.endpoint}/v1/payments", url=f"{self.endpoint}/v1/payments",
headers=self.auth_admin, headers=self.auth_admin,
@ -58,11 +68,11 @@ class LndWallet(Wallet):
) )
if not r.ok: if not r.ok:
return PaymentStatus(r, None) return PaymentStatus(None)
payments = [p for p in r.json()["payments"] if p["payment_hash"] == payment_hash] payments = [p for p in r.json()["payments"] if p["payment_hash"] == checking_id]
payment = payments[0] if payments else None payment = payments[0] if payments else None
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype # check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False} statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
return PaymentStatus(r, statuses[payment["status"]] if payment else None) return PaymentStatus(statuses[payment["status"]] if payment else None)

39
lnbits/wallets/lnpay.py

@ -1,3 +1,4 @@
from os import getenv
from requests import get, post from requests import get, post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -6,27 +7,27 @@ from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LNPayWallet(Wallet): class LNPayWallet(Wallet):
"""https://docs.lnpay.co/""" """https://docs.lnpay.co/"""
def __init__(self, *, endpoint: str, admin_key: str, invoice_key: str, api_key: str, read_key: str): def __init__(self):
endpoint = getenv("LNPAY_API_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = admin_key self.auth_admin = getenv("LNPAY_ADMIN_KEY")
self.auth_invoice = invoice_key self.auth_invoice = getenv("LNPAY_INVOICE_KEY")
self.auth_read = read_key self.auth_read = getenv("LNPAY_READ_KEY")
self.auth_api = {"X-Api-Key": api_key} self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")}
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
payment_hash, payment_request = None, None
r = post( r = post(
url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice", url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice",
headers=self.auth_api, headers=self.auth_api,
json={"num_satoshis": f"{amount}", "memo": memo}, json={"num_satoshis": f"{amount}", "memo": memo},
) )
ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, None
if r.ok: if ok:
data = r.json() data = r.json()
payment_hash, payment_request = data["id"], data["payment_request"] checking_id, payment_request = data["id"], data["payment_request"]
return InvoiceResponse(r, payment_hash, payment_request) return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post( r = post(
@ -34,17 +35,21 @@ class LNPayWallet(Wallet):
headers=self.auth_api, headers=self.auth_api,
json={"payment_request": bolt11}, json={"payment_request": bolt11},
) )
ok, checking_id, fee_msat, error_message = r.status_code == 201, None, 0, None
if ok:
checking_id = r.json()["lnTx"]["id"]
return PaymentResponse(r, not r.ok) return PaymentResponse(ok, checking_id, fee_msat, error_message)
def get_invoice_status(self, payment_hash: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return self.get_payment_status(payment_hash) return self.get_payment_status(checking_id)
def get_payment_status(self, payment_hash: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/user/lntx/{payment_hash}", headers=self.auth_api) r = get(url=f"{self.endpoint}/user/lntx/{checking_id}", headers=self.auth_api)
if not r.ok: if not r.ok:
return PaymentStatus(r, None) return PaymentStatus(None)
statuses = {0: None, 1: True, -1: False} statuses = {0: None, 1: True, -1: False}
return PaymentStatus(r, statuses[r.json()["settled"]]) return PaymentStatus(statuses[r.json()["settled"]])

55
lnbits/wallets/lntxbot.py

@ -1,3 +1,4 @@
from os import getenv
from requests import post from requests import post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -6,56 +7,58 @@ from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class LntxbotWallet(Wallet): class LntxbotWallet(Wallet):
"""https://github.com/fiatjaf/lntxbot/blob/master/api.go""" """https://github.com/fiatjaf/lntxbot/blob/master/api.go"""
def __init__(self, *, endpoint: str, admin_key: str, invoice_key: str): def __init__(self):
endpoint = getenv("LNTXBOT_API_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = {"Authorization": f"Basic {admin_key}"} self.auth_admin = {"Authorization": f"Basic {getenv('LNTXBOT_ADMIN_KEY')}"}
self.auth_invoice = {"Authorization": f"Basic {invoice_key}"} self.auth_invoice = {"Authorization": f"Basic {getenv('LNTXBOT_INVOICE_KEY')}"}
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: 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}) r = post(url=f"{self.endpoint}/addinvoice", headers=self.auth_invoice, json={"amt": str(amount), "memo": memo})
ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.ok: if r.ok:
data = r.json() data = r.json()
payment_hash, payment_request = data["payment_hash"], data["pay_req"] checking_id, payment_request = data["payment_hash"], data["pay_req"]
return InvoiceResponse(r, payment_hash, payment_request) if "error" in data and data["error"]:
ok = False
error_message = data["message"]
return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post(url=f"{self.endpoint}/payinvoice", headers=self.auth_admin, json={"invoice": bolt11}) r = post(url=f"{self.endpoint}/payinvoice", headers=self.auth_admin, json={"invoice": bolt11})
failed, fee_msat, error_message = not r.ok, 0, None ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
if r.ok: if r.ok:
data = r.json() data = r.json()
if "error" in data and data["error"]:
failed = True
error_message = data["message"]
elif "fee_msat" in data:
fee_msat = data["fee_msat"]
return PaymentResponse(r, failed, fee_msat, error_message) if "payment_hash" in data:
checking_id, fee_msat = data["decoded"]["payment_hash"], data["fee_msat"]
elif "error" in data and data["error"]:
ok, error_message = False, data["message"]
def get_invoice_status(self, payment_hash: str) -> PaymentStatus: return PaymentResponse(ok, checking_id, fee_msat, error_message)
r = post(url=f"{self.endpoint}/invoicestatus/{payment_hash}?wait=false", headers=self.auth_invoice)
if not r.ok: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(r, None) r = post(url=f"{self.endpoint}/invoicestatus/{checking_id}?wait=false", headers=self.auth_invoice)
data = r.json() if not r.ok or "error" in r.json():
return PaymentStatus(None)
if "error" in data: data = r.json()
return PaymentStatus(r, None)
if "preimage" not in data or not data["preimage"]: if "preimage" not in data or not data["preimage"]:
return PaymentStatus(r, False) return PaymentStatus(False)
return PaymentStatus(r, True) return PaymentStatus(True)
def get_payment_status(self, payment_hash: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = post(url=f"{self.endpoint}/paymentstatus/{payment_hash}", headers=self.auth_invoice) r = post(url=f"{self.endpoint}/paymentstatus/{checking_id}", headers=self.auth_invoice)
if not r.ok or "error" in r.json(): if not r.ok or "error" in r.json():
return PaymentStatus(r, None) return PaymentStatus(None)
statuses = {"complete": True, "failed": False, "pending": None, "unknown": None} statuses = {"complete": True, "failed": False, "pending": None, "unknown": None}
return PaymentStatus(r, statuses[r.json().get("status", "unknown")]) return PaymentStatus(statuses[r.json().get("status", "unknown")])

47
lnbits/wallets/opennode.py

@ -1,47 +1,60 @@
from os import getenv
from requests import get, post from requests import get, post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
class OpenNodeWallet(Wallet): class OpenNodeWallet(Wallet):
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" """https://developers.opennode.com/"""
def __init__(self, *, endpoint: str, admin_key: str, invoice_key: str): def __init__(self):
endpoint = getenv("OPENNODE_API_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = {"Authorization": admin_key} self.auth_admin = {"Authorization": getenv("OPENNODE_ADMIN_KEY")}
self.auth_invoice = {"Authorization": invoice_key} self.auth_invoice = {"Authorization": getenv("OPENNODE_INVOICE_KEY")}
def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse:
payment_hash, payment_request = None, None
r = post( r = post(
url=f"{self.endpoint}/v1/charges", url=f"{self.endpoint}/v1/charges",
headers=self.auth_invoice, headers=self.auth_invoice,
json={"amount": f"{amount}", "description": memo}, # , "private": True}, json={"amount": f"{amount}", "description": memo}, # , "private": True},
) )
ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.ok: if r.ok:
data = r.json() data = r.json()["data"]
payment_hash, payment_request = data["data"]["id"], data["data"]["lightning_invoice"]["payreq"] checking_id, payment_request = data["id"], data["lightning_invoice"]["payreq"]
else:
error_message = r.json()["message"]
return InvoiceResponse(r, payment_hash, payment_request) return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11}) r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11})
return PaymentResponse(r, not r.ok) ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
if r.ok:
data = r.json()["data"]
checking_id, fee_msat = data["id"], data["fee"] * 1000
else:
error_message = r.json()["message"]
return PaymentResponse(ok, checking_id, fee_msat, error_message)
def get_invoice_status(self, payment_hash: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/v1/charge/{payment_hash}", headers=self.auth_invoice) r = get(url=f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
if not r.ok: if not r.ok:
return PaymentStatus(r, None) return PaymentStatus(None)
statuses = {"processing": None, "paid": True, "unpaid": False} statuses = {"processing": None, "paid": True, "unpaid": False}
return PaymentStatus(r, statuses[r.json()["data"]["status"]]) return PaymentStatus(statuses[r.json()["data"]["status"]])
def get_payment_status(self, payment_hash: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/v1/withdrawal/{payment_hash}", headers=self.auth_admin) r = get(url=f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin)
if not r.ok: if not r.ok:
return PaymentStatus(r, None) return PaymentStatus(None)
statuses = {"pending": None, "confirmed": True, "error": False, "failed": False} statuses = {"pending": None, "confirmed": True, "error": False, "failed": False}
return PaymentStatus(r, statuses[r.json()["data"]["status"]]) return PaymentStatus(statuses[r.json()["data"]["status"]])

4
requirements.txt

@ -2,7 +2,7 @@ bech32==1.2.0
bitstring==3.1.6 bitstring==3.1.6
certifi==2019.11.28 certifi==2019.11.28
chardet==3.0.4 chardet==3.0.4
click==7.0 click==7.1.1
flask-assets==2.0 flask-assets==2.0
flask-compress==1.4.0 flask-compress==1.4.0
flask-limiter==1.2.1 flask-limiter==1.2.1
@ -19,7 +19,7 @@ limits==1.5.1
lnurl==0.3.3 lnurl==0.3.3
markupsafe==1.1.1 markupsafe==1.1.1
pydantic==1.4 pydantic==1.4
pyscss==1.3.5 pyscss==1.3.7
requests==2.23.0 requests==2.23.0
six==1.14.0 six==1.14.0
typing-extensions==3.7.4.1 ; python_version < '3.8' typing-extensions==3.7.4.1 ; python_version < '3.8'

Loading…
Cancel
Save