payment logic safer and adapt db calls to new schema.

This commit is contained in:
fiatjaf 2019-12-13 22:59:35 -03:00
parent 754782b598
commit 582aaa812b
2 changed files with 95 additions and 60 deletions

View File

@ -1,10 +1,10 @@
import os import os
import lnurl import lnurl
import requests import requests
import time
from flask import Flask, jsonify, render_template, request from flask import Flask, jsonify, render_template, request
from . import bolt11
from .db import Database from .db import Database
from .helpers import encrypt from .helpers import encrypt
from .settings import INVOICE_KEY, ADMIN_KEY, API_ENDPOINT, DATABASE_PATH, LNBITS_PATH from .settings import INVOICE_KEY, ADMIN_KEY, API_ENDPOINT, DATABASE_PATH, LNBITS_PATH
@ -115,7 +115,7 @@ def lnurlwallet():
inkey = encrypt(adminkey) inkey = encrypt(adminkey)
cur.execute( cur.execute(
"INSERT INTO wallets (hash, balance, transactions, name, user, adminkey, inkey) VALUES ('" "INSERT INTO wallets (hash, name, user, adminkey, inkey) VALUES ('"
+ thewal + thewal
+ "',',0," + "',',0,"
+ str(withdraw) + str(withdraw)
@ -197,8 +197,7 @@ def wallet():
inkey = encrypt(adminkey) inkey = encrypt(adminkey)
db.execute( db.execute(
"INSERT INTO wallets (hash, balance, transactions, name, user, adminkey, inkey) " "INSERT INTO wallets (hash, name, user, adminkey, inkey) VALUES (?, ?, ?, ?, ?)",
"VALUES (?, 0, 0, ?, ?, ?, ?)",
(thewal, thenme, theid, adminkey, inkey), (thewal, thenme, theid, adminkey, inkey),
) )
rows = db.fetchall("SELECT * FROM wallets WHERE user = ?", (theid,)) rows = db.fetchall("SELECT * FROM wallets WHERE user = ?", (theid,))
@ -222,8 +221,7 @@ def wallet():
inkey = encrypt(adminkey) inkey = encrypt(adminkey)
db.execute( db.execute(
"INSERT INTO wallets (hash, balance, transactions, name, user, adminkey, inkey) " "INSERT INTO wallets (hash, name, user, adminkey, inkey) " "VALUES (?, ?, ?, ?, ?)",
"VALUES (?, 0, 0, ?, ?, ?, ?)",
(thewal, thenme, theid, adminkey, inkey), (thewal, thenme, theid, adminkey, inkey),
) )
@ -247,8 +245,7 @@ def wallet():
inkey = encrypt(adminkey) inkey = encrypt(adminkey)
db.execute( db.execute(
"INSERT INTO wallets (hash, balance, transactions, name, user, adminkey, inkey) " "INSERT INTO wallets (hash, name, user, adminkey, inkey) " "VALUES (?, ?, ?, ?, ?)",
"VALUES (?, 0, 0, ?, ?, ?, ?)",
(thewal, thenme, theid, adminkey, inkey), (thewal, thenme, theid, adminkey, inkey),
) )
@ -281,28 +278,27 @@ def api_invoices():
return jsonify({"ERROR": "NO MEMO"}), 200 return jsonify({"ERROR": "NO MEMO"}), 200
with Database() as db: with Database() as db:
rows = db.fetchall("SELECT * FROM wallets WHERE inkey = ?", (request.headers["Grpc-Metadata-macaroon"],)) wallet_row = db.fetchone(
"SELECT hash FROM wallets WHERE inkey = ? OR adminkey = ?",
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"],),
)
if not rows: if not wallet_row:
return jsonify({"ERROR": "NO KEY"}), 200 return jsonify({"ERROR": "NO KEY"}), 200
dataj = {"amt": postedjson["value"], "memo": postedjson["memo"]} r = requests.post(
headers = {"Authorization": f"Basic {INVOICE_KEY}"} url=f"{API_ENDPOINT}/addinvoice",
r = requests.post(url=f"{API_ENDPOINT}/addinvoice", json=dataj, headers=headers) json={"amt": postedjson["value"], "memo": postedjson["memo"]},
headers={"Authorization": f"Basic {INVOICE_KEY}"},
)
data = r.json() data = r.json()
pay_req = data["pay_req"] pay_req = data["pay_req"]
payment_hash = data["payment_hash"] payment_hash = data["payment_hash"]
db.execute( db.execute(
"INSERT INTO apipayments (payhash, amount, wallet, paid, inkey, memo) VALUES (?, ?, ?, 0, ?, ?)", "INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, true, ?)",
( (payment_hash, postedjson["value"] * 1000, wallet_row[0], postedjson["memo"],),
payment_hash,
postedjson["value"],
rows[0][0],
request.headers["Grpc-Metadata-macaroon"],
postedjson["memo"],
),
) )
return jsonify({"pay_req": pay_req, "payment_hash": payment_hash}), 200 return jsonify({"pay_req": pay_req, "payment_hash": payment_hash}), 200
@ -313,15 +309,17 @@ 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"}), 200
postedjson = request.json data = request.json
if "payment_request" not in postedjson: if "payment_request" not in data:
return jsonify({"ERROR": "NO PAY REQ"}), 200 return jsonify({"ERROR": "NO PAY REQ"}), 200
with Database() as db: with Database() as db:
wallets = db.fetchall("SELECT * FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],)) wallet_row = db.fetchone(
"SELECT hash FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],)
)
if not wallets: if not wallet_row:
return jsonify({"ERROR": "BAD AUTH"}), 200 return jsonify({"ERROR": "BAD AUTH"}), 200
# TODO: check this unused code # TODO: check this unused code
@ -350,31 +348,65 @@ def api_transactions():
""" """
# --------------------------------- # ---------------------------------
dataj = {"invoice": postedjson["payment_request"]} # decode the invoice
headers = {"Authorization": f"Basic {ADMIN_KEY}"} invoice = bolt11.decode(data["payment_request"])
r = requests.post(url=f"{API_ENDPOINT}/payinvoice", json=dataj, headers=headers)
data = r.json()
# insert the payment
db.execute( db.execute(
"INSERT INTO apipayments (payhash, amount, wallet, paid, adminkey, memo) VALUES (?, ?, ?, 1, ?, ?)'", "INSERT INTO apipayments (payhash, amount, fee, wallet, pending, memo) VALUES (?, ?, ?, true, ?)'",
( (
data["decoded"]["payment_hash"], invoice.payment_hash,
str(-int(data["decoded"]["num_satoshis"])), -int(invoice.amount_msat),
wallets[0][0], -int(invoice.amount_msat * 0.01),
request.headers["Grpc-Metadata-macaroon"], wallet_row[0],
data["decoded"]["description"], invoice.description,
), ),
) )
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (data["decoded"]["payment_hash"],))[0] # check balance
balance = db.fetchone(
"""
WITH wallettransactions AS (
SELECT *
FROM apipayments
WHERE wallet = ?
)
lastamt = str(wallets[0][1]).split(",") SELECT sum(s) FROM (
newamt = int(lastamt[-1]) - int(data["decoded"]["num_satoshis"]) SELECT sum(amount) AS s -- incoming
updamt = wallets[0][1] + "," + str(newamt) FROM wallettransactions
transactions = f"{wallets[0][2]}!{payment[5]},{time.time()},{payment[1]},{newamt}" WHERE amount > 0 AND pending = false -- don't sum pending
UNION ALL
SELECT sum(amount + fee) AS s -- outgoing, sum fees
FROM wallettransactions
WHERE amount < 0 -- do sum pending
)
""",
(wallet_row[0],),
)
if balance < 0:
return jsonify({"ERROR": "INSUFFICIENT BALANCE"}), 403
# actually send the payment
r = requests.post(
url=f"{API_ENDPOINT}/payinvoice",
json={"invoice": data["payment_request"]},
headers={"Authorization": f"Basic {ADMIN_KEY}"},
)
if not r.ok:
return jsonify({"ERROR": "UNEXPECTED PAYMENT ERROR"}), 500
data = r.json()
if r.ok and data["error"]:
# payment didn't went through, delete it here
# (these guarantees specific to lntxbot)
db.execute("DELETE FROM apipayments WHERE payhash = ?", (invoice.payment_hash,))
return jsonify({"PAID": "FALSE"}), 200
# payment went through, not pending anymore, save actual fees
db.execute( db.execute(
"UPDATE wallets SET balance = ?, transactions = ? WHERE hash = ?", (updamt, transactions, wallets[0][0]) "UPDATE apipayments SET pending = false, fee = ? WHERE payhash = ?",
(data["fee_msat"], invoice.payment_hash,),
) )
return jsonify({"PAID": "TRUE"}), 200 return jsonify({"PAID": "TRUE"}), 200
@ -386,31 +418,30 @@ def api_checkinvoice(payhash):
return jsonify({"ERROR": "MUST BE JSON"}), 200 return jsonify({"ERROR": "MUST BE JSON"}), 200
with Database() as db: with Database() as db:
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (payhash,))[0] payment_row = db.fetchone(
"""
SELECT pending FROM apipayments
INNER JOIN wallets AS w ON apipayments.wallet = w.hash
WHERE payhash = ?
AND (w.adminkey = ? OR w.invkey = ?)
""",
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
)
if request.headers["Grpc-Metadata-macaroon"] != payment[4]: if not payment_row:
return jsonify({"ERROR": "WRONG KEY"}), 400 return jsonify({"ERROR": "NO INVOICE"}), 404
if payment[3] != "0": if not payment_row[0]: # pending
return jsonify({"PAID": "TRUE"}), 200 return jsonify({"PAID": "TRUE"}), 200
headers = {"Authorization": f"Basic {INVOICE_KEY}"} headers = {"Authorization": f"Basic {INVOICE_KEY}"}
r = requests.post(url=f"{API_ENDPOINT}/invoicestatus/{payhash}", headers=headers) r = requests.post(url=f"{API_ENDPOINT}/invoicestatus/{payhash}", headers=headers)
data = r.json() if not r.ok:
if data == "":
return jsonify({"PAID": "FALSE"}), 400 return jsonify({"PAID": "FALSE"}), 400
wallet = db.fetchall("SELECT * FROM wallets WHERE hash = ?", (payment[2],))[0] data = r.json()
if "preimage" not in data or not data["preimage"]:
return jsonify({"PAID": "FALSE"}), 400
lastamt = wallet[1].split(",") db.execute("UPDATE apipayments SET pending = false WHERE payhash = ?", (payhash,))
newamt = int(lastamt[-1]) + int(payment[1]) return jsonify({"PAID": "TRUE"}), 200
updamt = wallet[1] + "," + str(newamt)
transactions = f"{wallet[2]}!{payment[5]},{time.time()},{payment[1]},{newamt}"
db.execute(
"UPDATE wallets SET balance = ?, transactions = ? WHERE hash = ?", (updamt, transactions, payment[2])
)
db.execute("UPDATE apipayments SET paid = '1' WHERE payhash = ?", (payhash,))
return jsonify({"PAID": "TRUE"}), 200

View File

@ -21,6 +21,10 @@ class Database:
self.cursor.execute(query, values) self.cursor.execute(query, values)
return self.cursor.fetchall() return self.cursor.fetchall()
def fetchone(self, query: str, values: tuple):
self.cursor.execute(query, values)
return self.cursor.fetchone()
def execute(self, query: str, values: tuple) -> None: def execute(self, query: str, values: tuple) -> None:
"""Given a query, cursor.execute() it.""" """Given a query, cursor.execute() it."""
self.cursor.execute(query, values) self.cursor.execute(query, values)